@jant/core 0.2.2 → 0.2.4

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.
Files changed (92) hide show
  1. package/dist/client.d.ts +4 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +6 -2
  4. package/dist/lib/assets.d.ts +4 -3
  5. package/dist/lib/assets.d.ts.map +1 -1
  6. package/dist/lib/assets.js +1 -3
  7. package/dist/theme/layouts/BaseLayout.js +0 -5
  8. package/package.json +4 -5
  9. package/src/app.tsx +377 -0
  10. package/src/auth.ts +38 -0
  11. package/src/client.ts +7 -2
  12. package/src/db/index.ts +14 -0
  13. package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
  14. package/src/db/migrations/0001_add_search_fts.sql +40 -0
  15. package/src/db/migrations/0002_collection_path.sql +2 -0
  16. package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
  17. package/src/db/migrations/0004_media_uuid.sql +35 -0
  18. package/src/db/migrations/meta/0000_snapshot.json +784 -0
  19. package/src/db/migrations/meta/_journal.json +41 -0
  20. package/src/db/schema.ts +159 -0
  21. package/src/i18n/EXAMPLES.md +235 -0
  22. package/src/i18n/README.md +296 -0
  23. package/src/i18n/Trans.tsx +31 -0
  24. package/src/i18n/context.tsx +101 -0
  25. package/src/i18n/detect.ts +100 -0
  26. package/src/i18n/i18n.ts +62 -0
  27. package/src/i18n/index.ts +65 -0
  28. package/src/i18n/locales/en.po +875 -0
  29. package/src/i18n/locales/en.ts +1 -0
  30. package/src/i18n/locales/zh-Hans.po +875 -0
  31. package/src/i18n/locales/zh-Hans.ts +1 -0
  32. package/src/i18n/locales/zh-Hant.po +875 -0
  33. package/src/i18n/locales/zh-Hant.ts +1 -0
  34. package/src/i18n/locales.ts +14 -0
  35. package/src/i18n/middleware.ts +59 -0
  36. package/src/index.ts +42 -0
  37. package/src/lib/assets.ts +49 -0
  38. package/src/lib/constants.ts +67 -0
  39. package/src/lib/image.ts +107 -0
  40. package/src/lib/index.ts +9 -0
  41. package/src/lib/markdown.ts +93 -0
  42. package/src/lib/schemas.ts +92 -0
  43. package/src/lib/sqid.ts +79 -0
  44. package/src/lib/sse.ts +152 -0
  45. package/src/lib/time.ts +117 -0
  46. package/src/lib/url.ts +107 -0
  47. package/src/middleware/auth.ts +59 -0
  48. package/src/preset.css +2 -11
  49. package/src/routes/api/posts.ts +127 -0
  50. package/src/routes/api/search.ts +53 -0
  51. package/src/routes/api/upload.ts +240 -0
  52. package/src/routes/dash/collections.tsx +341 -0
  53. package/src/routes/dash/index.tsx +89 -0
  54. package/src/routes/dash/media.tsx +551 -0
  55. package/src/routes/dash/pages.tsx +245 -0
  56. package/src/routes/dash/posts.tsx +202 -0
  57. package/src/routes/dash/redirects.tsx +155 -0
  58. package/src/routes/dash/settings.tsx +93 -0
  59. package/src/routes/feed/rss.ts +119 -0
  60. package/src/routes/feed/sitemap.ts +75 -0
  61. package/src/routes/pages/archive.tsx +223 -0
  62. package/src/routes/pages/collection.tsx +79 -0
  63. package/src/routes/pages/home.tsx +93 -0
  64. package/src/routes/pages/page.tsx +64 -0
  65. package/src/routes/pages/post.tsx +81 -0
  66. package/src/routes/pages/search.tsx +162 -0
  67. package/src/services/collection.ts +180 -0
  68. package/src/services/index.ts +40 -0
  69. package/src/services/media.ts +97 -0
  70. package/src/services/post.ts +279 -0
  71. package/src/services/redirect.ts +74 -0
  72. package/src/services/search.ts +117 -0
  73. package/src/services/settings.ts +76 -0
  74. package/src/theme/components/ActionButtons.tsx +98 -0
  75. package/src/theme/components/CrudPageHeader.tsx +48 -0
  76. package/src/theme/components/DangerZone.tsx +77 -0
  77. package/src/theme/components/EmptyState.tsx +56 -0
  78. package/src/theme/components/ListItemRow.tsx +24 -0
  79. package/src/theme/components/PageForm.tsx +114 -0
  80. package/src/theme/components/Pagination.tsx +196 -0
  81. package/src/theme/components/PostForm.tsx +122 -0
  82. package/src/theme/components/PostList.tsx +68 -0
  83. package/src/theme/components/ThreadView.tsx +118 -0
  84. package/src/theme/components/TypeBadge.tsx +28 -0
  85. package/src/theme/components/VisibilityBadge.tsx +33 -0
  86. package/src/theme/components/index.ts +12 -0
  87. package/src/theme/index.ts +24 -0
  88. package/src/theme/layouts/BaseLayout.tsx +50 -0
  89. package/src/theme/layouts/DashLayout.tsx +108 -0
  90. package/src/theme/layouts/index.ts +2 -0
  91. package/src/types.ts +222 -0
  92. package/static/assets/datastar.min.js +0 -7
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1769858252020,
9
+ "tag": "0000_solid_moon_knight",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1769859000000,
16
+ "tag": "0001_add_search_fts",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1769860000000,
23
+ "tag": "0002_collection_path",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1769861000000,
30
+ "tag": "0003_collection_path_nullable",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1770024000000,
37
+ "tag": "0004_media_uuid",
38
+ "breakpoints": true
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Drizzle Schema
3
+ *
4
+ * Database schema for Jant
5
+ */
6
+
7
+ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";
8
+
9
+ // =============================================================================
10
+ // Posts
11
+ // =============================================================================
12
+
13
+ export const posts = sqliteTable("posts", {
14
+ id: integer("id").primaryKey({ autoIncrement: true }),
15
+ type: text("type", { enum: ["note", "article", "link", "quote", "image", "page"] }).notNull(),
16
+ visibility: text("visibility", { enum: ["featured", "quiet", "unlisted", "draft"] })
17
+ .notNull()
18
+ .default("quiet"),
19
+ title: text("title"),
20
+ path: text("path"),
21
+ content: text("content"),
22
+ contentHtml: text("content_html"),
23
+ sourceUrl: text("source_url"),
24
+ sourceName: text("source_name"),
25
+ sourceDomain: text("source_domain"),
26
+ replyToId: integer("reply_to_id"),
27
+ threadId: integer("thread_id"),
28
+ deletedAt: integer("deleted_at"),
29
+ publishedAt: integer("published_at").notNull(),
30
+ createdAt: integer("created_at").notNull(),
31
+ updatedAt: integer("updated_at").notNull(),
32
+ });
33
+
34
+ // =============================================================================
35
+ // Media
36
+ // =============================================================================
37
+
38
+ export const media = sqliteTable("media", {
39
+ id: text("id").primaryKey(), // UUIDv7
40
+ postId: integer("post_id").references(() => posts.id),
41
+ filename: text("filename").notNull(),
42
+ originalName: text("original_name").notNull(),
43
+ mimeType: text("mime_type").notNull(),
44
+ size: integer("size").notNull(),
45
+ r2Key: text("r2_key").notNull(),
46
+ width: integer("width"),
47
+ height: integer("height"),
48
+ alt: text("alt"),
49
+ createdAt: integer("created_at").notNull(),
50
+ });
51
+
52
+ // =============================================================================
53
+ // Collections
54
+ // =============================================================================
55
+
56
+ export const collections = sqliteTable("collections", {
57
+ id: integer("id").primaryKey({ autoIncrement: true }),
58
+ path: text("path").unique(),
59
+ title: text("title").notNull(),
60
+ description: text("description"),
61
+ createdAt: integer("created_at").notNull(),
62
+ updatedAt: integer("updated_at").notNull(),
63
+ });
64
+
65
+ // =============================================================================
66
+ // Post-Collections (Many-to-Many)
67
+ // =============================================================================
68
+
69
+ export const postCollections = sqliteTable(
70
+ "post_collections",
71
+ {
72
+ postId: integer("post_id")
73
+ .notNull()
74
+ .references(() => posts.id),
75
+ collectionId: integer("collection_id")
76
+ .notNull()
77
+ .references(() => collections.id),
78
+ addedAt: integer("added_at").notNull(),
79
+ },
80
+ (table) => [primaryKey({ columns: [table.postId, table.collectionId] })]
81
+ );
82
+
83
+ // =============================================================================
84
+ // Redirects
85
+ // =============================================================================
86
+
87
+ export const redirects = sqliteTable("redirects", {
88
+ id: integer("id").primaryKey({ autoIncrement: true }),
89
+ fromPath: text("from_path").notNull().unique(),
90
+ toPath: text("to_path").notNull(),
91
+ type: integer("type", { mode: "number" }).notNull().default(301),
92
+ createdAt: integer("created_at").notNull(),
93
+ });
94
+
95
+ // =============================================================================
96
+ // Settings (Key-Value)
97
+ // =============================================================================
98
+
99
+ export const settings = sqliteTable("settings", {
100
+ key: text("key").primaryKey(),
101
+ value: text("value").notNull(),
102
+ updatedAt: integer("updated_at").notNull(),
103
+ });
104
+
105
+ // =============================================================================
106
+ // better-auth tables
107
+ // Note: Using { mode: "timestamp" } so drizzle auto-converts Date <-> integer
108
+ // =============================================================================
109
+
110
+ export const user = sqliteTable("user", {
111
+ id: text("id").primaryKey(),
112
+ name: text("name").notNull(),
113
+ email: text("email").notNull().unique(),
114
+ emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
115
+ image: text("image"),
116
+ role: text("role").default("admin"),
117
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
118
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
119
+ });
120
+
121
+ export const session = sqliteTable("session", {
122
+ id: text("id").primaryKey(),
123
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
124
+ token: text("token").notNull().unique(),
125
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
126
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
127
+ ipAddress: text("ip_address"),
128
+ userAgent: text("user_agent"),
129
+ userId: text("user_id")
130
+ .notNull()
131
+ .references(() => user.id),
132
+ });
133
+
134
+ export const account = sqliteTable("account", {
135
+ id: text("id").primaryKey(),
136
+ accountId: text("account_id").notNull(),
137
+ providerId: text("provider_id").notNull(),
138
+ userId: text("user_id")
139
+ .notNull()
140
+ .references(() => user.id),
141
+ accessToken: text("access_token"),
142
+ refreshToken: text("refresh_token"),
143
+ idToken: text("id_token"),
144
+ accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
145
+ refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
146
+ scope: text("scope"),
147
+ password: text("password"),
148
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
149
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
150
+ });
151
+
152
+ export const verification = sqliteTable("verification", {
153
+ id: text("id").primaryKey(),
154
+ identifier: text("identifier").notNull(),
155
+ value: text("value").notNull(),
156
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
157
+ createdAt: integer("created_at", { mode: "timestamp" }),
158
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
159
+ });
@@ -0,0 +1,235 @@
1
+ # i18n Usage Examples
2
+
3
+ ## New API: useLingui() Hook
4
+
5
+ We now use a React-like hook API that eliminates prop drilling and makes code cleaner.
6
+
7
+ ### Basic Pattern
8
+
9
+ ```tsx
10
+ import { I18nProvider, useLingui } from "@/i18n";
11
+
12
+ // 1. Wrap your app in route handler
13
+ dashRoute.get("/", async (c) => {
14
+ return c.html(
15
+ <I18nProvider c={c}>
16
+ <MyApp />
17
+ </I18nProvider>
18
+ );
19
+ });
20
+
21
+ // 2. Use useLingui() hook inside components
22
+ function MyApp() {
23
+ const { t } = useLingui();
24
+
25
+ return <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>;
26
+ }
27
+ ```
28
+
29
+ ### Why the `comment` field?
30
+
31
+ The `comment` field provides context for AI translators, improving translation quality:
32
+
33
+ ```tsx
34
+ // ✅ Good - clear context
35
+ t({ message: "Dashboard", comment: "@context: Page title" })
36
+ t({ message: "Dashboard", comment: "@context: Navigation link" })
37
+
38
+ // ❌ Bad - no context (translator might choose wrong word)
39
+ t({ message: "Dashboard" })
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Complete Example
45
+
46
+ ```tsx
47
+ import { I18nProvider, useLingui, Trans } from "@/i18n";
48
+
49
+ // Route handler: wrap in I18nProvider
50
+ dashRoute.get("/", async (c) => {
51
+ const posts = await c.var.services.posts.list();
52
+
53
+ return c.html(
54
+ <I18nProvider c={c}>
55
+ <Dashboard postCount={posts.length} username="Alice" />
56
+ </I18nProvider>
57
+ );
58
+ });
59
+
60
+ // Component: use useLingui() hook
61
+ function Dashboard({ postCount, username }: { postCount: number; username: string }) {
62
+ const { t } = useLingui();
63
+
64
+ return (
65
+ <div>
66
+ {/* 1. Simple translation */}
67
+ <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
68
+
69
+ {/* 2. With variables */}
70
+ <p>
71
+ {t(
72
+ { message: "Welcome back, {name}!", comment: "@context: Welcome message" },
73
+ { name: username }
74
+ )}
75
+ </p>
76
+
77
+ {/* 3. With dynamic values */}
78
+ <p>
79
+ {t(
80
+ { message: "You have {count} posts", comment: "@context: Post count" },
81
+ { count: postCount }
82
+ )}
83
+ </p>
84
+
85
+ {/* 4. With embedded components */}
86
+ <p>
87
+ <Trans comment="@context: Help text">
88
+ Read the <a href="/docs" class="underline">documentation</a> for help
89
+ </Trans>
90
+ </p>
91
+ </div>
92
+ );
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Trans Component: Embedded JSX
99
+
100
+ The `Trans` component is a simplified implementation that renders children as-is. It's useful for translations with embedded links or formatting.
101
+
102
+ ```tsx
103
+ import { Trans } from "@/i18n";
104
+
105
+ // Simple link
106
+ <Trans comment="@context: Website link">
107
+ Visit <a href="/" class="text-primary">our website</a>
108
+ </Trans>
109
+
110
+ // Multiple elements
111
+ <Trans comment="@context: Learn more link">
112
+ Click <strong class="font-bold">here</strong> to <a href="/learn" class="underline">learn more</a>
113
+ </Trans>
114
+
115
+ // With formatting
116
+ <Trans comment="@context: Welcome message">
117
+ Welcome <strong class="font-semibold">back</strong>!
118
+ </Trans>
119
+ ```
120
+
121
+ **Note**: This is a simplified implementation that renders children directly. For complex translations with dynamic placeholders, use the `t()` function instead:
122
+
123
+ ```tsx
124
+ const { t } = useLingui();
125
+
126
+ // For dynamic content, use t() with placeholders
127
+ <p>
128
+ {t(
129
+ { message: "Visit {linkStart}our website{linkEnd}", comment: "@context: Website link" },
130
+ {
131
+ linkStart: '<a href="/" class="text-primary">',
132
+ linkEnd: '</a>',
133
+ }
134
+ )}
135
+ </p>
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Comparison: Before vs Now
141
+
142
+ ### Before (Prop drilling required)
143
+
144
+ ```tsx
145
+ import { getI18n } from "@/i18n";
146
+
147
+ dashRoute.get("/", async (c) => {
148
+ const i18n = getI18n(c);
149
+
150
+ return c.html(
151
+ <Layout>
152
+ <MyComponent c={c} /> {/* Must pass c prop */}
153
+ </Layout>
154
+ );
155
+ });
156
+
157
+ function MyComponent({ c }: { c: Context }) {
158
+ const i18n = getI18n(c);
159
+ const title = i18n._({ message: "Dashboard", comment: "@context: Title" });
160
+ const greeting = i18n._(
161
+ { message: "Hello {name}", comment: "@context: Greeting" },
162
+ { name: "Alice" }
163
+ );
164
+
165
+ return <h1>{title}</h1>;
166
+ }
167
+ ```
168
+
169
+ ### Now (No prop drilling)
170
+
171
+ ```tsx
172
+ import { I18nProvider, useLingui } from "@/i18n";
173
+
174
+ dashRoute.get("/", async (c) => {
175
+ return c.html(
176
+ <I18nProvider c={c}>
177
+ <Layout>
178
+ <MyComponent /> {/* No props needed */}
179
+ </Layout>
180
+ </I18nProvider>
181
+ );
182
+ });
183
+
184
+ function MyComponent() {
185
+ const { t } = useLingui();
186
+ const title = t({ message: "Dashboard", comment: "@context: Title" });
187
+ const greeting = t(
188
+ { message: "Hello {name}", comment: "@context: Greeting" },
189
+ { name: "Alice" }
190
+ );
191
+
192
+ return <h1>{title}</h1>;
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Best Practices
199
+
200
+ 1. **Always wrap in I18nProvider**: Wrap your app in `<I18nProvider c={c}>` in route handlers
201
+ 2. **Use useLingui() hook**: Call `const { t } = useLingui()` inside components
202
+ 3. **Always include comment**: Provide `@context:` comment for better AI translations
203
+ 4. **Variables as second param**: `t({ message: "Hello {name}", comment: "..." }, { name })`
204
+ 5. **Use Trans for embedded JSX**: Use `<Trans>` for links and formatting
205
+ 6. **Extract translations**: Run `pnpm i18n:extract` after adding new strings
206
+ 7. **Compile translations**: Run `pnpm i18n:compile` to generate catalog files
207
+
208
+ ---
209
+
210
+ ## Common Questions
211
+
212
+ ### Q: Why can't I use `t("Dashboard")` directly?
213
+
214
+ A: Lingui uses a build-time extraction process. The `t()` function expects a message descriptor object that gets transformed by Lingui's macro system during the build. If you pass a plain string, the extraction tool won't be able to find and extract the message for translation.
215
+
216
+ You must use the object syntax:
217
+ ```tsx
218
+ t({ message: "Dashboard", comment: "@context: Page title" })
219
+ ```
220
+
221
+ ### Q: Can I use Lingui's official Trans component?
222
+
223
+ A: Lingui's `Trans` component is designed for React and requires React Context. Since we use Hono JSX (not React), we provide a simplified `Trans` component that works with our SSR setup. It renders children directly without complex transformation.
224
+
225
+ ### Q: Why use useLingui() instead of getI18n(c)?
226
+
227
+ A: The `useLingui()` hook provides a cleaner API that eliminates prop drilling. Instead of passing the Hono context `c` to every component, you wrap your app once in `I18nProvider` and all child components can access i18n via the hook.
228
+
229
+ ### Q: Is this safe for concurrent requests?
230
+
231
+ A: Yes! Each request creates its own i18n instance via `I18nProvider`. The global state is only used during the synchronous rendering phase, so there's no risk of race conditions between concurrent requests.
232
+
233
+ ### Q: What if I call useLingui() outside I18nProvider?
234
+
235
+ A: You'll get an error: "useLingui() called outside of I18nProvider". This is intentional - the hook must be used within the provider context. Always wrap your app in `<I18nProvider c={c}>` in your route handlers.