create-mikstack 0.1.0
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/README.md +54 -0
- package/dist/index.js +410 -0
- package/package.json +43 -0
- package/templates/adapters/cloudflare/package.json.partial +5 -0
- package/templates/adapters/cloudflare/svelte.config.js +19 -0
- package/templates/adapters/node/Dockerfile +30 -0
- package/templates/adapters/node/docker-compose.prod.yml +27 -0
- package/templates/adapters/node/package.json.partial +5 -0
- package/templates/adapters/node/svelte.config.js +19 -0
- package/templates/adapters/vercel/package.json.partial +5 -0
- package/templates/adapters/vercel/svelte.config.js +19 -0
- package/templates/base/.env.example +23 -0
- package/templates/base/.gitignore.append +2 -0
- package/templates/base/.mcp.json +9 -0
- package/templates/base/.prettierignore +10 -0
- package/templates/base/.vscode/extensions.json +3 -0
- package/templates/base/AGENTS.md +123 -0
- package/templates/base/README.md +27 -0
- package/templates/base/agents.md +28 -0
- package/templates/base/docker-compose.yml +15 -0
- package/templates/base/drizzle-zero.config.ts +17 -0
- package/templates/base/drizzle.config.ts +17 -0
- package/templates/base/eslint.config.ts +65 -0
- package/templates/base/package.json.partial +43 -0
- package/templates/base/prettier.config.js +6 -0
- package/templates/base/src/app.d.ts +12 -0
- package/templates/base/src/app.html +11 -0
- package/templates/base/src/hooks.server.ts +15 -0
- package/templates/base/src/lib/auth-client.ts +6 -0
- package/templates/base/src/lib/server/auth.ts +52 -0
- package/templates/base/src/lib/server/db/index.ts +19 -0
- package/templates/base/src/lib/server/db/schema.ts +117 -0
- package/templates/base/src/lib/server/db/seed.ts +21 -0
- package/templates/base/src/lib/server/emails/magic-link.ts +77 -0
- package/templates/base/src/lib/server/emails/send.ts +55 -0
- package/templates/base/src/lib/server/notifications/definitions.ts +12 -0
- package/templates/base/src/lib/server/notifications.ts +38 -0
- package/templates/base/src/lib/z.svelte.ts +14 -0
- package/templates/base/src/lib/zero/context.ts +9 -0
- package/templates/base/src/lib/zero/db-provider.server.ts +11 -0
- package/templates/base/src/lib/zero/mutators.ts +35 -0
- package/templates/base/src/lib/zero/queries.ts +21 -0
- package/templates/base/src/lib/zero/schema.ts +1 -0
- package/templates/base/src/routes/+layout.server.ts +5 -0
- package/templates/base/src/routes/+layout.svelte +7 -0
- package/templates/base/src/routes/+page.server.ts +7 -0
- package/templates/base/src/routes/+page.svelte +319 -0
- package/templates/base/src/routes/api/dev/emails/+server.ts +89 -0
- package/templates/base/src/routes/api/dev/emails/[id]/+server.ts +24 -0
- package/templates/base/src/routes/api/notifications/[...path]/+server.ts +10 -0
- package/templates/base/src/routes/api/zero/get-queries/+server.ts +29 -0
- package/templates/base/src/routes/api/zero/mutate/+server.ts +31 -0
- package/templates/base/src/routes/sign-in/+page.svelte +97 -0
- package/templates/base/tsconfig.json +40 -0
- package/templates/github-actions-bun/.github/workflows/ci.yml +22 -0
- package/templates/github-actions-npm/.github/workflows/ci.yml +25 -0
- package/templates/github-actions-pnpm/.github/workflows/ci.yml +27 -0
- package/templates/i18n/lingui.config.ts +16 -0
- package/templates/i18n/package.json.partial +14 -0
- package/templates/i18n/src/lib/i18n.ts +10 -0
- package/templates/i18n/src/locales/en.po +6 -0
- package/templates/i18n/src/po.d.ts +3 -0
- package/templates/i18n/vite.config.ts +7 -0
- package/templates/supply-chain-bun/bunfig.toml +3 -0
- package/templates/testing/package.json.partial +11 -0
- package/templates/testing/src/example.test.ts +7 -0
- package/templates/testing/src/lib/server/db/test-utils.ts +25 -0
- package/templates/testing/vitest.config.ts +9 -0
- package/templates/ui/.vscode/extensions.json +8 -0
- package/templates/ui/package.json.partial +13 -0
- package/templates/ui/src/app.css +94 -0
- package/templates/ui/src/routes/+layout.svelte +12 -0
- package/templates/ui/stylelint.config.js +7 -0
- package/templates/ui/vite.config.ts +6 -0
- package/templates/ui-dependency/package.json.partial +5 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/Accordion.svelte +71 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/Alert.svelte +60 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/Badge.svelte +48 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/Button.svelte +77 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/Card.svelte +49 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/Dialog.svelte +70 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/FormField.svelte +53 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/Input.svelte +27 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/Separator.svelte +26 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/Skeleton.svelte +40 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/Switch.svelte +86 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/Textarea.svelte +29 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/index.ts +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { dev } from "$app/environment";
|
|
2
|
+
import { error } from "@sveltejs/kit";
|
|
3
|
+
import { desc, eq } from "drizzle-orm";
|
|
4
|
+
import { db } from "$lib/server/db";
|
|
5
|
+
import { notificationDelivery } from "$lib/server/db/schema";
|
|
6
|
+
import type { RequestHandler } from "./$types";
|
|
7
|
+
|
|
8
|
+
export const GET: RequestHandler = async () => {
|
|
9
|
+
if (!dev) error(404);
|
|
10
|
+
|
|
11
|
+
const deliveries = await db
|
|
12
|
+
.select({
|
|
13
|
+
id: notificationDelivery.id,
|
|
14
|
+
type: notificationDelivery.type,
|
|
15
|
+
channel: notificationDelivery.channel,
|
|
16
|
+
recipientEmail: notificationDelivery.recipientEmail,
|
|
17
|
+
status: notificationDelivery.status,
|
|
18
|
+
error: notificationDelivery.error,
|
|
19
|
+
createdAt: notificationDelivery.createdAt,
|
|
20
|
+
})
|
|
21
|
+
.from(notificationDelivery)
|
|
22
|
+
.where(eq(notificationDelivery.channel, "email"))
|
|
23
|
+
.orderBy(desc(notificationDelivery.createdAt))
|
|
24
|
+
.limit(50);
|
|
25
|
+
|
|
26
|
+
const rows = deliveries
|
|
27
|
+
.map(
|
|
28
|
+
(d) => `
|
|
29
|
+
<tr>
|
|
30
|
+
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb">
|
|
31
|
+
<a href="/api/dev/emails/${d.id}" target="_blank" style="color:#2563eb">${esc(d.type)}</a>
|
|
32
|
+
</td>
|
|
33
|
+
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb">${esc(d.recipientEmail ?? "—")}</td>
|
|
34
|
+
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb">
|
|
35
|
+
<span style="padding:2px 8px;border-radius:9999px;font-size:12px;background:${statusColor(d.status)}">${esc(d.status)}</span>
|
|
36
|
+
${d.error ? `<span style="color:#ef4444;font-size:12px"> ${esc(d.error)}</span>` : ""}
|
|
37
|
+
</td>
|
|
38
|
+
<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px">${d.createdAt.toLocaleString()}</td>
|
|
39
|
+
</tr>`,
|
|
40
|
+
)
|
|
41
|
+
.join("");
|
|
42
|
+
|
|
43
|
+
const html = `<!DOCTYPE html>
|
|
44
|
+
<html>
|
|
45
|
+
<head><meta charset="utf-8"><title>Dev Emails</title></head>
|
|
46
|
+
<body style="font-family:system-ui,sans-serif;margin:0;padding:24px;background:#f9fafb">
|
|
47
|
+
<h1 style="font-size:20px;margin:0 0 16px">Dev Email Log</h1>
|
|
48
|
+
${
|
|
49
|
+
deliveries.length === 0
|
|
50
|
+
? '<p style="color:#6b7280">No emails sent yet.</p>'
|
|
51
|
+
: `<table style="width:100%;border-collapse:collapse;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1)">
|
|
52
|
+
<thead>
|
|
53
|
+
<tr style="background:#f3f4f6;text-align:left">
|
|
54
|
+
<th style="padding:8px 12px;font-weight:600">Type</th>
|
|
55
|
+
<th style="padding:8px 12px;font-weight:600">To</th>
|
|
56
|
+
<th style="padding:8px 12px;font-weight:600">Status</th>
|
|
57
|
+
<th style="padding:8px 12px;font-weight:600">Sent</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody>${rows}</tbody>
|
|
61
|
+
</table>`
|
|
62
|
+
}
|
|
63
|
+
</body>
|
|
64
|
+
</html>`;
|
|
65
|
+
|
|
66
|
+
return new Response(html, {
|
|
67
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function esc(s: string): string {
|
|
72
|
+
return s
|
|
73
|
+
.replace(/&/g, "&")
|
|
74
|
+
.replace(/</g, "<")
|
|
75
|
+
.replace(/>/g, ">")
|
|
76
|
+
.replace(/"/g, """);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function statusColor(status: string): string {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case "sent":
|
|
82
|
+
case "delivered":
|
|
83
|
+
return "#dcfce7";
|
|
84
|
+
case "failed":
|
|
85
|
+
return "#fee2e2";
|
|
86
|
+
default:
|
|
87
|
+
return "#fef3c7";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { dev } from "$app/environment";
|
|
2
|
+
import { error } from "@sveltejs/kit";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { db } from "$lib/server/db";
|
|
5
|
+
import { notificationDelivery } from "$lib/server/db/schema";
|
|
6
|
+
import type { RequestHandler } from "./$types";
|
|
7
|
+
|
|
8
|
+
export const GET: RequestHandler = async ({ params }) => {
|
|
9
|
+
if (!dev) error(404);
|
|
10
|
+
|
|
11
|
+
const [delivery] = await db
|
|
12
|
+
.select({ content: notificationDelivery.content })
|
|
13
|
+
.from(notificationDelivery)
|
|
14
|
+
.where(eq(notificationDelivery.id, params.id));
|
|
15
|
+
|
|
16
|
+
if (!delivery?.content) error(404);
|
|
17
|
+
|
|
18
|
+
const content = delivery.content as { html?: string };
|
|
19
|
+
if (!content.html) error(404, "No HTML content for this delivery");
|
|
20
|
+
|
|
21
|
+
return new Response(content.html, {
|
|
22
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { notif } from "$lib/server/notifications";
|
|
2
|
+
import type { RequestHandler } from "./$types";
|
|
3
|
+
|
|
4
|
+
const handle: RequestHandler = async ({ request, locals }) => {
|
|
5
|
+
return notif.handler(request, locals.user?.id ?? null);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const GET = handle;
|
|
9
|
+
export const POST = handle;
|
|
10
|
+
export const PUT = handle;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RequestHandler } from "./$types";
|
|
2
|
+
import { handleQueryRequest } from "@rocicorp/zero/server";
|
|
3
|
+
import { mustGetQuery } from "@rocicorp/zero";
|
|
4
|
+
import { schema } from "$lib/zero/schema";
|
|
5
|
+
import { queries } from "$lib/zero/queries";
|
|
6
|
+
|
|
7
|
+
export const POST: RequestHandler = async ({ request, locals }) => {
|
|
8
|
+
if (!locals.user) {
|
|
9
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const ctx = { userID: locals.user.id };
|
|
14
|
+
|
|
15
|
+
const response = await handleQueryRequest(
|
|
16
|
+
(name, args) => {
|
|
17
|
+
const query = mustGetQuery(queries, name);
|
|
18
|
+
return query.fn({ args, ctx });
|
|
19
|
+
},
|
|
20
|
+
schema,
|
|
21
|
+
request,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return Response.json(response);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Query error:", error);
|
|
27
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RequestHandler } from "./$types";
|
|
2
|
+
import { handleMutateRequest } from "@rocicorp/zero/server";
|
|
3
|
+
import { mustGetMutator } from "@rocicorp/zero";
|
|
4
|
+
import { dbProvider } from "$lib/zero/db-provider.server";
|
|
5
|
+
import { mutators } from "$lib/zero/mutators";
|
|
6
|
+
|
|
7
|
+
export const POST: RequestHandler = async ({ request, locals }) => {
|
|
8
|
+
if (!locals.user) {
|
|
9
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const ctx = { userID: locals.user.id };
|
|
14
|
+
|
|
15
|
+
const result = await handleMutateRequest(
|
|
16
|
+
dbProvider,
|
|
17
|
+
async (transact) => {
|
|
18
|
+
return await transact(async (tx, name, args) => {
|
|
19
|
+
const mutator = mustGetMutator(mutators, name);
|
|
20
|
+
return await mutator.fn({ tx, ctx, args });
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
request,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return Response.json(result);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("Mutate error:", error);
|
|
29
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EnvelopeSimple from "phosphor-svelte/lib/EnvelopeSimple";
|
|
3
|
+
import CheckCircle from "phosphor-svelte/lib/CheckCircle";
|
|
4
|
+
import Button from "{{uiPrefix}}/Button";
|
|
5
|
+
import FormField from "{{uiPrefix}}/FormField";
|
|
6
|
+
import Input from "{{uiPrefix}}/Input";
|
|
7
|
+
import { createForm } from "@mikstack/form";
|
|
8
|
+
import * as v from "valibot";
|
|
9
|
+
import { authClient } from "$lib/auth-client";
|
|
10
|
+
|
|
11
|
+
const form = createForm({
|
|
12
|
+
schema: v.object({
|
|
13
|
+
email: v.pipe(v.string(), v.email("Please enter a valid email address")),
|
|
14
|
+
}),
|
|
15
|
+
initialValues: { email: "" },
|
|
16
|
+
async onSubmit(data) {
|
|
17
|
+
const { error } = await authClient.signIn.magicLink({
|
|
18
|
+
email: data.email,
|
|
19
|
+
callbackURL: "/",
|
|
20
|
+
});
|
|
21
|
+
if (error) throw new Error(error.message ?? "Failed to send magic link");
|
|
22
|
+
return { sent: true };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const emailField = form.fields.email;
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="sign-in">
|
|
30
|
+
<h1>Sign in to {{projectName}}</h1>
|
|
31
|
+
|
|
32
|
+
{#if form.result}
|
|
33
|
+
<div class="success">
|
|
34
|
+
<CheckCircle size={24} weight="duotone" />
|
|
35
|
+
<p>Check your email for a magic link to sign in.</p>
|
|
36
|
+
</div>
|
|
37
|
+
{:else}
|
|
38
|
+
<form id={form.id} onsubmit={form.onsubmit} class="sign-in-form">
|
|
39
|
+
<FormField for={emailField.as("email").id}>
|
|
40
|
+
{#snippet label(attrs)}
|
|
41
|
+
<label {...attrs}>Email</label>
|
|
42
|
+
{/snippet}
|
|
43
|
+
<Input {...emailField.as("email")} placeholder="you@example.com" />
|
|
44
|
+
{#snippet error(attrs)}
|
|
45
|
+
{#each emailField.issues() as issue (issue.message)}
|
|
46
|
+
<p {...attrs}>{issue.message}</p>
|
|
47
|
+
{/each}
|
|
48
|
+
{/snippet}
|
|
49
|
+
</FormField>
|
|
50
|
+
|
|
51
|
+
{#if form.error}
|
|
52
|
+
<p class="form-error">{form.error}</p>
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
<Button type="submit" disabled={form.pending}>
|
|
56
|
+
<EnvelopeSimple size={16} weight="bold" />
|
|
57
|
+
{form.pending ? "Sending magic link..." : "Sign in with magic link"}
|
|
58
|
+
</Button>
|
|
59
|
+
</form>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.sign-in {
|
|
65
|
+
max-width: 24rem;
|
|
66
|
+
margin: 0 auto;
|
|
67
|
+
padding: var(--space-8) var(--space-4);
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: var(--space-5);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h1 {
|
|
74
|
+
font-size: var(--text-2xl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.sign-in-form {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--space-3);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.form-error {
|
|
84
|
+
font-size: var(--text-sm);
|
|
85
|
+
color: var(--danger);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.success {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--space-2);
|
|
92
|
+
padding: var(--space-4);
|
|
93
|
+
border-radius: var(--radius-md);
|
|
94
|
+
background-color: var(--surface-2);
|
|
95
|
+
color: var(--text-1);
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rewriteRelativeImportExtensions": true,
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"noUncheckedIndexedAccess": true
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
".svelte-kit/ambient.d.ts",
|
|
18
|
+
".svelte-kit/non-ambient.d.ts",
|
|
19
|
+
".svelte-kit/types/**/$types.d.ts",
|
|
20
|
+
"vite.config.js",
|
|
21
|
+
"vite.config.ts",
|
|
22
|
+
"src/**/*.js",
|
|
23
|
+
"src/**/*.ts",
|
|
24
|
+
"src/**/*.svelte",
|
|
25
|
+
"tests/**/*.js",
|
|
26
|
+
"tests/**/*.ts",
|
|
27
|
+
"tests/**/*.svelte",
|
|
28
|
+
"*.ts",
|
|
29
|
+
"*.js"
|
|
30
|
+
],
|
|
31
|
+
"exclude": [
|
|
32
|
+
"node_modules/**",
|
|
33
|
+
"src/service-worker.js",
|
|
34
|
+
"src/service-worker/**/*.js",
|
|
35
|
+
"src/service-worker.ts",
|
|
36
|
+
"src/service-worker/**/*.ts",
|
|
37
|
+
"src/service-worker.d.ts",
|
|
38
|
+
"src/service-worker/**/*.d.ts"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- uses: oven-sh/setup-bun@v2
|
|
16
|
+
|
|
17
|
+
- run: bun install --frozen-lockfile
|
|
18
|
+
|
|
19
|
+
- run: bun run lint
|
|
20
|
+
- run: bun run format:check
|
|
21
|
+
- run: bun run check
|
|
22
|
+
- run: bun run build
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
cache: npm
|
|
19
|
+
|
|
20
|
+
- run: npm ci
|
|
21
|
+
|
|
22
|
+
- run: npm run lint
|
|
23
|
+
- run: npm run format:check
|
|
24
|
+
- run: npm run check
|
|
25
|
+
- run: npm run build
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- uses: pnpm/action-setup@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-node@v6
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22
|
|
20
|
+
cache: pnpm
|
|
21
|
+
|
|
22
|
+
- run: pnpm install --frozen-lockfile
|
|
23
|
+
|
|
24
|
+
- run: pnpm run lint
|
|
25
|
+
- run: pnpm run format:check
|
|
26
|
+
- run: pnpm run check
|
|
27
|
+
- run: pnpm run build
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { extractor } from "@mikstack/svelte-lingui/extractor";
|
|
2
|
+
import type { LinguiConfig } from "@lingui/conf";
|
|
3
|
+
|
|
4
|
+
const config: LinguiConfig = {
|
|
5
|
+
locales: ["en"],
|
|
6
|
+
sourceLocale: "en",
|
|
7
|
+
catalogs: [
|
|
8
|
+
{
|
|
9
|
+
path: "src/locales/{locale}",
|
|
10
|
+
include: ["src"],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
extractors: [extractor],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default config;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"i18n:extract": "lingui extract",
|
|
4
|
+
"i18n:extract:watch": "lingui extract --watch"
|
|
5
|
+
},
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@lingui/core": "^5.9.0",
|
|
8
|
+
"@mikstack/svelte-lingui": "^0.1.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@lingui/cli": "^5.9.0",
|
|
12
|
+
"@lingui/conf": "^5.9.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { setupI18n } from "@lingui/core";
|
|
2
|
+
import { setI18n } from "@mikstack/svelte-lingui";
|
|
3
|
+
import { messages } from "../locales/en.po";
|
|
4
|
+
|
|
5
|
+
const i18n = setupI18n();
|
|
6
|
+
|
|
7
|
+
export function initI18n(): void {
|
|
8
|
+
i18n.loadAndActivate({ locale: "en", messages });
|
|
9
|
+
setI18n(i18n);
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
|
|
2
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
|
+
import postgres from "postgres";
|
|
4
|
+
import * as schema from "./schema";
|
|
5
|
+
import type { DrizzleDB } from "./index";
|
|
6
|
+
|
|
7
|
+
export type TestDatabase = {
|
|
8
|
+
container: StartedPostgreSqlContainer;
|
|
9
|
+
client: ReturnType<typeof postgres>;
|
|
10
|
+
db: DrizzleDB;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function createTestDatabase(): Promise<TestDatabase> {
|
|
14
|
+
const container = await new PostgreSqlContainer("postgres:17-alpine").start();
|
|
15
|
+
const connectionUri = container.getConnectionUri();
|
|
16
|
+
const client = postgres(connectionUri);
|
|
17
|
+
const db = drizzle(client, { schema, casing: "snake_case" });
|
|
18
|
+
|
|
19
|
+
return { container, client, db };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function stopTestDatabase(testDb: TestDatabase): Promise<void> {
|
|
23
|
+
await testDb.client.end();
|
|
24
|
+
await testDb.container.stop();
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"lint:css": "stylelint \"src/**/*.{css,svelte}\""
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"phosphor-svelte": "^3.1.0",
|
|
7
|
+
"svelte-sonner": "^1.0.0"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"stylelint": "^17.1.1",
|
|
11
|
+
"stylelint-config-standard": "^40.0.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Reset */
|
|
2
|
+
*,
|
|
3
|
+
*::before,
|
|
4
|
+
*::after {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
margin: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
min-height: 100dvh;
|
|
11
|
+
line-height: 1.5;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
img,
|
|
16
|
+
picture,
|
|
17
|
+
video,
|
|
18
|
+
canvas,
|
|
19
|
+
svg {
|
|
20
|
+
display: block;
|
|
21
|
+
max-width: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
input,
|
|
25
|
+
button,
|
|
26
|
+
textarea,
|
|
27
|
+
select {
|
|
28
|
+
font: inherit;
|
|
29
|
+
color: inherit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Design Tokens */
|
|
33
|
+
:root {
|
|
34
|
+
/* Typography */
|
|
35
|
+
font-family:
|
|
36
|
+
system-ui,
|
|
37
|
+
-apple-system,
|
|
38
|
+
"Segoe UI",
|
|
39
|
+
Roboto,
|
|
40
|
+
"Helvetica Neue",
|
|
41
|
+
Arial,
|
|
42
|
+
sans-serif;
|
|
43
|
+
|
|
44
|
+
--text-xs: 0.75rem;
|
|
45
|
+
--text-sm: 0.875rem;
|
|
46
|
+
--text-base: 1rem;
|
|
47
|
+
--text-lg: 1.125rem;
|
|
48
|
+
--text-xl: 1.25rem;
|
|
49
|
+
--text-2xl: 1.5rem;
|
|
50
|
+
--text-3xl: 1.875rem;
|
|
51
|
+
|
|
52
|
+
/* Spacing */
|
|
53
|
+
--space-1: 0.25rem;
|
|
54
|
+
--space-2: 0.5rem;
|
|
55
|
+
--space-3: 0.75rem;
|
|
56
|
+
--space-4: 1rem;
|
|
57
|
+
--space-5: 1.5rem;
|
|
58
|
+
--space-6: 2rem;
|
|
59
|
+
--space-7: 3rem;
|
|
60
|
+
--space-8: 4rem;
|
|
61
|
+
|
|
62
|
+
/* Border Radii */
|
|
63
|
+
--radius-sm: 0.25rem;
|
|
64
|
+
--radius-md: 0.5rem;
|
|
65
|
+
--radius-lg: 0.75rem;
|
|
66
|
+
|
|
67
|
+
/* Light Mode Colors (oklch) */
|
|
68
|
+
--surface-1: oklch(100% 0 0);
|
|
69
|
+
--surface-2: oklch(97% 0 0);
|
|
70
|
+
--surface-3: oklch(93% 0 0);
|
|
71
|
+
--text-1: oklch(20% 0 0);
|
|
72
|
+
--text-2: oklch(40% 0 0);
|
|
73
|
+
--accent: oklch(55% 0.2 260);
|
|
74
|
+
--danger: oklch(55% 0.2 25);
|
|
75
|
+
--border: oklch(85% 0 0);
|
|
76
|
+
--focus: oklch(55% 0.2 260 / 50%);
|
|
77
|
+
|
|
78
|
+
color: var(--text-1);
|
|
79
|
+
background-color: var(--surface-1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (prefers-color-scheme: dark) {
|
|
83
|
+
:root {
|
|
84
|
+
--surface-1: oklch(15% 0 0);
|
|
85
|
+
--surface-2: oklch(20% 0 0);
|
|
86
|
+
--surface-3: oklch(25% 0 0);
|
|
87
|
+
--text-1: oklch(93% 0 0);
|
|
88
|
+
--text-2: oklch(70% 0 0);
|
|
89
|
+
--accent: oklch(70% 0.18 260);
|
|
90
|
+
--danger: oklch(70% 0.18 25);
|
|
91
|
+
--border: oklch(30% 0 0);
|
|
92
|
+
--focus: oklch(70% 0.18 260 / 50%);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import "../app.css";
|
|
4
|
+
// {{#if:i18n}}
|
|
5
|
+
import { initI18n } from "$lib/i18n";
|
|
6
|
+
initI18n();
|
|
7
|
+
// {{/if:i18n}}
|
|
8
|
+
|
|
9
|
+
let { children }: { children: Snippet } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{@render children()}
|