Skip to content

Commit e2ca21d

Browse files
committed
feat: CMS improvements batch 1-4
Batch 1: - Fix .mcp.json formatting - Remove type assertions in migration files - Add aria-label to toast close button Batch 2: - Fix AddMemberModal accessibility (focus trap, Esc handler, aria) - Add alt text to image-upload preview Batch 3: - Fix HeroSection color scheme (orange → green for brand consistency) Batch 4: - Replace getStats with COUNT queries for performance - Image upload: progressive compression (quality 0.9→0.3, dimension 1920→720), never rejects - Add meta info display (cover image, slug) to Article/Project/Member editors - Document image upload pipeline in docs/knowledges/image-upload.md
1 parent 9f0515c commit e2ca21d

File tree

74 files changed

+2882
-597
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2882
-597
lines changed

.mcp.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"mcpServers": {
33
"svelte": {
44
"type": "stdio",
5-
"command": "npx",
5+
"command": "bunx",
66
"env": {},
7-
"args": ["-y", "@sveltejs/mcp"]
7+
"args": ["@sveltejs/mcp"]
88
}
99
}
1010
}

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,16 @@ run `bun tidy` after you finish your work. i.e. before commit
9797

9898
- Never use `as` or `any`. Let TypeScript infer types properly.
9999
- Never just "fire and forget". it crashes the entire server. instead, catch `.catch(console.error)` then forget, if you want to dispatch the job.
100+
- **NEVER use `$derived(await ...)` in Svelte 5.** This causes infinite loops and memory leaks. Use top-level `await` instead:
101+
```ts
102+
// ❌ WRONG - causes memory leak
103+
const data = $derived(await fetchData());
104+
105+
// ✅ CORRECT - use top-level await
106+
const data = await fetchData();
107+
108+
// ✅ For reactive dependencies, use $derived without await
109+
const filtered = $derived(data.filter(x => x.active));
110+
```
100111

101112
For detailed coding standards (import order, async patterns, naming conventions), see `docs/knowledges/coding-standards.md`.

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dependencies": {
88
"better-auth": "^1.4.6",
99
"bits-ui": "^2.14.4",
10+
"chart.js": "^4.5.1",
1011
"drizzle-valibot": "^0.4.2",
1112
"isomorphic-dompurify": "^2.34.0",
1213
"lucide-svelte": "^0.561.0",
@@ -217,6 +218,8 @@
217218

218219
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
219220

221+
"@kurkle/color": ["@kurkle/[email protected]", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
222+
220223
"@libsql/client": ["@libsql/[email protected]", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="],
221224

222225
"@libsql/core": ["@libsql/[email protected]", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="],
@@ -401,6 +404,8 @@
401404

402405
"call-bound": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
403406

407+
"chart.js": ["[email protected]", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
408+
404409
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
405410

406411
"clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],

docs/knowledges/data-models.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ projectMembers
8686
├── projectId TEXT FK → projects.id ┐
8787
├── memberId TEXT FK → members.id ┘ PK
8888
└── role TEXT -- "lead", "member"
89+
90+
viewLog (時系列アナリティクス用)
91+
├── id TEXT PK
92+
├── resourceType TEXT NOT NULL -- "article", "member", "project"
93+
├── resourceId TEXT NOT NULL
94+
└── viewedAt TIMESTAMP NOT NULL
8995
```
9096

9197
## Actions
@@ -146,10 +152,12 @@ projectMembers
146152

147153
### Analytics
148154

149-
| アクション | 公開 | 実装 |
150-
| ---------------------------- | ---- | ---- |
151-
| 閲覧数統計を見る | ||
152-
| 記事別閲覧数ランキング | ||
155+
| アクション | 公開 | 実装 |
156+
| ------------------------------ | ---- | ---- |
157+
| 閲覧数統計を見る | ||
158+
| 記事別閲覧数ランキング | ||
153159
| プロジェクト別閲覧数ランキング | ||
154-
| メンバー別閲覧数ランキング | ||
160+
| メンバー別閲覧数ランキング | ||
155161
| ダッシュボードで統計概要を見る | ||
162+
| 時系列グラフで閲覧傾向を見る | ||
163+
| コンテンツタイプ別推移を見る | ||

docs/knowledges/image-upload.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Image Upload Pipeline
2+
3+
## Size Limits
4+
5+
```
6+
Browser (any size) → Compress → Server (10MB) → S3 (no limit)
7+
```
8+
9+
| Stage | Limit | Enforcement |
10+
|-------|-------|-------------|
11+
| Browser input | None | User can upload any size |
12+
| Browser → Server | 10MB | Client compresses until it fits |
13+
| Server validation | 10MB (base64 ~13.7MB) | DoS protection |
14+
| Server → S3 | None | MinIO accepts any size |
15+
16+
## Browser Compression
17+
18+
Located in `$lib/shared/logic/image-processing.ts`:
19+
20+
```ts
21+
const SERVER_LIMIT = 10 * 1024 * 1024; // 10MB
22+
const QUALITY_STEPS = [0.9, 0.7, 0.5, 0.3];
23+
const DIMENSION_STEPS = [1920, 1440, 1080, 720];
24+
```
25+
26+
Algorithm:
27+
1. Start at 1920px, quality 0.9
28+
2. If > 10MB, reduce quality (0.9 → 0.7 → 0.5 → 0.3)
29+
3. If all qualities fail, reduce dimension (1920 → 1440)
30+
4. Reset quality, repeat
31+
5. Max 16 attempts (4 qualities × 4 dimensions)
32+
6. Best-effort: if still > 10MB after all attempts, return smallest achieved
33+
34+
## Server Validation
35+
36+
Located in `$lib/data/private/storage.remote.ts`:
37+
38+
```ts
39+
const MAX_BASE64_SIZE = Math.ceil(10 * 1024 * 1024 * 1.37);
40+
```
41+
42+
- Base64 adds ~37% overhead
43+
- 10MB file → ~13.7MB base64
44+
45+
## Design Decisions
46+
47+
- **Never reject user uploads**: Compress instead of refusing
48+
- **Quality over size**: Try high quality first, only reduce if needed
49+
- **GIF exception**: GIFs skip compression (animation would be lost)

docs/knowledges/project-context.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ bun tidy # Auto-check + fix (type + format + lint)
9797
6. **Security**: Never read `.env` files, use `.env.sample` or `.env.example`
9898
7. **Type Safety**: Never use `as` or `any`, let TypeScript infer
9999
8. **Error Handling**: Never "fire and forget", use `.catch(console.error)` for async jobs
100+
9. **Remote Functions**: Always use explicit arrow functions with `query()` and `command()`
101+
-`query(listPublishedArticles)` (function reference)
102+
-`query(async () => listPublishedArticles())` (explicit arrow function)
103+
-`query(v.string(), getPublishedArticle)` (function reference)
104+
-`query(v.string(), async (slug) => getPublishedArticle(slug))` (explicit arrow function)
105+
- Reason: Function references can cause 400 Bad Request errors due to validation issues
100106

101107
## Knowledge Base
102108

docs/knowledges/ui-design.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# UI Design
22

3+
団体要件: プライマリカラー = RGB(0,211,114) #00D372 CMYK(68%, 0, 72%, 0)
4+
35
## Inspiration
46

57
- **Linear**: Typography, clarity, professional developer aesthetic

drizzle/0004_dry_adam_destine.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CREATE TABLE "view_log" (
2+
"id" text PRIMARY KEY NOT NULL,
3+
"resource_type" text NOT NULL,
4+
"resource_id" text NOT NULL,
5+
"viewed_at" timestamp with time zone DEFAULT now() NOT NULL
6+
);
7+
--> statement-breakpoint
8+
CREATE INDEX "view_log_resourceType_resourceId_idx" ON "view_log" USING btree ("resource_type","resource_id");--> statement-breakpoint
9+
CREATE INDEX "view_log_viewedAt_idx" ON "view_log" USING btree ("viewed_at");

0 commit comments

Comments
 (0)