From 91f1a2c7c0f2965f9cdec13b5c9736d5fa51d493 Mon Sep 17 00:00:00 2001 From: Kahhow Lee <44336310+ghostleek@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:39:40 +0800 Subject: [PATCH 1/3] Update claude.md --- claude.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/claude.md b/claude.md index 2b24d57..4661a1f 100644 --- a/claude.md +++ b/claude.md @@ -422,9 +422,9 @@ mkdir extension 3. User types app name → autocomplete suggests existing apps to prevent duplicates 4. If selecting existing app → yellow warning appears 5. Form validates and submits with `status: 'pending'` -6. **Submitter sees their app immediately in dashboard** +6. **App shows immediately on user's personal profile (`/[slug]`)** 7. Admin reviews via Drizzle Studio -8. Approved → visible globally in app directory +8. **Approved** → visible globally on main `string.sg` homepage and searchable in app directory ### From Profile Page (NEW) 1. User visits their own profile → Sees "+ Add App" button (if no apps yet) From 850ec569e6eb656b23d0c188fd379f243a19a6d6 Mon Sep 17 00:00:00 2001 From: Kahhow Lee <44336310+ghostleek@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:43:36 +0800 Subject: [PATCH 2/3] Upsert apps and refresh bump rules Make the seeder idempotent: scripts/seed-apps.ts now upserts apps (onConflictDoUpdate) and returns app IDs instead of skipping existing records, and it updates updatedAt. Bump rules are cleared per-app before reinserting so the JSON remains authoritative, and apps without a DB id are skipped. Update seed data (data/apps-seed.json) to add new teaching apps and adjust featured flags. Minor import tweaks (added eq/asc) to support the changes. --- api/apps.ts | 2 +- data/apps-seed.json | 38 +++++++++++++++++++++++++++++++++-- scripts/seed-apps.ts | 48 ++++++++++++++++++++++++++++++++------------ 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/api/apps.ts b/api/apps.ts index 1e2997a..34eb58f 100644 --- a/api/apps.ts +++ b/api/apps.ts @@ -1,7 +1,7 @@ import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; import { apps, featuredApps, bumpRules, appSubmissions } from '../src/db/schema'; -import { eq, desc, and, gte, lte } from 'drizzle-orm'; +import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'; export const config = { runtime: 'edge', diff --git a/data/apps-seed.json b/data/apps-seed.json index d1cbc41..9ac053c 100644 --- a/data/apps-seed.json +++ b/data/apps-seed.json @@ -463,8 +463,7 @@ "category": "Productivity", "tags": ["ai", "writing", "research", "transcription"], "is_official": true, - "frequency": 0, - "featured": true + "frequency": 0 }, { "name": "SmartCompose", @@ -506,6 +505,41 @@ "tags": ["ai", "mother-tongue", "language", "learning"], "is_official": true, "frequency": 0 + }, + { + "name": "Write Formula Game", + "slug": "write-formula-game", + "url": "https://writeformulagame.com/", + "description": "Interactive game for practising chemistry formula writing", + "tagline": "Make formula writing fun", + "category": "Teaching", + "tags": ["science"], + "is_official": false, + "frequency": 0 + }, + { + "name": "String Buy", + "slug": "buy", + "url": "https://buy.string.sg/", + "description": "Simulate a pit market and visualise demand-supply equilibrium with live price discovery", + "tagline": "Pit market demand-supply simulator", + "category": "Teaching", + "tags": ["economics", "simulation", "games", "classroom"], + "is_official": false, + "frequency": 0, + "featured": true + }, + { + "name": "String Diagrams", + "slug": "diagrams", + "url": "https://diagrams.string.sg/", + "description": "Generate circuit diagrams and build isometric cube illustrations for lessons", + "tagline": "Circuit diagrams & isometric cubes", + "category": "Teaching", + "tags": ["science", "physics", "diagrams", "drawing"], + "is_official": false, + "frequency": 0, + "featured": true } ], "categories": [ diff --git a/scripts/seed-apps.ts b/scripts/seed-apps.ts index 4cc927f..bd38ebd 100644 --- a/scripts/seed-apps.ts +++ b/scripts/seed-apps.ts @@ -6,6 +6,7 @@ import 'dotenv/config'; import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; import { apps, bumpRules, categories } from '../src/db/schema'; +import { eq } from 'drizzle-orm'; import seedData from '../data/apps-seed.json'; async function main() { @@ -31,12 +32,12 @@ async function main() { } console.log(` ✓ ${seedData.categories.length} categories\n`); - // Seed apps + // Seed apps (upsert — re-running always reflects the JSON) console.log('Seeding apps...'); const appIdMap: Record = {}; for (const app of seedData.apps) { - const [inserted] = await db.insert(apps).values({ + const values = { name: app.name, slug: app.slug, url: app.url, @@ -47,26 +48,47 @@ async function main() { isOfficial: app.is_official, frequency: app.frequency, featured: app.featured || false, - }).onConflictDoNothing().returning({ id: apps.id }); + }; - if (inserted) { - appIdMap[app.slug] = inserted.id; - console.log(` ✓ ${app.name}`); - } else { - console.log(` - ${app.name} (already exists)`); - } + const [upserted] = await db.insert(apps) + .values(values) + .onConflictDoUpdate({ + target: apps.slug, + set: { + name: values.name, + url: values.url, + description: values.description, + tagline: values.tagline, + category: values.category, + tags: values.tags, + isOfficial: values.isOfficial, + frequency: values.frequency, + featured: values.featured, + updatedAt: new Date(), + }, + }) + .returning({ id: apps.id }); + + appIdMap[app.slug] = upserted.id; + console.log(` ✓ ${app.name}`); } - console.log(`\n Total: ${Object.keys(appIdMap).length} apps seeded\n`); + console.log(`\n Total: ${Object.keys(appIdMap).length} apps upserted\n`); - // Seed bump rules for apps that have them + // Seed bump rules (delete + reinsert per app so JSON stays authoritative) console.log('Seeding bump rules...'); let ruleCount = 0; for (const app of seedData.apps) { - if (app.bump_rules && appIdMap[app.slug]) { + if (!appIdMap[app.slug]) continue; + const appId = appIdMap[app.slug]; + + // Clear existing rules for this app before reinserting + await db.delete(bumpRules).where(eq(bumpRules.appId, appId)); + + if (app.bump_rules) { for (const rule of app.bump_rules) { await db.insert(bumpRules).values({ - appId: appIdMap[app.slug], + appId, ruleType: rule.type, startTime: rule.start || null, endTime: rule.end || null, From feeed23045388d3607d6a294ba2c38bbf4d2df8b Mon Sep 17 00:00:00 2001 From: Kahhow Lee <44336310+ghostleek@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:23:38 +0800 Subject: [PATCH 3/3] Remove STYLING.md in favour of shared parent-level copy Design system docs moved to string/STYLING.md so all String apps can reference it via the parent CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- STYLING.md | 82 ------------------------------------------------------ 1 file changed, 82 deletions(-) delete mode 100644 STYLING.md diff --git a/STYLING.md b/STYLING.md deleted file mode 100644 index f97c909..0000000 --- a/STYLING.md +++ /dev/null @@ -1,82 +0,0 @@ -# String Design System - -## Color Palette - -### Primary Colors -- **String Mint**: `#75F8CC` (RGB: 117, 248, 204) -- **String Dark**: `#33373B` (RGB: 51, 55, 59) -- **String Light**: `#C0F4FB` (RGB: 192, 244, 251) -- **White**: `#FFFFFF` - -### Usage Guidelines -- **String Mint**: Primary accent, buttons, highlights, active states -- **String Dark**: Text, backgrounds, headers -- **String Light**: Secondary accents, subtle backgrounds -- **White**: Main backgrounds, cards - -## Component Patterns - -### Buttons -```tsx -// Primary Button -className="bg-string-mint text-string-dark hover:bg-string-mint-light px-4 py-2 rounded-xl font-medium transition-colors" - -// Secondary Button -className="bg-white border border-gray-200 text-string-dark hover:bg-gray-50 px-4 py-2 rounded-xl font-medium transition-colors" - -// Text Button -className="text-string-mint hover:text-string-mint-light font-medium transition-colors" -``` - -### Cards -```tsx -className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 hover:border-string-mint transition-colors" -``` - -### Headers -```tsx -// Main Header -className="text-3xl font-bold text-string-dark" - -// Section Header -className="text-xl font-semibold text-string-dark" - -// Card Header -className="text-lg font-medium text-string-dark" -``` - -### Interactive Elements -- **Hover states**: Use `hover:border-string-mint` for subtle interactions -- **Active states**: Use `border-string-mint text-string-mint` for selection -- **Transitions**: Always include `transition-colors` or `transition-all duration-200` - -## Layout Standards - -### Spacing -- **Container max-width**: `max-w-4xl` or `max-w-7xl` -- **Section spacing**: `space-y-6` or `space-y-8` -- **Card padding**: `p-6` or `p-8` -- **Button padding**: `px-4 py-2` or `px-6 py-3` - -### Border Radius -- **Cards**: `rounded-xl` -- **Buttons**: `rounded-xl` -- **Small elements**: `rounded-lg` -- **Avatars**: `rounded-2xl` - -### Typography -- **Display**: `text-3xl font-bold` -- **Heading**: `text-xl font-semibold` -- **Subheading**: `text-lg font-medium` -- **Body**: `text-sm` or `text-base` -- **Caption**: `text-xs` - -## Theme Support -Components should support both light and dark themes using the `t()` helper function: - -```tsx -const t = (light: string, dark: string) => isDark ? dark : light; - -// Usage -className={`${t('bg-white', 'bg-string-dark')} ${t('text-string-dark', 'text-white')}`} -``` \ No newline at end of file