create-mikstack 0.1.23 → 0.1.24
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/dist/index.js +4 -2
- package/package.json +1 -1
- package/templates/adapters/node/docker-compose.prod.yml +23 -0
- package/templates/base/src/lib/server/db/schema.ts +68 -68
- package/templates/base/src/routes/(app)/+page.svelte +112 -12
- package/templates/base/src/routes/(public)/sign-in/+page.svelte +42 -4
- package/templates/i18n/lingui.config.ts +1 -1
- package/templates/i18n/src/lib/LocaleSwitcher.svelte +50 -0
- package/templates/i18n/src/lib/i18n.ts +22 -3
- package/templates/i18n/src/locales/en.po +92 -0
- package/templates/i18n/src/locales/fi.po +98 -0
package/dist/index.js
CHANGED
|
@@ -145,11 +145,13 @@ async function runPrompts(projectName, packageManager) {
|
|
|
145
145
|
|
|
146
146
|
//#endregion
|
|
147
147
|
//#region src/template-engine.ts
|
|
148
|
-
const CONDITIONAL_BLOCK_RE = /^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{#if:(
|
|
148
|
+
const CONDITIONAL_BLOCK_RE = /^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{#if:(!?\w+)\}\}[^\S\n]*(?:-->)?[^\S\n]*\n([\s\S]*?)^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{\/if:\1\}\}[^\S\n]*(?:-->)?[^\S\n]*\n?/gm;
|
|
149
149
|
const VARIABLE_RE = /\{\{(\w+)\}\}/g;
|
|
150
150
|
function renderTemplate(content, context) {
|
|
151
151
|
let result = content.replace(CONDITIONAL_BLOCK_RE, (_match, name, body) => {
|
|
152
|
-
|
|
152
|
+
const negated = name.startsWith("!");
|
|
153
|
+
const value = context[negated ? name.slice(1) : name];
|
|
154
|
+
if (negated ? !value : value) return body;
|
|
153
155
|
return "";
|
|
154
156
|
});
|
|
155
157
|
result = result.replace(VARIABLE_RE, (_match, name) => {
|
package/package.json
CHANGED
|
@@ -10,6 +10,26 @@ services:
|
|
|
10
10
|
volumes:
|
|
11
11
|
- pgdata:/var/lib/postgresql/data
|
|
12
12
|
|
|
13
|
+
zero-cache:
|
|
14
|
+
image: rocicorp/zero:${ZERO_VERSION}
|
|
15
|
+
restart: always
|
|
16
|
+
ports:
|
|
17
|
+
- 4848:4848
|
|
18
|
+
environment:
|
|
19
|
+
ZERO_UPSTREAM_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}
|
|
20
|
+
ZERO_CVR_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}_cvr
|
|
21
|
+
ZERO_CHANGE_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}_cdb
|
|
22
|
+
ZERO_REPLICA_FILE: /data/sync-replica.db
|
|
23
|
+
ZERO_PUSH_URL: http://app:3000/api/zero/mutate
|
|
24
|
+
ZERO_QUERY_URL: http://app:3000/api/zero/get-queries
|
|
25
|
+
ZERO_QUERY_FORWARD_COOKIES: "true"
|
|
26
|
+
ZERO_MUTATE_FORWARD_COOKIES: "true"
|
|
27
|
+
ZERO_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
|
28
|
+
volumes:
|
|
29
|
+
- zero-data:/data
|
|
30
|
+
depends_on:
|
|
31
|
+
- db
|
|
32
|
+
|
|
13
33
|
app:
|
|
14
34
|
build: .
|
|
15
35
|
restart: always
|
|
@@ -20,8 +40,11 @@ services:
|
|
|
20
40
|
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
|
21
41
|
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
|
|
22
42
|
ORIGIN: ${BETTER_AUTH_URL}
|
|
43
|
+
PUBLIC_SERVER: http://zero-cache:4848
|
|
23
44
|
depends_on:
|
|
24
45
|
- db
|
|
46
|
+
- zero-cache
|
|
25
47
|
|
|
26
48
|
volumes:
|
|
27
49
|
pgdata:
|
|
50
|
+
zero-data:
|
|
@@ -2,114 +2,114 @@ import { boolean, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/p
|
|
|
2
2
|
|
|
3
3
|
// better-auth tables (managed by better-auth, do not insert/update directly)
|
|
4
4
|
export const user = pgTable("user", {
|
|
5
|
-
id: text(
|
|
6
|
-
name: text(
|
|
7
|
-
email: text(
|
|
8
|
-
emailVerified: boolean(
|
|
9
|
-
image: text(
|
|
10
|
-
createdAt: timestamp(
|
|
11
|
-
updatedAt: timestamp(
|
|
5
|
+
id: text().primaryKey(),
|
|
6
|
+
name: text().notNull(),
|
|
7
|
+
email: text().notNull().unique(),
|
|
8
|
+
emailVerified: boolean().notNull().default(false),
|
|
9
|
+
image: text(),
|
|
10
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
11
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
export const session = pgTable("session", {
|
|
15
|
-
id: text(
|
|
16
|
-
expiresAt: timestamp(
|
|
17
|
-
token: text(
|
|
18
|
-
ipAddress: text(
|
|
19
|
-
userAgent: text(
|
|
20
|
-
userId: text(
|
|
15
|
+
id: text().primaryKey(),
|
|
16
|
+
expiresAt: timestamp().notNull(),
|
|
17
|
+
token: text().notNull().unique(),
|
|
18
|
+
ipAddress: text(),
|
|
19
|
+
userAgent: text(),
|
|
20
|
+
userId: text()
|
|
21
21
|
.notNull()
|
|
22
22
|
.references(() => user.id),
|
|
23
|
-
createdAt: timestamp(
|
|
24
|
-
updatedAt: timestamp(
|
|
23
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
24
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
export const account = pgTable("account", {
|
|
28
|
-
id: text(
|
|
29
|
-
accountId: text(
|
|
30
|
-
providerId: text(
|
|
31
|
-
userId: text(
|
|
28
|
+
id: text().primaryKey(),
|
|
29
|
+
accountId: text().notNull(),
|
|
30
|
+
providerId: text().notNull(),
|
|
31
|
+
userId: text()
|
|
32
32
|
.notNull()
|
|
33
33
|
.references(() => user.id),
|
|
34
|
-
accessToken: text(
|
|
35
|
-
refreshToken: text(
|
|
36
|
-
idToken: text(
|
|
37
|
-
accessTokenExpiresAt: timestamp(
|
|
38
|
-
refreshTokenExpiresAt: timestamp(
|
|
39
|
-
scope: text(
|
|
40
|
-
password: text(
|
|
41
|
-
createdAt: timestamp(
|
|
42
|
-
updatedAt: timestamp(
|
|
34
|
+
accessToken: text(),
|
|
35
|
+
refreshToken: text(),
|
|
36
|
+
idToken: text(),
|
|
37
|
+
accessTokenExpiresAt: timestamp(),
|
|
38
|
+
refreshTokenExpiresAt: timestamp(),
|
|
39
|
+
scope: text(),
|
|
40
|
+
password: text(),
|
|
41
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
42
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
export const verification = pgTable("verification", {
|
|
46
|
-
id: text(
|
|
47
|
-
identifier: text(
|
|
48
|
-
value: text(
|
|
49
|
-
expiresAt: timestamp(
|
|
50
|
-
createdAt: timestamp(
|
|
51
|
-
updatedAt: timestamp(
|
|
46
|
+
id: text().primaryKey(),
|
|
47
|
+
identifier: text().notNull(),
|
|
48
|
+
value: text().notNull(),
|
|
49
|
+
expiresAt: timestamp().notNull(),
|
|
50
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
51
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
// Notification tables (managed by @mikstack/notifications)
|
|
55
55
|
export const notificationDelivery = pgTable("notification_delivery", {
|
|
56
|
-
id: text(
|
|
56
|
+
id: text()
|
|
57
57
|
.primaryKey()
|
|
58
58
|
.$defaultFn(() => crypto.randomUUID()),
|
|
59
|
-
userId: text(
|
|
60
|
-
type: text(
|
|
61
|
-
channel: text(
|
|
62
|
-
status: text(
|
|
59
|
+
userId: text().references(() => user.id),
|
|
60
|
+
type: text().notNull(),
|
|
61
|
+
channel: text().notNull(),
|
|
62
|
+
status: text({ enum: ["pending", "sent", "delivered", "failed"] })
|
|
63
63
|
.notNull()
|
|
64
64
|
.default("pending"),
|
|
65
|
-
content: jsonb(
|
|
66
|
-
error: text(
|
|
67
|
-
retryOf: text(
|
|
68
|
-
retriesLeft: integer(
|
|
69
|
-
recipientEmail: text(
|
|
70
|
-
externalId: text(
|
|
71
|
-
createdAt: timestamp(
|
|
72
|
-
updatedAt: timestamp(
|
|
65
|
+
content: jsonb(),
|
|
66
|
+
error: text(),
|
|
67
|
+
retryOf: text(),
|
|
68
|
+
retriesLeft: integer().notNull().default(0),
|
|
69
|
+
recipientEmail: text(),
|
|
70
|
+
externalId: text(),
|
|
71
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
72
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
export const inAppNotification = pgTable("in_app_notification", {
|
|
76
|
-
id: text(
|
|
76
|
+
id: text()
|
|
77
77
|
.primaryKey()
|
|
78
78
|
.$defaultFn(() => crypto.randomUUID()),
|
|
79
|
-
userId: text(
|
|
79
|
+
userId: text()
|
|
80
80
|
.notNull()
|
|
81
81
|
.references(() => user.id),
|
|
82
|
-
type: text(
|
|
83
|
-
title: text(
|
|
84
|
-
body: text(
|
|
85
|
-
url: text(
|
|
86
|
-
icon: text(
|
|
87
|
-
read: boolean(
|
|
88
|
-
createdAt: timestamp(
|
|
82
|
+
type: text().notNull(),
|
|
83
|
+
title: text().notNull(),
|
|
84
|
+
body: text(),
|
|
85
|
+
url: text(),
|
|
86
|
+
icon: text(),
|
|
87
|
+
read: boolean().notNull().default(false),
|
|
88
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
export const notificationPreference = pgTable("notification_preference", {
|
|
92
|
-
id: text(
|
|
92
|
+
id: text()
|
|
93
93
|
.primaryKey()
|
|
94
94
|
.$defaultFn(() => crypto.randomUUID()),
|
|
95
|
-
userId: text(
|
|
95
|
+
userId: text()
|
|
96
96
|
.notNull()
|
|
97
97
|
.references(() => user.id),
|
|
98
|
-
notificationType: text(
|
|
99
|
-
channel: text(
|
|
100
|
-
enabled: boolean(
|
|
101
|
-
updatedAt: timestamp(
|
|
98
|
+
notificationType: text().notNull(),
|
|
99
|
+
channel: text().notNull(),
|
|
100
|
+
enabled: boolean().notNull(),
|
|
101
|
+
updatedAt: timestamp().notNull().defaultNow(),
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
// Application tables
|
|
105
105
|
|
|
106
106
|
export const note = pgTable("note", {
|
|
107
|
-
id: text(
|
|
108
|
-
title: text(
|
|
109
|
-
content: text(
|
|
110
|
-
userId: text(
|
|
107
|
+
id: text().primaryKey(),
|
|
108
|
+
title: text().notNull(),
|
|
109
|
+
content: text().notNull().default(""),
|
|
110
|
+
userId: text()
|
|
111
111
|
.notNull()
|
|
112
112
|
.references(() => user.id),
|
|
113
|
-
createdAt: timestamp(
|
|
114
|
-
updatedAt: timestamp(
|
|
113
|
+
createdAt: timestamp().notNull(),
|
|
114
|
+
updatedAt: timestamp().notNull(),
|
|
115
115
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
2
|
+
import NotePencilIcon from "phosphor-svelte/lib/NotePencilIcon";
|
|
3
|
+
import PencilSimpleIcon from "phosphor-svelte/lib/PencilSimpleIcon";
|
|
4
|
+
import PlusIcon from "phosphor-svelte/lib/PlusIcon";
|
|
5
|
+
import SignOutIcon from "phosphor-svelte/lib/SignOutIcon";
|
|
6
|
+
import TrashIcon from "phosphor-svelte/lib/TrashIcon";
|
|
7
|
+
import XIcon from "phosphor-svelte/lib/XIcon";
|
|
8
8
|
import Button from "{{uiPrefix}}/Button";
|
|
9
9
|
import FormField from "{{uiPrefix}}/FormField";
|
|
10
10
|
import Input from "{{uiPrefix}}/Input";
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
import { get_z } from "$lib/z.svelte";
|
|
19
19
|
import { queries } from "$lib/zero/queries";
|
|
20
20
|
import { mutators } from "$lib/zero/mutators";
|
|
21
|
+
// {{#if:i18n}}
|
|
22
|
+
import { useLingui } from "@mikstack/svelte-lingui";
|
|
23
|
+
import LocaleSwitcher from "$lib/LocaleSwitcher.svelte";
|
|
24
|
+
const { t } = useLingui();
|
|
25
|
+
// {{/if:i18n}}
|
|
21
26
|
|
|
22
27
|
let { data } = $props();
|
|
23
28
|
|
|
@@ -39,7 +44,12 @@
|
|
|
39
44
|
let editingId = $state<string | null>(null);
|
|
40
45
|
|
|
41
46
|
const noteSchema = v.object({
|
|
47
|
+
// {{#if:i18n}}
|
|
48
|
+
title: v.pipe(v.string(), v.minLength(1, t`Title is required`)),
|
|
49
|
+
// {{/if:i18n}}
|
|
50
|
+
// {{#if:!i18n}}
|
|
42
51
|
title: v.pipe(v.string(), v.minLength(1, "Title is required")),
|
|
52
|
+
// {{/if:!i18n}}
|
|
43
53
|
content: v.string(),
|
|
44
54
|
});
|
|
45
55
|
|
|
@@ -103,13 +113,26 @@
|
|
|
103
113
|
<div class="container">
|
|
104
114
|
<header class="header">
|
|
105
115
|
<div class="header-title">
|
|
106
|
-
<
|
|
116
|
+
<NotePencilIcon size={24} weight="duotone" />
|
|
117
|
+
<!-- {{#if:i18n}} -->
|
|
118
|
+
<h1>{t`Notes`}</h1>
|
|
119
|
+
<!-- {{/if:i18n}} -->
|
|
120
|
+
<!-- {{#if:!i18n}} -->
|
|
107
121
|
<h1>Notes</h1>
|
|
122
|
+
<!-- {{/if:!i18n}} -->
|
|
108
123
|
</div>
|
|
109
124
|
<div class="header-actions">
|
|
125
|
+
<!-- {{#if:i18n}} -->
|
|
126
|
+
<LocaleSwitcher />
|
|
127
|
+
<!-- {{/if:i18n}} -->
|
|
110
128
|
<span class="email">{data.user.email}</span>
|
|
111
129
|
<Button variant="ghost" onclick={signOut}>
|
|
112
|
-
|
|
130
|
+
<!-- {{#if:i18n}} -->
|
|
131
|
+
<SignOutIcon size={16} weight="bold" /> {t`Sign out`}
|
|
132
|
+
<!-- {{/if:i18n}} -->
|
|
133
|
+
<!-- {{#if:!i18n}} -->
|
|
134
|
+
<SignOutIcon size={16} weight="bold" /> Sign out
|
|
135
|
+
<!-- {{/if:!i18n}} -->
|
|
113
136
|
</Button>
|
|
114
137
|
</div>
|
|
115
138
|
</header>
|
|
@@ -117,13 +140,28 @@
|
|
|
117
140
|
<Separator />
|
|
118
141
|
|
|
119
142
|
<section>
|
|
143
|
+
<!-- {{#if:i18n}} -->
|
|
144
|
+
<h2>{t`New note`}</h2>
|
|
145
|
+
<!-- {{/if:i18n}} -->
|
|
146
|
+
<!-- {{#if:!i18n}} -->
|
|
120
147
|
<h2>New note</h2>
|
|
148
|
+
<!-- {{/if:!i18n}} -->
|
|
121
149
|
<form id={createNoteForm.id} onsubmit={createNoteForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
|
|
122
150
|
<FormField for={createNoteForm.fields.title.as("text").id}>
|
|
123
151
|
{#snippet label(attrs)}
|
|
152
|
+
<!-- {{#if:i18n}} -->
|
|
153
|
+
<label {...attrs}>{t`Title`}</label>
|
|
154
|
+
<!-- {{/if:i18n}} -->
|
|
155
|
+
<!-- {{#if:!i18n}} -->
|
|
124
156
|
<label {...attrs}>Title</label>
|
|
157
|
+
<!-- {{/if:!i18n}} -->
|
|
125
158
|
{/snippet}
|
|
159
|
+
<!-- {{#if:i18n}} -->
|
|
160
|
+
<Input {...createNoteForm.fields.title.as("text")} placeholder={t`Note title`} />
|
|
161
|
+
<!-- {{/if:i18n}} -->
|
|
162
|
+
<!-- {{#if:!i18n}} -->
|
|
126
163
|
<Input {...createNoteForm.fields.title.as("text")} placeholder="Note title" />
|
|
164
|
+
<!-- {{/if:!i18n}} -->
|
|
127
165
|
{#snippet error(attrs)}
|
|
128
166
|
{#each createNoteForm.fields.title.issues() as issue (issue.message)}
|
|
129
167
|
<p {...attrs}>{issue.message}</p>
|
|
@@ -133,8 +171,24 @@
|
|
|
133
171
|
|
|
134
172
|
<FormField for={`${createNoteForm.id}-content`}>
|
|
135
173
|
{#snippet label(attrs)}
|
|
174
|
+
<!-- {{#if:i18n}} -->
|
|
175
|
+
<label {...attrs}>{t`Content`}</label>
|
|
176
|
+
<!-- {{/if:i18n}} -->
|
|
177
|
+
<!-- {{#if:!i18n}} -->
|
|
136
178
|
<label {...attrs}>Content</label>
|
|
179
|
+
<!-- {{/if:!i18n}} -->
|
|
137
180
|
{/snippet}
|
|
181
|
+
<!-- {{#if:i18n}} -->
|
|
182
|
+
<Textarea
|
|
183
|
+
name={createNoteForm.fields.content.name()}
|
|
184
|
+
id="{createNoteForm.id}-content"
|
|
185
|
+
oninput={(e) =>
|
|
186
|
+
createNoteForm.fields.content.set((e.target as HTMLTextAreaElement).value)}
|
|
187
|
+
value={createNoteForm.fields.content.value() as string}
|
|
188
|
+
placeholder={t`Write something...`}
|
|
189
|
+
/>
|
|
190
|
+
<!-- {{/if:i18n}} -->
|
|
191
|
+
<!-- {{#if:!i18n}} -->
|
|
138
192
|
<Textarea
|
|
139
193
|
name={createNoteForm.fields.content.name()}
|
|
140
194
|
id="{createNoteForm.id}-content"
|
|
@@ -143,6 +197,7 @@
|
|
|
143
197
|
value={createNoteForm.fields.content.value() as string}
|
|
144
198
|
placeholder="Write something..."
|
|
145
199
|
/>
|
|
200
|
+
<!-- {{/if:!i18n}} -->
|
|
146
201
|
</FormField>
|
|
147
202
|
|
|
148
203
|
{#if createNoteForm.error}
|
|
@@ -150,8 +205,13 @@
|
|
|
150
205
|
{/if}
|
|
151
206
|
|
|
152
207
|
<Button type="submit" disabled={createNoteForm.pending}>
|
|
153
|
-
<
|
|
208
|
+
<PlusIcon size={16} weight="bold" />
|
|
209
|
+
<!-- {{#if:i18n}} -->
|
|
210
|
+
{createNoteForm.pending ? t`Creating...` : t`Create note`}
|
|
211
|
+
<!-- {{/if:i18n}} -->
|
|
212
|
+
<!-- {{#if:!i18n}} -->
|
|
154
213
|
{createNoteForm.pending ? "Creating..." : "Create note"}
|
|
214
|
+
<!-- {{/if:!i18n}} -->
|
|
155
215
|
{#if !isMobile}<kbd>{modLabel}+Enter</kbd>{/if}
|
|
156
216
|
</Button>
|
|
157
217
|
</form>
|
|
@@ -160,9 +220,19 @@
|
|
|
160
220
|
<Separator />
|
|
161
221
|
|
|
162
222
|
<section>
|
|
223
|
+
<!-- {{#if:i18n}} -->
|
|
224
|
+
<h2>{t`Your notes`}</h2>
|
|
225
|
+
<!-- {{/if:i18n}} -->
|
|
226
|
+
<!-- {{#if:!i18n}} -->
|
|
163
227
|
<h2>Your notes</h2>
|
|
228
|
+
<!-- {{/if:!i18n}} -->
|
|
164
229
|
{#if notes.length === 0}
|
|
230
|
+
<!-- {{#if:i18n}} -->
|
|
231
|
+
<p class="empty">{t`No notes yet. Create one above!`}</p>
|
|
232
|
+
<!-- {{/if:i18n}} -->
|
|
233
|
+
<!-- {{#if:!i18n}} -->
|
|
165
234
|
<p class="empty">No notes yet. Create one above!</p>
|
|
235
|
+
<!-- {{/if:!i18n}} -->
|
|
166
236
|
{:else}
|
|
167
237
|
<ul class="note-list">
|
|
168
238
|
{#each notes as note (note.id)}
|
|
@@ -171,7 +241,12 @@
|
|
|
171
241
|
<form id={editForm.id} onsubmit={editForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
|
|
172
242
|
<FormField for={editForm.fields.title.as("text").id}>
|
|
173
243
|
{#snippet label(attrs)}
|
|
244
|
+
<!-- {{#if:i18n}} -->
|
|
245
|
+
<label {...attrs}>{t`Title`}</label>
|
|
246
|
+
<!-- {{/if:i18n}} -->
|
|
247
|
+
<!-- {{#if:!i18n}} -->
|
|
174
248
|
<label {...attrs}>Title</label>
|
|
249
|
+
<!-- {{/if:!i18n}} -->
|
|
175
250
|
{/snippet}
|
|
176
251
|
<Input {...editForm.fields.title.as("text")} />
|
|
177
252
|
{#snippet error(attrs)}
|
|
@@ -183,7 +258,12 @@
|
|
|
183
258
|
|
|
184
259
|
<FormField for={`${editForm.id}-content`}>
|
|
185
260
|
{#snippet label(attrs)}
|
|
261
|
+
<!-- {{#if:i18n}} -->
|
|
262
|
+
<label {...attrs}>{t`Content`}</label>
|
|
263
|
+
<!-- {{/if:i18n}} -->
|
|
264
|
+
<!-- {{#if:!i18n}} -->
|
|
186
265
|
<label {...attrs}>Content</label>
|
|
266
|
+
<!-- {{/if:!i18n}} -->
|
|
187
267
|
{/snippet}
|
|
188
268
|
<Textarea
|
|
189
269
|
name={editForm.fields.content.name()}
|
|
@@ -201,11 +281,21 @@
|
|
|
201
281
|
|
|
202
282
|
<div class="actions">
|
|
203
283
|
<Button type="submit" disabled={editForm.pending}>
|
|
284
|
+
<!-- {{#if:i18n}} -->
|
|
285
|
+
{editForm.pending ? t`Saving...` : t`Save`}
|
|
286
|
+
<!-- {{/if:i18n}} -->
|
|
287
|
+
<!-- {{#if:!i18n}} -->
|
|
204
288
|
{editForm.pending ? "Saving..." : "Save"}
|
|
289
|
+
<!-- {{/if:!i18n}} -->
|
|
205
290
|
{#if !isMobile}<kbd>{modLabel}+Enter</kbd>{/if}
|
|
206
291
|
</Button>
|
|
207
292
|
<Button variant="ghost" type="button" onclick={cancelEdit}>
|
|
208
|
-
|
|
293
|
+
<!-- {{#if:i18n}} -->
|
|
294
|
+
<XIcon size={16} weight="bold" /> {t`Cancel`}
|
|
295
|
+
<!-- {{/if:i18n}} -->
|
|
296
|
+
<!-- {{#if:!i18n}} -->
|
|
297
|
+
<XIcon size={16} weight="bold" /> Cancel
|
|
298
|
+
<!-- {{/if:!i18n}} -->
|
|
209
299
|
</Button>
|
|
210
300
|
</div>
|
|
211
301
|
</form>
|
|
@@ -218,10 +308,20 @@
|
|
|
218
308
|
</div>
|
|
219
309
|
<div class="actions">
|
|
220
310
|
<Button variant="ghost" onclick={() => startEdit(note)}>
|
|
221
|
-
|
|
311
|
+
<!-- {{#if:i18n}} -->
|
|
312
|
+
<PencilSimpleIcon size={16} /> {t`Edit`}
|
|
313
|
+
<!-- {{/if:i18n}} -->
|
|
314
|
+
<!-- {{#if:!i18n}} -->
|
|
315
|
+
<PencilSimpleIcon size={16} /> Edit
|
|
316
|
+
<!-- {{/if:!i18n}} -->
|
|
222
317
|
</Button>
|
|
223
318
|
<Button variant="danger" onclick={() => deleteNote(note.id)}>
|
|
224
|
-
|
|
319
|
+
<!-- {{#if:i18n}} -->
|
|
320
|
+
<TrashIcon size={16} /> {t`Delete`}
|
|
321
|
+
<!-- {{/if:i18n}} -->
|
|
322
|
+
<!-- {{#if:!i18n}} -->
|
|
323
|
+
<TrashIcon size={16} /> Delete
|
|
324
|
+
<!-- {{/if:!i18n}} -->
|
|
225
325
|
</Button>
|
|
226
326
|
</div>
|
|
227
327
|
{/if}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import EnvelopeSimpleIcon from "phosphor-svelte/lib/EnvelopeSimpleIcon";
|
|
3
|
+
import CheckCircleIcon from "phosphor-svelte/lib/CheckCircleIcon";
|
|
4
4
|
import Button from "{{uiPrefix}}/Button";
|
|
5
5
|
import FormField from "{{uiPrefix}}/FormField";
|
|
6
6
|
import Input from "{{uiPrefix}}/Input";
|
|
7
7
|
import { createForm } from "@mikstack/form";
|
|
8
8
|
import * as v from "valibot";
|
|
9
9
|
import { authClient } from "$lib/auth-client";
|
|
10
|
+
// {{#if:i18n}}
|
|
11
|
+
import { useLingui } from "@mikstack/svelte-lingui";
|
|
12
|
+
import LocaleSwitcher from "$lib/LocaleSwitcher.svelte";
|
|
13
|
+
const { t } = useLingui();
|
|
14
|
+
// {{/if:i18n}}
|
|
10
15
|
|
|
11
16
|
const form = createForm({
|
|
12
17
|
schema: v.object({
|
|
18
|
+
// {{#if:i18n}}
|
|
19
|
+
email: v.pipe(v.string(), v.email(t`Please enter a valid email address`)),
|
|
20
|
+
// {{/if:i18n}}
|
|
21
|
+
// {{#if:!i18n}}
|
|
13
22
|
email: v.pipe(v.string(), v.email("Please enter a valid email address")),
|
|
23
|
+
// {{/if:!i18n}}
|
|
14
24
|
}),
|
|
15
25
|
initialValues: { email: "" },
|
|
16
26
|
async onSubmit(data) {
|
|
@@ -27,18 +37,36 @@
|
|
|
27
37
|
</script>
|
|
28
38
|
|
|
29
39
|
<div class="sign-in">
|
|
40
|
+
<!-- {{#if:i18n}} -->
|
|
41
|
+
<div class="locale-bar">
|
|
42
|
+
<LocaleSwitcher />
|
|
43
|
+
</div>
|
|
44
|
+
<h1>{t`Sign in to {{projectName}}`}</h1>
|
|
45
|
+
<!-- {{/if:i18n}} -->
|
|
46
|
+
<!-- {{#if:!i18n}} -->
|
|
30
47
|
<h1>Sign in to {{projectName}}</h1>
|
|
48
|
+
<!-- {{/if:!i18n}} -->
|
|
31
49
|
|
|
32
50
|
{#if form.result}
|
|
33
51
|
<div class="success">
|
|
34
|
-
<
|
|
52
|
+
<CheckCircleIcon size={24} weight="duotone" />
|
|
53
|
+
<!-- {{#if:i18n}} -->
|
|
54
|
+
<p>{t`Check your email for a magic link to sign in.`}</p>
|
|
55
|
+
<!-- {{/if:i18n}} -->
|
|
56
|
+
<!-- {{#if:!i18n}} -->
|
|
35
57
|
<p>Check your email for a magic link to sign in.</p>
|
|
58
|
+
<!-- {{/if:!i18n}} -->
|
|
36
59
|
</div>
|
|
37
60
|
{:else}
|
|
38
61
|
<form id={form.id} onsubmit={form.onsubmit} class="sign-in-form">
|
|
39
62
|
<FormField for={emailField.as("email").id}>
|
|
40
63
|
{#snippet label(attrs)}
|
|
64
|
+
<!-- {{#if:i18n}} -->
|
|
65
|
+
<label {...attrs}>{t`Email`}</label>
|
|
66
|
+
<!-- {{/if:i18n}} -->
|
|
67
|
+
<!-- {{#if:!i18n}} -->
|
|
41
68
|
<label {...attrs}>Email</label>
|
|
69
|
+
<!-- {{/if:!i18n}} -->
|
|
42
70
|
{/snippet}
|
|
43
71
|
<Input {...emailField.as("email")} placeholder="you@example.com" />
|
|
44
72
|
{#snippet error(attrs)}
|
|
@@ -53,8 +81,13 @@
|
|
|
53
81
|
{/if}
|
|
54
82
|
|
|
55
83
|
<Button type="submit" disabled={form.pending}>
|
|
56
|
-
<
|
|
84
|
+
<EnvelopeSimpleIcon size={16} weight="bold" />
|
|
85
|
+
<!-- {{#if:i18n}} -->
|
|
86
|
+
{form.pending ? t`Sending magic link...` : t`Sign in with magic link`}
|
|
87
|
+
<!-- {{/if:i18n}} -->
|
|
88
|
+
<!-- {{#if:!i18n}} -->
|
|
57
89
|
{form.pending ? "Sending magic link..." : "Sign in with magic link"}
|
|
90
|
+
<!-- {{/if:!i18n}} -->
|
|
58
91
|
</Button>
|
|
59
92
|
</form>
|
|
60
93
|
{/if}
|
|
@@ -70,6 +103,11 @@
|
|
|
70
103
|
gap: var(--space-5);
|
|
71
104
|
}
|
|
72
105
|
|
|
106
|
+
.locale-bar {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: flex-end;
|
|
109
|
+
}
|
|
110
|
+
|
|
73
111
|
h1 {
|
|
74
112
|
font-size: var(--text-2xl);
|
|
75
113
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { locales, getLocale, setLocale } from "$lib/i18n";
|
|
3
|
+
|
|
4
|
+
let currentLocale = $state(getLocale());
|
|
5
|
+
|
|
6
|
+
function switchLocale(locale: string) {
|
|
7
|
+
setLocale(locale);
|
|
8
|
+
currentLocale = locale;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="locale-switcher">
|
|
13
|
+
{#each locales as locale (locale)}
|
|
14
|
+
<button
|
|
15
|
+
class:active={locale === currentLocale}
|
|
16
|
+
disabled={locale === currentLocale}
|
|
17
|
+
onclick={() => switchLocale(locale)}
|
|
18
|
+
>
|
|
19
|
+
{locale.toUpperCase()}
|
|
20
|
+
</button>
|
|
21
|
+
{/each}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.locale-switcher {
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: var(--space-1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button {
|
|
31
|
+
padding: var(--space-1) var(--space-2);
|
|
32
|
+
font-size: var(--text-xs);
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
border: 1px solid transparent;
|
|
35
|
+
border-radius: var(--radius-sm);
|
|
36
|
+
background: none;
|
|
37
|
+
color: var(--text-2);
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button:hover:not(:disabled) {
|
|
42
|
+
background-color: var(--surface-2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button.active {
|
|
46
|
+
color: var(--text-1);
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
cursor: default;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { setupI18n } from "@lingui/core";
|
|
2
2
|
import { setI18n } from "@mikstack/svelte-lingui";
|
|
3
|
-
import { messages } from "../locales/en.po";
|
|
3
|
+
import { messages as enMessages } from "../locales/en.po";
|
|
4
|
+
import { messages as fiMessages } from "../locales/fi.po";
|
|
5
|
+
|
|
6
|
+
const allMessages: Record<string, typeof enMessages> = {
|
|
7
|
+
en: enMessages,
|
|
8
|
+
fi: fiMessages,
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
const i18n = setupI18n();
|
|
6
12
|
|
|
7
|
-
export function initI18n(): void {
|
|
8
|
-
i18n.loadAndActivate({ locale
|
|
13
|
+
export function initI18n(locale = "en"): void {
|
|
14
|
+
i18n.loadAndActivate({ locale, messages: allMessages[locale] });
|
|
9
15
|
setI18n(i18n);
|
|
10
16
|
}
|
|
17
|
+
|
|
18
|
+
export function setLocale(locale: string): void {
|
|
19
|
+
const messages = allMessages[locale];
|
|
20
|
+
if (messages) {
|
|
21
|
+
i18n.loadAndActivate({ locale, messages });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getLocale(): string {
|
|
26
|
+
return i18n.locale;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const locales = Object.keys(allMessages);
|
|
@@ -4,3 +4,95 @@ msgstr ""
|
|
|
4
4
|
"MIME-Version: 1.0\n"
|
|
5
5
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
6
6
|
"Content-Transfer-Encoding: 8bit\n"
|
|
7
|
+
|
|
8
|
+
#: src/routes/(app)/+page.svelte
|
|
9
|
+
msgid "Cancel"
|
|
10
|
+
msgstr "Cancel"
|
|
11
|
+
|
|
12
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
13
|
+
msgid "Check your email for a magic link to sign in."
|
|
14
|
+
msgstr "Check your email for a magic link to sign in."
|
|
15
|
+
|
|
16
|
+
#: src/routes/(app)/+page.svelte
|
|
17
|
+
msgid "Content"
|
|
18
|
+
msgstr "Content"
|
|
19
|
+
|
|
20
|
+
#: src/routes/(app)/+page.svelte
|
|
21
|
+
msgid "Create note"
|
|
22
|
+
msgstr "Create note"
|
|
23
|
+
|
|
24
|
+
#: src/routes/(app)/+page.svelte
|
|
25
|
+
msgid "Creating..."
|
|
26
|
+
msgstr "Creating..."
|
|
27
|
+
|
|
28
|
+
#: src/routes/(app)/+page.svelte
|
|
29
|
+
msgid "Delete"
|
|
30
|
+
msgstr "Delete"
|
|
31
|
+
|
|
32
|
+
#: src/routes/(app)/+page.svelte
|
|
33
|
+
msgid "Edit"
|
|
34
|
+
msgstr "Edit"
|
|
35
|
+
|
|
36
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
37
|
+
msgid "Email"
|
|
38
|
+
msgstr "Email"
|
|
39
|
+
|
|
40
|
+
#: src/routes/(app)/+page.svelte
|
|
41
|
+
msgid "New note"
|
|
42
|
+
msgstr "New note"
|
|
43
|
+
|
|
44
|
+
#: src/routes/(app)/+page.svelte
|
|
45
|
+
msgid "No notes yet. Create one above!"
|
|
46
|
+
msgstr "No notes yet. Create one above!"
|
|
47
|
+
|
|
48
|
+
#: src/routes/(app)/+page.svelte
|
|
49
|
+
msgid "Note title"
|
|
50
|
+
msgstr "Note title"
|
|
51
|
+
|
|
52
|
+
#: src/routes/(app)/+page.svelte
|
|
53
|
+
msgid "Notes"
|
|
54
|
+
msgstr "Notes"
|
|
55
|
+
|
|
56
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
57
|
+
msgid "Please enter a valid email address"
|
|
58
|
+
msgstr "Please enter a valid email address"
|
|
59
|
+
|
|
60
|
+
#: src/routes/(app)/+page.svelte
|
|
61
|
+
msgid "Save"
|
|
62
|
+
msgstr "Save"
|
|
63
|
+
|
|
64
|
+
#: src/routes/(app)/+page.svelte
|
|
65
|
+
msgid "Saving..."
|
|
66
|
+
msgstr "Saving..."
|
|
67
|
+
|
|
68
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
69
|
+
msgid "Sending magic link..."
|
|
70
|
+
msgstr "Sending magic link..."
|
|
71
|
+
|
|
72
|
+
#: src/routes/(app)/+page.svelte
|
|
73
|
+
msgid "Sign out"
|
|
74
|
+
msgstr "Sign out"
|
|
75
|
+
|
|
76
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
77
|
+
msgid "Sign in to {{projectName}}"
|
|
78
|
+
msgstr "Sign in to {{projectName}}"
|
|
79
|
+
|
|
80
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
81
|
+
msgid "Sign in with magic link"
|
|
82
|
+
msgstr "Sign in with magic link"
|
|
83
|
+
|
|
84
|
+
#: src/routes/(app)/+page.svelte
|
|
85
|
+
msgid "Title"
|
|
86
|
+
msgstr "Title"
|
|
87
|
+
|
|
88
|
+
#: src/routes/(app)/+page.svelte
|
|
89
|
+
msgid "Title is required"
|
|
90
|
+
msgstr "Title is required"
|
|
91
|
+
|
|
92
|
+
#: src/routes/(app)/+page.svelte
|
|
93
|
+
msgid "Write something..."
|
|
94
|
+
msgstr "Write something..."
|
|
95
|
+
|
|
96
|
+
#: src/routes/(app)/+page.svelte
|
|
97
|
+
msgid "Your notes"
|
|
98
|
+
msgstr "Your notes"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
msgid ""
|
|
2
|
+
msgstr ""
|
|
3
|
+
"Language: fi\n"
|
|
4
|
+
"MIME-Version: 1.0\n"
|
|
5
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
6
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
7
|
+
|
|
8
|
+
#: src/routes/(app)/+page.svelte
|
|
9
|
+
msgid "Cancel"
|
|
10
|
+
msgstr "Peruuta"
|
|
11
|
+
|
|
12
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
13
|
+
msgid "Check your email for a magic link to sign in."
|
|
14
|
+
msgstr "Tarkista sähköpostisi kirjautumislinkin varalta."
|
|
15
|
+
|
|
16
|
+
#: src/routes/(app)/+page.svelte
|
|
17
|
+
msgid "Content"
|
|
18
|
+
msgstr "Sisältö"
|
|
19
|
+
|
|
20
|
+
#: src/routes/(app)/+page.svelte
|
|
21
|
+
msgid "Create note"
|
|
22
|
+
msgstr "Luo muistiinpano"
|
|
23
|
+
|
|
24
|
+
#: src/routes/(app)/+page.svelte
|
|
25
|
+
msgid "Creating..."
|
|
26
|
+
msgstr "Luodaan..."
|
|
27
|
+
|
|
28
|
+
#: src/routes/(app)/+page.svelte
|
|
29
|
+
msgid "Delete"
|
|
30
|
+
msgstr "Poista"
|
|
31
|
+
|
|
32
|
+
#: src/routes/(app)/+page.svelte
|
|
33
|
+
msgid "Edit"
|
|
34
|
+
msgstr "Muokkaa"
|
|
35
|
+
|
|
36
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
37
|
+
msgid "Email"
|
|
38
|
+
msgstr "Sähköposti"
|
|
39
|
+
|
|
40
|
+
#: src/routes/(app)/+page.svelte
|
|
41
|
+
msgid "New note"
|
|
42
|
+
msgstr "Uusi muistiinpano"
|
|
43
|
+
|
|
44
|
+
#: src/routes/(app)/+page.svelte
|
|
45
|
+
msgid "No notes yet. Create one above!"
|
|
46
|
+
msgstr "Ei vielä muistiinpanoja. Luo ensimmäinen yllä!"
|
|
47
|
+
|
|
48
|
+
#: src/routes/(app)/+page.svelte
|
|
49
|
+
msgid "Note title"
|
|
50
|
+
msgstr "Muistiinpanon otsikko"
|
|
51
|
+
|
|
52
|
+
#: src/routes/(app)/+page.svelte
|
|
53
|
+
msgid "Notes"
|
|
54
|
+
msgstr "Muistiinpanot"
|
|
55
|
+
|
|
56
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
57
|
+
msgid "Please enter a valid email address"
|
|
58
|
+
msgstr "Syötä kelvollinen sähköpostiosoite"
|
|
59
|
+
|
|
60
|
+
#: src/routes/(app)/+page.svelte
|
|
61
|
+
msgid "Save"
|
|
62
|
+
msgstr "Tallenna"
|
|
63
|
+
|
|
64
|
+
#: src/routes/(app)/+page.svelte
|
|
65
|
+
msgid "Saving..."
|
|
66
|
+
msgstr "Tallennetaan..."
|
|
67
|
+
|
|
68
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
69
|
+
msgid "Sending magic link..."
|
|
70
|
+
msgstr "Lähetetään kirjautumislinkkiä..."
|
|
71
|
+
|
|
72
|
+
#: src/routes/(app)/+page.svelte
|
|
73
|
+
msgid "Sign out"
|
|
74
|
+
msgstr "Kirjaudu ulos"
|
|
75
|
+
|
|
76
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
77
|
+
msgid "Sign in to {{projectName}}"
|
|
78
|
+
msgstr "Kirjaudu palveluun {{projectName}}"
|
|
79
|
+
|
|
80
|
+
#: src/routes/(public)/sign-in/+page.svelte
|
|
81
|
+
msgid "Sign in with magic link"
|
|
82
|
+
msgstr "Kirjaudu kirjautumislinkillä"
|
|
83
|
+
|
|
84
|
+
#: src/routes/(app)/+page.svelte
|
|
85
|
+
msgid "Title"
|
|
86
|
+
msgstr "Otsikko"
|
|
87
|
+
|
|
88
|
+
#: src/routes/(app)/+page.svelte
|
|
89
|
+
msgid "Title is required"
|
|
90
|
+
msgstr "Otsikko on pakollinen"
|
|
91
|
+
|
|
92
|
+
#: src/routes/(app)/+page.svelte
|
|
93
|
+
msgid "Write something..."
|
|
94
|
+
msgstr "Kirjoita jotain..."
|
|
95
|
+
|
|
96
|
+
#: src/routes/(app)/+page.svelte
|
|
97
|
+
msgid "Your notes"
|
|
98
|
+
msgstr "Muistiinpanosi"
|