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 diff --git a/api/apps.ts b/api/apps.ts index e1ffad1..5c6291d 100644 --- a/api/apps.ts +++ b/api/apps.ts @@ -46,7 +46,7 @@ export default async function handler(_request: Request) { })); // Combine both lists - const allApps = [...dbApps, ...submissionsAsApps]; + const allApps = [...officialApps, ...submissionsAsApps]; // Get today's featured app (if any) const today = new Date().toISOString().split('T')[0]; diff --git a/claude.md b/claude.md index 709d846..4bebd30 100644 --- a/claude.md +++ b/claude.md @@ -981,9 +981,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) diff --git a/data/apps-seed.json b/data/apps-seed.json index ea26953..4236030 100644 --- a/data/apps-seed.json +++ b/data/apps-seed.json @@ -637,8 +637,7 @@ "transcription" ], "is_official": true, - "frequency": 0, - "featured": true + "frequency": 0 }, { "name": "SmartCompose", @@ -706,6 +705,41 @@ "is_official": true, "frequency": 0, "logo_url": "/src/app-icons/langbuddy.png" + }, + { + "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,