@stjoseph/prumo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prumo-templates/api/auth.md +130 -0
- package/.prumo-templates/api/config.md +85 -0
- package/.prumo-templates/api/errors.md +82 -0
- package/.prumo-templates/api/logging.md +79 -0
- package/.prumo-templates/api/modules.md +117 -0
- package/.prumo-templates/api/observability.md +91 -0
- package/.prumo-templates/api/pagination.md +71 -0
- package/.prumo-templates/api/routes.md +89 -0
- package/.prumo-templates/client/data.md +124 -0
- package/.prumo-templates/client/forms.md +88 -0
- package/.prumo-templates/core/conventions.md +74 -0
- package/.prumo-templates/core/testing.md +100 -0
- package/.prumo-templates/core/tooling.md +118 -0
- package/.prumo-templates/database/entities.md +97 -0
- package/.prumo-templates/database/migrations.md +74 -0
- package/.prumo-templates/database/queries.md +79 -0
- package/.prumo-templates/database/testing.md +82 -0
- package/.prumo-templates/database/transactions.md +68 -0
- package/.prumo-templates/mobile/components.md +83 -0
- package/.prumo-templates/mobile/config.md +84 -0
- package/.prumo-templates/mobile/routing.md +74 -0
- package/.prumo-templates/mobile/storage.md +84 -0
- package/.prumo-templates/mobile/structure.md +79 -0
- package/.prumo-templates/monorepo/dependencies.md +84 -0
- package/.prumo-templates/monorepo/layout.md +89 -0
- package/.prumo-templates/monorepo/tasks.md +76 -0
- package/.prumo-templates/multi-tenancy/client.md +83 -0
- package/.prumo-templates/multi-tenancy/isolation.md +104 -0
- package/.prumo-templates/site/rendering.md +83 -0
- package/.prumo-templates/site/structure.md +68 -0
- package/.prumo-templates/web/components.md +106 -0
- package/.prumo-templates/web/routing.md +82 -0
- package/.prumo-templates/web/structure.md +92 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/asker.js +28 -0
- package/dist/cli.js +62 -0
- package/dist/compose.js +222 -0
- package/dist/context.js +38 -0
- package/dist/generate.js +60 -0
- package/dist/jsonc.js +76 -0
- package/dist/names.js +10 -0
- package/dist/questions.js +63 -0
- package/package.json +55 -0
- package/templates/api/.env.example +12 -0
- package/templates/api/.githooks/install.mjs +18 -0
- package/templates/api/.githooks/pre-commit +2 -0
- package/templates/api/.githooks/pre-push +2 -0
- package/templates/api/README.md +35 -0
- package/templates/api/biome.jsonc +18 -0
- package/templates/api/docker/init/01-auth-schema.sql +1 -0
- package/templates/api/docker-compose.yml +20 -0
- package/templates/api/gitignore +5 -0
- package/templates/api/migrations/Migration20260914123239_create_profile.ts +13 -0
- package/templates/api/nest-cli.json +10 -0
- package/templates/api/package.json +69 -0
- package/templates/api/pnpm-workspace.yaml +9 -0
- package/templates/api/src/app.module.ts +46 -0
- package/templates/api/src/auth/auth.constants.ts +2 -0
- package/templates/api/src/auth/auth.controller.ts +19 -0
- package/templates/api/src/auth/auth.factory.spec.ts +29 -0
- package/templates/api/src/auth/auth.factory.ts +52 -0
- package/templates/api/src/auth/auth.guard.ts +43 -0
- package/templates/api/src/auth/auth.module.ts +30 -0
- package/templates/api/src/auth/auth.ts +4 -0
- package/templates/api/src/auth/current-user.decorator.ts +9 -0
- package/templates/api/src/auth/express.d.ts +10 -0
- package/templates/api/src/auth/public.decorator.ts +4 -0
- package/templates/api/src/config/env.module.ts +11 -0
- package/templates/api/src/config/env.ts +81 -0
- package/templates/api/src/errors/problem-details.filter.ts +86 -0
- package/templates/api/src/errors/validation-exception.factory.ts +30 -0
- package/templates/api/src/health/health.controller.ts +32 -0
- package/templates/api/src/health/health.module.ts +9 -0
- package/templates/api/src/main.ts +43 -0
- package/templates/api/src/mikro-orm.config.ts +4 -0
- package/templates/api/src/mikro-orm.factory.ts +14 -0
- package/templates/api/src/request-context/request-context.module.ts +9 -0
- package/templates/api/src/request-context/request-context.service.ts +17 -0
- package/templates/api/src/request-context/request-id.middleware.ts +19 -0
- package/templates/api/src/users/dto/update-profile.dto.ts +20 -0
- package/templates/api/src/users/entities/profile.entity.ts +27 -0
- package/templates/api/src/users/users.controller.ts +23 -0
- package/templates/api/src/users/users.module.ts +10 -0
- package/templates/api/src/users/users.service.spec.ts +44 -0
- package/templates/api/src/users/users.service.ts +36 -0
- package/templates/api/test/factories/profile.factory.ts +26 -0
- package/templates/api/test/factories/user.factory.ts +21 -0
- package/templates/api/test/global-setup.ts +20 -0
- package/templates/api/test/setup.ts +80 -0
- package/templates/api/tsconfig.build.json +8 -0
- package/templates/api/tsconfig.json +29 -0
- package/templates/api/vitest.config.mts +17 -0
- package/templates/mobile/.env.example +1 -0
- package/templates/mobile/.githooks/install.mjs +18 -0
- package/templates/mobile/.githooks/pre-commit +2 -0
- package/templates/mobile/.githooks/pre-push +2 -0
- package/templates/mobile/README.md +30 -0
- package/templates/mobile/app.config.ts +13 -0
- package/templates/mobile/app.json +39 -0
- package/templates/mobile/assets/expo.icon/Assets/expo-symbol 2.svg +3 -0
- package/templates/mobile/assets/expo.icon/Assets/grid.png +0 -0
- package/templates/mobile/assets/expo.icon/icon.json +35 -0
- package/templates/mobile/assets/images/android-icon-background.png +0 -0
- package/templates/mobile/assets/images/android-icon-foreground.png +0 -0
- package/templates/mobile/assets/images/android-icon-monochrome.png +0 -0
- package/templates/mobile/assets/images/icon.png +0 -0
- package/templates/mobile/assets/images/splash-icon.png +0 -0
- package/templates/mobile/babel.config.js +6 -0
- package/templates/mobile/biome.jsonc +21 -0
- package/templates/mobile/gitignore +11 -0
- package/templates/mobile/global.d.ts +1 -0
- package/templates/mobile/jest.config.js +21 -0
- package/templates/mobile/metro.config.js +4 -0
- package/templates/mobile/nativewind-env.d.ts +1 -0
- package/templates/mobile/package.json +63 -0
- package/templates/mobile/src/api-contract/api-error.ts +51 -0
- package/templates/mobile/src/api-contract/client.ts +26 -0
- package/templates/mobile/src/api-contract/index.ts +10 -0
- package/templates/mobile/src/api-contract/profile.ts +27 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +18 -0
- package/templates/mobile/src/app/(app)/index.tsx +18 -0
- package/templates/mobile/src/app/(auth)/sign-in.tsx +24 -0
- package/templates/mobile/src/app/(auth)/sign-up.tsx +23 -0
- package/templates/mobile/src/app/_layout.tsx +54 -0
- package/templates/mobile/src/components/ui/button.tsx +36 -0
- package/templates/mobile/src/components/ui/field.tsx +30 -0
- package/templates/mobile/src/config.ts +3 -0
- package/templates/mobile/src/features/auth/auth-client.ts +27 -0
- package/templates/mobile/src/features/auth/intended-route.ts +11 -0
- package/templates/mobile/src/features/auth/session.ts +17 -0
- package/templates/mobile/src/features/auth/sign-in-form.spec.tsx +37 -0
- package/templates/mobile/src/features/auth/sign-in-form.tsx +80 -0
- package/templates/mobile/src/features/auth/sign-up-form.tsx +99 -0
- package/templates/mobile/src/features/clients/clients-context.tsx +21 -0
- package/templates/mobile/src/features/forms/form-error.tsx +16 -0
- package/templates/mobile/src/features/forms/server-errors.ts +33 -0
- package/templates/mobile/src/features/profile/profile-form.spec.tsx +58 -0
- package/templates/mobile/src/features/profile/profile-form.tsx +96 -0
- package/templates/mobile/src/features/storage/query-persister.ts +15 -0
- package/templates/mobile/src/features/storage/storage.ts +11 -0
- package/templates/mobile/src/global.css +3 -0
- package/templates/mobile/tailwind.config.js +9 -0
- package/templates/mobile/test/fake-transport.ts +23 -0
- package/templates/mobile/tsconfig.json +21 -0
- package/templates/site/.githooks/install.mjs +18 -0
- package/templates/site/.githooks/pre-commit +2 -0
- package/templates/site/.githooks/pre-push +2 -0
- package/templates/site/README.md +28 -0
- package/templates/site/biome.jsonc +12 -0
- package/templates/site/components.json +25 -0
- package/templates/site/gitignore +7 -0
- package/templates/site/next.config.ts +7 -0
- package/templates/site/package.json +40 -0
- package/templates/site/pnpm-workspace.yaml +5 -0
- package/templates/site/postcss.config.mjs +7 -0
- package/templates/site/src/app/globals.css +129 -0
- package/templates/site/src/app/layout.tsx +13 -0
- package/templates/site/src/app/page.spec.ts +10 -0
- package/templates/site/src/app/page.tsx +17 -0
- package/templates/site/src/lib/utils.ts +1 -0
- package/templates/site/tsconfig.json +34 -0
- package/templates/site/vitest.config.mts +8 -0
- package/templates/web/.env.example +1 -0
- package/templates/web/.githooks/install.mjs +18 -0
- package/templates/web/.githooks/pre-commit +2 -0
- package/templates/web/.githooks/pre-push +2 -0
- package/templates/web/README.md +29 -0
- package/templates/web/biome.jsonc +24 -0
- package/templates/web/components.json +25 -0
- package/templates/web/gitignore +6 -0
- package/templates/web/index.html +12 -0
- package/templates/web/package.json +55 -0
- package/templates/web/src/api-contract/api-error.ts +51 -0
- package/templates/web/src/api-contract/client.ts +26 -0
- package/templates/web/src/api-contract/index.ts +10 -0
- package/templates/web/src/api-contract/profile.ts +27 -0
- package/templates/web/src/app.spec.tsx +12 -0
- package/templates/web/src/app.tsx +14 -0
- package/templates/web/src/components/ui/button.tsx +57 -0
- package/templates/web/src/components/ui/field.tsx +224 -0
- package/templates/web/src/components/ui/input.tsx +19 -0
- package/templates/web/src/components/ui/label.tsx +17 -0
- package/templates/web/src/components/ui/separator.tsx +20 -0
- package/templates/web/src/config.ts +7 -0
- package/templates/web/src/features/auth/auth-client.ts +12 -0
- package/templates/web/src/features/auth/session.ts +17 -0
- package/templates/web/src/features/auth/sign-in-form.spec.tsx +30 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +73 -0
- package/templates/web/src/features/auth/sign-up-form.tsx +79 -0
- package/templates/web/src/features/forms/form-error.tsx +15 -0
- package/templates/web/src/features/forms/server-errors.ts +33 -0
- package/templates/web/src/features/profile/profile-form.spec.tsx +58 -0
- package/templates/web/src/features/profile/profile-form.tsx +74 -0
- package/templates/web/src/index.css +130 -0
- package/templates/web/src/lib/utils.ts +1 -0
- package/templates/web/src/main.tsx +28 -0
- package/templates/web/src/routeTree.gen.ts +125 -0
- package/templates/web/src/router.tsx +21 -0
- package/templates/web/src/routes/__root.tsx +10 -0
- package/templates/web/src/routes/_authenticated/index.tsx +21 -0
- package/templates/web/src/routes/_authenticated.tsx +15 -0
- package/templates/web/src/routes/sign-in.tsx +20 -0
- package/templates/web/src/routes/sign-up.tsx +17 -0
- package/templates/web/test/fake-transport.ts +23 -0
- package/templates/web/test/render-app.tsx +17 -0
- package/templates/web/test/setup.ts +7 -0
- package/templates/web/tsconfig.json +21 -0
- package/templates/web/vite.config.ts +18 -0
- package/templates/workspace/.githooks/install.mjs +18 -0
- package/templates/workspace/.githooks/pre-commit +2 -0
- package/templates/workspace/.githooks/pre-push +2 -0
- package/templates/workspace/README.md +25 -0
- package/templates/workspace/biome.jsonc +8 -0
- package/templates/workspace/gitignore +5 -0
- package/templates/workspace/package.json +17 -0
- package/templates/workspace/packages/api-contract/package.json +20 -0
- package/templates/workspace/packages/api-contract/tsconfig.json +11 -0
- package/templates/workspace/pnpm-workspace.yaml +3 -0
- package/templates/workspace/tsconfig.base.json +10 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useForm } from 'react-hook-form'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { type ApiClient, LOCALES, type Profile, profileQuery, updateProfile } from '@/api-contract'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { FormError } from '@/features/forms/form-error'
|
|
10
|
+
import { applyServerError } from '@/features/forms/server-errors'
|
|
11
|
+
|
|
12
|
+
const schema = z.object({
|
|
13
|
+
displayName: z.string().min(1).max(80),
|
|
14
|
+
locale: z.enum(LOCALES),
|
|
15
|
+
timezone: z.string().min(1).max(64),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type ProfileValues = z.infer<typeof schema>
|
|
19
|
+
|
|
20
|
+
const FIELDS = ['displayName', 'locale', 'timezone'] as const
|
|
21
|
+
|
|
22
|
+
export function ProfileForm({ api, profile }: { api: ApiClient; profile: Profile }) {
|
|
23
|
+
const queryClient = useQueryClient()
|
|
24
|
+
const form = useForm<ProfileValues>({
|
|
25
|
+
resolver: zodResolver(schema),
|
|
26
|
+
mode: 'onTouched',
|
|
27
|
+
reValidateMode: 'onChange',
|
|
28
|
+
defaultValues: {
|
|
29
|
+
displayName: profile.displayName,
|
|
30
|
+
locale: profile.locale,
|
|
31
|
+
timezone: profile.timezone,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
const { errors, isSubmitting } = form.formState
|
|
35
|
+
|
|
36
|
+
const mutation = useMutation({
|
|
37
|
+
mutationFn: (values: ProfileValues) => updateProfile(api, values),
|
|
38
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: profileQuery(api).queryKey }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
async function onSubmit(values: ProfileValues) {
|
|
42
|
+
try {
|
|
43
|
+
await mutation.mutateAsync(values)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
applyServerError(error, FIELDS, form.setError)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form onSubmit={form.handleSubmit(onSubmit)} noValidate className="flex flex-col gap-6">
|
|
51
|
+
<FormError errors={errors} />
|
|
52
|
+
<FieldGroup>
|
|
53
|
+
<Field data-invalid={errors.displayName !== undefined}>
|
|
54
|
+
<FieldLabel htmlFor="displayName">Display name</FieldLabel>
|
|
55
|
+
<Input id="displayName" {...form.register('displayName')} />
|
|
56
|
+
<FieldError errors={[errors.displayName]} />
|
|
57
|
+
</Field>
|
|
58
|
+
<Field data-invalid={errors.locale !== undefined}>
|
|
59
|
+
<FieldLabel htmlFor="locale">Locale</FieldLabel>
|
|
60
|
+
<Input id="locale" {...form.register('locale')} />
|
|
61
|
+
<FieldError errors={[errors.locale]} />
|
|
62
|
+
</Field>
|
|
63
|
+
<Field data-invalid={errors.timezone !== undefined}>
|
|
64
|
+
<FieldLabel htmlFor="timezone">Timezone</FieldLabel>
|
|
65
|
+
<Input id="timezone" {...form.register('timezone')} />
|
|
66
|
+
<FieldError errors={[errors.timezone]} />
|
|
67
|
+
</Field>
|
|
68
|
+
</FieldGroup>
|
|
69
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
70
|
+
Save
|
|
71
|
+
</Button>
|
|
72
|
+
</form>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
@import "@fontsource-variable/geist";
|
|
5
|
+
|
|
6
|
+
@custom-variant dark (&:is(.dark *));
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--font-heading: var(--font-sans);
|
|
10
|
+
--font-sans: "Geist Variable", sans-serif;
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--color-foreground: var(--foreground);
|
|
41
|
+
--color-background: var(--background);
|
|
42
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
43
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
44
|
+
--radius-lg: var(--radius);
|
|
45
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
46
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
47
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
48
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:root {
|
|
52
|
+
--background: oklch(1 0 0);
|
|
53
|
+
--foreground: oklch(0.145 0 0);
|
|
54
|
+
--card: oklch(1 0 0);
|
|
55
|
+
--card-foreground: oklch(0.145 0 0);
|
|
56
|
+
--popover: oklch(1 0 0);
|
|
57
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
58
|
+
--primary: oklch(0.205 0 0);
|
|
59
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--secondary: oklch(0.97 0 0);
|
|
61
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
62
|
+
--muted: oklch(0.97 0 0);
|
|
63
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
64
|
+
--accent: oklch(0.97 0 0);
|
|
65
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
66
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
67
|
+
--border: oklch(0.922 0 0);
|
|
68
|
+
--input: oklch(0.922 0 0);
|
|
69
|
+
--ring: oklch(0.708 0 0);
|
|
70
|
+
--chart-1: oklch(0.87 0 0);
|
|
71
|
+
--chart-2: oklch(0.556 0 0);
|
|
72
|
+
--chart-3: oklch(0.439 0 0);
|
|
73
|
+
--chart-4: oklch(0.371 0 0);
|
|
74
|
+
--chart-5: oklch(0.269 0 0);
|
|
75
|
+
--radius: 0.625rem;
|
|
76
|
+
--sidebar: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
78
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
82
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
83
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dark {
|
|
87
|
+
--background: oklch(0.145 0 0);
|
|
88
|
+
--foreground: oklch(0.985 0 0);
|
|
89
|
+
--card: oklch(0.205 0 0);
|
|
90
|
+
--card-foreground: oklch(0.985 0 0);
|
|
91
|
+
--popover: oklch(0.205 0 0);
|
|
92
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
93
|
+
--primary: oklch(0.922 0 0);
|
|
94
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
95
|
+
--secondary: oklch(0.269 0 0);
|
|
96
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--muted: oklch(0.269 0 0);
|
|
98
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
99
|
+
--accent: oklch(0.269 0 0);
|
|
100
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
101
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
102
|
+
--border: oklch(1 0 0 / 10%);
|
|
103
|
+
--input: oklch(1 0 0 / 15%);
|
|
104
|
+
--ring: oklch(0.556 0 0);
|
|
105
|
+
--chart-1: oklch(0.87 0 0);
|
|
106
|
+
--chart-2: oklch(0.556 0 0);
|
|
107
|
+
--chart-3: oklch(0.439 0 0);
|
|
108
|
+
--chart-4: oklch(0.371 0 0);
|
|
109
|
+
--chart-5: oklch(0.269 0 0);
|
|
110
|
+
--sidebar: oklch(0.205 0 0);
|
|
111
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
113
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
115
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
117
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@layer base {
|
|
121
|
+
* {
|
|
122
|
+
@apply border-border outline-ring/50;
|
|
123
|
+
}
|
|
124
|
+
body {
|
|
125
|
+
@apply bg-background text-foreground;
|
|
126
|
+
}
|
|
127
|
+
html {
|
|
128
|
+
@apply font-sans;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from 'cn'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import '@/index.css'
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { StrictMode } from 'react'
|
|
4
|
+
import { createRoot } from 'react-dom/client'
|
|
5
|
+
import { createClient, type Transport } from '@/api-contract'
|
|
6
|
+
import { createAuth } from '@/features/auth/auth-client'
|
|
7
|
+
import { App } from './app'
|
|
8
|
+
import { config } from './config'
|
|
9
|
+
|
|
10
|
+
const transport: Transport = (input, init) => fetch(input, { ...init, credentials: 'include' })
|
|
11
|
+
|
|
12
|
+
const context = {
|
|
13
|
+
queryClient: new QueryClient(),
|
|
14
|
+
api: createClient({ baseUrl: config.VITE_API_URL, fetch: transport }),
|
|
15
|
+
auth: createAuth({ baseUrl: config.VITE_API_URL, fetch: transport }),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const root = document.getElementById('root')
|
|
19
|
+
|
|
20
|
+
if (root === null) {
|
|
21
|
+
throw new Error('Missing #root element')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
createRoot(root).render(
|
|
25
|
+
<StrictMode>
|
|
26
|
+
<App context={context} />
|
|
27
|
+
</StrictMode>,
|
|
28
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
|
13
|
+
import { Route as SignInRouteImport } from './routes/sign-in'
|
|
14
|
+
import { Route as SignUpRouteImport } from './routes/sign-up'
|
|
15
|
+
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
|
16
|
+
|
|
17
|
+
const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
|
18
|
+
id: '/_authenticated',
|
|
19
|
+
getParentRoute: () => rootRouteImport,
|
|
20
|
+
} as any)
|
|
21
|
+
const SignInRoute = SignInRouteImport.update({
|
|
22
|
+
id: '/sign-in',
|
|
23
|
+
path: '/sign-in',
|
|
24
|
+
getParentRoute: () => rootRouteImport,
|
|
25
|
+
} as any)
|
|
26
|
+
const SignUpRoute = SignUpRouteImport.update({
|
|
27
|
+
id: '/sign-up',
|
|
28
|
+
path: '/sign-up',
|
|
29
|
+
getParentRoute: () => rootRouteImport,
|
|
30
|
+
} as any)
|
|
31
|
+
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
|
32
|
+
id: '/',
|
|
33
|
+
path: '/',
|
|
34
|
+
getParentRoute: () => AuthenticatedRoute,
|
|
35
|
+
} as any)
|
|
36
|
+
|
|
37
|
+
export interface FileRoutesByFullPath {
|
|
38
|
+
'/': typeof AuthenticatedIndexRoute
|
|
39
|
+
'/sign-in': typeof SignInRoute
|
|
40
|
+
'/sign-up': typeof SignUpRoute
|
|
41
|
+
}
|
|
42
|
+
export interface FileRoutesByTo {
|
|
43
|
+
'/sign-in': typeof SignInRoute
|
|
44
|
+
'/sign-up': typeof SignUpRoute
|
|
45
|
+
'/': typeof AuthenticatedIndexRoute
|
|
46
|
+
}
|
|
47
|
+
export interface FileRoutesById {
|
|
48
|
+
__root__: typeof rootRouteImport
|
|
49
|
+
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
|
50
|
+
'/sign-in': typeof SignInRoute
|
|
51
|
+
'/sign-up': typeof SignUpRoute
|
|
52
|
+
'/_authenticated/': typeof AuthenticatedIndexRoute
|
|
53
|
+
}
|
|
54
|
+
export interface FileRouteTypes {
|
|
55
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
56
|
+
fullPaths: '/' | '/sign-in' | '/sign-up'
|
|
57
|
+
fileRoutesByTo: FileRoutesByTo
|
|
58
|
+
to: '/sign-in' | '/sign-up' | '/'
|
|
59
|
+
id:
|
|
60
|
+
| '__root__'
|
|
61
|
+
| '/_authenticated'
|
|
62
|
+
| '/sign-in'
|
|
63
|
+
| '/sign-up'
|
|
64
|
+
| '/_authenticated/'
|
|
65
|
+
fileRoutesById: FileRoutesById
|
|
66
|
+
}
|
|
67
|
+
export interface RootRouteChildren {
|
|
68
|
+
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
|
69
|
+
SignInRoute: typeof SignInRoute
|
|
70
|
+
SignUpRoute: typeof SignUpRoute
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare module '@tanstack/react-router' {
|
|
74
|
+
interface FileRoutesByPath {
|
|
75
|
+
'/_authenticated': {
|
|
76
|
+
id: '/_authenticated'
|
|
77
|
+
path: ''
|
|
78
|
+
fullPath: '/'
|
|
79
|
+
preLoaderRoute: typeof AuthenticatedRouteImport
|
|
80
|
+
parentRoute: typeof rootRouteImport
|
|
81
|
+
}
|
|
82
|
+
'/sign-in': {
|
|
83
|
+
id: '/sign-in'
|
|
84
|
+
path: '/sign-in'
|
|
85
|
+
fullPath: '/sign-in'
|
|
86
|
+
preLoaderRoute: typeof SignInRouteImport
|
|
87
|
+
parentRoute: typeof rootRouteImport
|
|
88
|
+
}
|
|
89
|
+
'/sign-up': {
|
|
90
|
+
id: '/sign-up'
|
|
91
|
+
path: '/sign-up'
|
|
92
|
+
fullPath: '/sign-up'
|
|
93
|
+
preLoaderRoute: typeof SignUpRouteImport
|
|
94
|
+
parentRoute: typeof rootRouteImport
|
|
95
|
+
}
|
|
96
|
+
'/_authenticated/': {
|
|
97
|
+
id: '/_authenticated/'
|
|
98
|
+
path: '/'
|
|
99
|
+
fullPath: '/'
|
|
100
|
+
preLoaderRoute: typeof AuthenticatedIndexRouteImport
|
|
101
|
+
parentRoute: typeof AuthenticatedRoute
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface AuthenticatedRouteChildren {
|
|
107
|
+
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
|
111
|
+
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
|
115
|
+
AuthenticatedRouteChildren,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
119
|
+
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
|
120
|
+
SignInRoute: SignInRoute,
|
|
121
|
+
SignUpRoute: SignUpRoute,
|
|
122
|
+
}
|
|
123
|
+
export const routeTree = rootRouteImport
|
|
124
|
+
._addFileChildren(rootRouteChildren)
|
|
125
|
+
._addFileTypes<FileRouteTypes>()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { createRouter, type RouterHistory } from '@tanstack/react-router'
|
|
3
|
+
import type { ApiClient } from '@/api-contract'
|
|
4
|
+
import type { AuthClient } from '@/features/auth/auth-client'
|
|
5
|
+
import { routeTree } from './routeTree.gen'
|
|
6
|
+
|
|
7
|
+
export type RouterContext = {
|
|
8
|
+
queryClient: QueryClient
|
|
9
|
+
api: ApiClient
|
|
10
|
+
auth: AuthClient
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createAppRouter(context: RouterContext, history?: RouterHistory) {
|
|
14
|
+
return createRouter({ routeTree, context, history, defaultPreload: 'intent' })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module '@tanstack/react-router' {
|
|
18
|
+
interface Register {
|
|
19
|
+
router: ReturnType<typeof createAppRouter>
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
|
|
2
|
+
import type { RouterContext } from '@/router'
|
|
3
|
+
|
|
4
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
5
|
+
component: () => (
|
|
6
|
+
<main className="mx-auto flex min-h-svh w-full max-w-sm flex-col justify-center gap-6 p-6">
|
|
7
|
+
<Outlet />
|
|
8
|
+
</main>
|
|
9
|
+
),
|
|
10
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { profileQuery } from '@/api-contract'
|
|
4
|
+
import { ProfileForm } from '@/features/profile/profile-form'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_authenticated/')({
|
|
7
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(profileQuery(context.api)),
|
|
8
|
+
component: ProfilePage,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
function ProfilePage() {
|
|
12
|
+
const { api } = Route.useRouteContext()
|
|
13
|
+
const { data: profile } = useSuspenseQuery(profileQuery(api))
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<h1 className="text-2xl font-semibold">Your profile</h1>
|
|
18
|
+
<ProfileForm api={api} profile={profile} />
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { sessionQuery } from '@/features/auth/session'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/_authenticated')({
|
|
5
|
+
beforeLoad: async ({ context, location }) => {
|
|
6
|
+
const session = await context.queryClient.ensureQueryData(sessionQuery(context.auth))
|
|
7
|
+
|
|
8
|
+
if (session === null) {
|
|
9
|
+
throw redirect({ to: '/sign-in', search: { redirect: location.href } })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return { session }
|
|
13
|
+
},
|
|
14
|
+
component: Outlet,
|
|
15
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { SignInForm } from '@/features/auth/sign-in-form'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/sign-in')({
|
|
6
|
+
validateSearch: z.object({ redirect: z.string().optional() }),
|
|
7
|
+
component: SignInPage,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function SignInPage() {
|
|
11
|
+
const { auth } = Route.useRouteContext()
|
|
12
|
+
const { redirect } = Route.useSearch()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<h1 className="text-2xl font-semibold">Sign in</h1>
|
|
17
|
+
<SignInForm auth={auth} redirect={redirect} />
|
|
18
|
+
</>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { SignUpForm } from '@/features/auth/sign-up-form'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/sign-up')({
|
|
5
|
+
component: SignUpPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function SignUpPage() {
|
|
9
|
+
const { auth } = Route.useRouteContext()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<h1 className="text-2xl font-semibold">Create an account</h1>
|
|
14
|
+
<SignUpForm auth={auth} />
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Transport } from '@/api-contract'
|
|
2
|
+
|
|
3
|
+
export const API_URL = 'http://api.test'
|
|
4
|
+
|
|
5
|
+
type Handler = (request: Request) => Response | Promise<Response>
|
|
6
|
+
|
|
7
|
+
export function json(status: number, body: unknown, contentType = 'application/json'): Response {
|
|
8
|
+
return new Response(JSON.stringify(body), { status, headers: { 'content-type': contentType } })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fakeTransport(routes: Record<string, Handler>): Transport {
|
|
12
|
+
return async (input, init) => {
|
|
13
|
+
const request = new Request(input, init)
|
|
14
|
+
const key = `${request.method} ${new URL(request.url).pathname}`
|
|
15
|
+
const handler = routes[key]
|
|
16
|
+
|
|
17
|
+
if (handler === undefined) {
|
|
18
|
+
throw new Error(`No fake response for ${key}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return handler(request)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { createMemoryHistory } from '@tanstack/react-router'
|
|
3
|
+
import { render } from '@testing-library/react'
|
|
4
|
+
import { createClient, type Transport } from '@/api-contract'
|
|
5
|
+
import { App } from '@/app'
|
|
6
|
+
import { createAuth } from '@/features/auth/auth-client'
|
|
7
|
+
import { API_URL } from './fake-transport'
|
|
8
|
+
|
|
9
|
+
export function renderApp(path: string, transport: Transport) {
|
|
10
|
+
const context = {
|
|
11
|
+
queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }),
|
|
12
|
+
api: createClient({ baseUrl: API_URL, fetch: transport }),
|
|
13
|
+
auth: createAuth({ baseUrl: API_URL, fetch: transport }),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return render(<App context={context} history={createMemoryHistory({ initialEntries: [path] })} />)
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
},
|
|
18
|
+
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src", "test", "vite.config.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
2
|
+
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import { defineConfig } from 'vitest/config'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [tanstackRouter({ target: 'react', autoCodeSplitting: true }), react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: { '@': `${import.meta.dirname}/src` },
|
|
10
|
+
},
|
|
11
|
+
test: {
|
|
12
|
+
environment: 'jsdom',
|
|
13
|
+
globals: true,
|
|
14
|
+
include: ['src/**/*.spec.{ts,tsx}'],
|
|
15
|
+
setupFiles: ['./test/setup.ts'],
|
|
16
|
+
coverage: { provider: 'v8', include: ['src/**'] },
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
function insideRepository() {
|
|
4
|
+
try {
|
|
5
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { stdio: 'ignore' })
|
|
6
|
+
return true
|
|
7
|
+
} catch (error) {
|
|
8
|
+
// No git, or no repository: an install from an archive or inside a container, where hooks have no use.
|
|
9
|
+
if (error.code === 'ENOENT' || error.status === 128) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
throw error
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (insideRepository()) {
|
|
17
|
+
execFileSync('git', ['config', 'core.hooksPath', '.githooks'], { stdio: 'inherit' })
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# workspace
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
Node 22.17 or later, pnpm, and Docker for the API.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
| Directory | What it is |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `apps/` | Deployable applications, one per type. Each has its own README with the commands to run it |
|
|
12
|
+
| `packages/api-contract` | The wire contract between the clients and the API, shipped as TypeScript source |
|
|
13
|
+
|
|
14
|
+
## Everyday commands
|
|
15
|
+
|
|
16
|
+
| Command | What it does |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `pnpm install` | Installs every app and package |
|
|
19
|
+
| `pnpm --filter <app> dev` | Starts one app. There is no root `dev` on purpose |
|
|
20
|
+
| `pnpm lint` · `pnpm typecheck` · `pnpm test` | Runs across every app and package |
|
|
21
|
+
|
|
22
|
+
## Conventions
|
|
23
|
+
|
|
24
|
+
The rules this project follows live in `.prumo/`, and `AGENTS.md` points to them. Read those before
|
|
25
|
+
changing how something is done.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.13/schema.json",
|
|
3
|
+
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
|
4
|
+
"files": { "includes": ["**", "!**/dist/**", "!**/coverage/**"] },
|
|
5
|
+
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
|
|
6
|
+
"linter": { "rules": { "preset": "recommended" } },
|
|
7
|
+
"javascript": { "formatter": { "quoteStyle": "single", "semicolons": "asNeeded" } }
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "workspace",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint": "pnpm -r lint",
|
|
7
|
+
"typecheck": "pnpm -r typecheck",
|
|
8
|
+
"test": "pnpm -r test",
|
|
9
|
+
"prepare": "node .githooks/install.mjs"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@biomejs/biome": "catalog:"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.17.0"
|
|
16
|
+
}
|
|
17
|
+
}
|