Skip to content

Commit

Permalink
feat: landing and login (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
qin-guan authored Aug 26, 2023
1 parent 993b881 commit 07123ca
Show file tree
Hide file tree
Showing 38 changed files with 3,741 additions and 1,424 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"extends": [
"@antfu"
]
],
"rules": {
// https://github.com/antfu/eslint-config/pull/214
"n/prefer-global/process": [
"off",
"never"
]
}
}
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: ci

permissions:
contents: read

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch: {}

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile

- name: 🚧 Set up project
run: pnpm nuxi prepare

- name: 📝 Lint
run: pnpm lint

- name: 💪 Type check
run: pnpm typecheck
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ logs

# SQLite files
*.db

# ESLint
.eslintcache
6 changes: 6 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default defineAppConfig({
ui: {
primary: 'teal',
gray: 'neutral',
},
})
3 changes: 2 additions & 1 deletion app.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<template>
<div>
<div class="dark:bg-neutral-900 h-screen">
<VitePwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
</div>
</template>
81 changes: 81 additions & 0 deletions components/onboarding/Setup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import Step01 from './SetupStep01.vue'
const router = useRouter()
const step = computed({
get() {
try {
return Number.parseInt(router.currentRoute.value.query.step as string ?? '-1')
}
catch {
return -1
}
},
set(value: number) {
router.push({
query: {
step: value,
},
})
},
})
const steps = [
Step01,
]
function done() {
if (step.value === steps.length - 1)
router.push('/login')
else
step.value++
}
</script>

<template>
<section class="py-16">
<div class="h-16">
<UButton v-if="step > -1" label="Back" color="gray" @click="step--">
<template #leading>
<UIcon name="i-tabler-arrow-left" />
</template>
</UButton>
</div>

<Transition appear>
<div v-if="step === -1" class="space-y-6">
<h1 class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Your private money tracker.
</h1>
<p class="leading-7 text-neutral-400">
Private, not straightforward and definitely easy to use. We promise you'll have a fun time here!
</p>

<UButton @click="step++">
Begin setup
</UButton>
</div>

<component :is="steps[step]" v-else @done="done" />
</Transition>
</section>
</template>

<style scoped>
.v-move,
.v-enter-active,
.v-leave-active {
transition: all 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: translateX(20px);
}
.v-leave-active {
position: absolute;
}
</style>
92 changes: 92 additions & 0 deletions components/onboarding/SetupStep01.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup lang="ts">
import { z } from 'zod'
import type { TRPCClientError } from '~/shared/types'
const emit = defineEmits(['done'])
const { $client } = useNuxtApp()
const toast = useToast()
const schema = z.object({
username: z.string().nonempty('Username must not be empty'),
password: z.string().nonempty('Password must not be empty'),
})
const state = ref<{
pending: boolean
error: {
username?: string
password?: string
}
}>({
pending: false,
error: {},
})
async function create(event: Event) {
state.value.error = {}
state.value.pending = true
const formData = new FormData(event.currentTarget as HTMLFormElement)
const result = await schema.safeParseAsync({
username: formData.get('username'),
password: formData.get('password'),
})
if (!result.success) {
if (result.error.formErrors.fieldErrors.password)
state.value.error.password = result.error.formErrors.fieldErrors.password[0]
if (result.error.formErrors.fieldErrors.username)
state.value.error.username = result.error.formErrors.fieldErrors.username[0]
state.value.pending = false
return
}
try {
await $client.onboarding.createInitialUser.mutate({
username: formData.get('username') as string,
password: formData.get('password') as string,
})
emit('done')
}
catch (_err) {
const err = _err as TRPCClientError
toast.add({
title: 'Error creating user',
description: err.data?.code,
color: 'rose',
})
}
finally {
state.value.pending = false
}
}
</script>

<template>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div class="space-y-6">
<h1 class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Create a new user
</h1>
<p class="leading-7 text-neutral-400">
This user will have full permissions. You can create more users later.
</p>
</div>

<form class="space-y-4" @submit.prevent="create">
<UFormGroup label="Username" :error="state.error.username">
<UInput name="username" placeholder="rickastley" />
</UFormGroup>
<UFormGroup label="Password" :error="state.error.password">
<UInput name="password" type="password" />
</UFormGroup>
<br>
<UButton type="submit" :loading="state.pending">
Create
</UButton>
</form>
</div>
</template>
6 changes: 6 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'drizzle-kit'

export default {
schema: './server/db/schema.ts',
out: './server/drizzle',
} satisfies Config
27 changes: 27 additions & 0 deletions middleware/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// For simplicity sake, just block navigation until this resolves.
// Should not be a big matter since onboarding only runs once and
// users should not go back to this page again.
export default defineNuxtRouteMiddleware(async (to) => {
const { $client } = useNuxtApp()
const { data: status } = await $client.onboarding.status.useQuery()
const { data: me } = await $client.auth.me.useQuery()

if (to.path === '/') {
if (!status.value?.completed)
return navigateTo('/onboarding')
if (!me.value?.session)
return navigateTo('/login')
}

if (to.path === '/onboarding') {
if (status.value?.completed)
return navigateTo('/')
}

if (to.path === '/login') {
if (!status.value?.completed)
return navigateTo('/onboarding')
if (me.value?.session)
return navigateTo('/')
}
})
46 changes: 43 additions & 3 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import defaultTheme from 'tailwindcss/defaultTheme'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },

nitro: {
esbuild: {
options: {
target: 'esnext',
},
},
moduleSideEffects: ['lucia/polyfill/node'],
},

modules: [
'@vueuse/nuxt',
'@vite-pwa/nuxt',
'@nuxthq/ui',
'@nuxtjs/google-fonts',
'@nuxtjs/fontaine',
],

build: {
transpile: [
'trpc-nuxt',
],
},

tailwindcss: {
exposeConfig: true,
config: {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'Inter fallback', ...defaultTheme.fontFamily.sans],
},
},
},
},
},

ui: {
icons: [
'tabler',
],
},

googleFonts: {
families: {
Inter: [400, 600, 800],
},
},

pwa: {
registerType: 'autoUpdate',
manifest: {
Expand Down Expand Up @@ -48,9 +86,7 @@ export default defineNuxtConfig({
periodicSyncForUpdates: 20,
},
devOptions: {
enabled: true,
suppressWarnings: true,
navigateFallbackAllowlist: [/^\/$/],
enabled: process.env.VITE_DEV_PWA === 'true',
type: 'module',
},
},
Expand All @@ -60,4 +96,8 @@ export default defineNuxtConfig({
sqliteFileName: 'sqlite.db',
},
},

typescript: {
strict: true,
},
})
Loading

0 comments on commit 07123ca

Please sign in to comment.