Browse Source

new design

drawing-pad
Stephanie Gredell 1 month ago
parent
commit
c18aef8492
  1. 4
      frontend/index.html
  2. 703
      frontend/package-lock.json
  3. 19
      frontend/package.json
  4. 8
      frontend/postcss.config.mjs
  5. 48
      frontend/src/App.css
  6. 3
      frontend/src/App.tsx
  7. 60
      frontend/src/components/Footer/Footer.tsx
  8. 68
      frontend/src/components/Navbar/Navbar.tsx
  9. 125
      frontend/src/globals.css
  10. 65
      frontend/src/pages/LandingPage.tsx
  11. 39
      frontend/src/pages/LoginPage.tsx
  12. 147
      frontend/src/pages/SpeechSoundsApp.css

4
frontend/index.html

@ -4,9 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kiddos - YouTube Channel Aggregator</title> <title>Rainbow, Cupcakes & Unicorns - Free Games for Kids</title>
</head> </head>
<body> <body class="font-sans antialiased">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

703
frontend/package-lock.json generated

@ -19,6 +19,26 @@
"react-router-dom": "^6.21.1", "react-router-dom": "^6.21.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.8" "vite": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -998,6 +1018,277 @@
"win32" "win32"
] ]
}, },
"node_modules/@tailwindcss/node": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.17"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-x64": "4.1.17",
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.6.0",
"@emnapi/runtime": "^1.6.0",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.17",
"@tailwindcss/oxide": "4.1.17",
"postcss": "^8.4.41",
"tailwindcss": "4.1.17"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -1132,6 +1423,44 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/autoprefixer": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
"integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.27.0",
"caniuse-lite": "^1.0.30001754",
"fraction.js": "^5.3.4",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
@ -1268,6 +1597,16 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1288,6 +1627,20 @@
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1416,6 +1769,20 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1497,6 +1864,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1536,6 +1910,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1566,6 +1950,267 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"devOptional": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -1587,6 +2232,16 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1647,6 +2302,16 @@
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1681,6 +2346,13 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -1821,6 +2493,37 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
"integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

19
frontend/package.json

@ -9,18 +9,23 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"react": "^18.2.0", "@types/node": "^20.10.5",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"axios": "^1.6.0",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/node": "^20.10.5",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"axios": "^1.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.8" "vite": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0"
} }
} }

8
frontend/postcss.config.mjs

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config

48
frontend/src/App.css

@ -1,43 +1,3 @@
:root {
/* Playful, gender-neutral color palette */
--color-bg: #f0f4ff;
--color-surface: #ffffff;
--color-surface-alt: #fff8e1;
--color-surface-muted: #e8f5e9;
--color-primary: #7c3aed;
--color-primary-dark: #6d28d9;
--color-secondary: #f59e0b;
--color-secondary-dark: #d97706;
--color-accent: #10b981;
--color-accent-alt: #ef4444;
--color-muted: #6b7280;
--color-text: #1f2937;
--color-border: #e5e7eb;
--color-shadow: rgba(124, 58, 237, 0.15);
/* Subtle gradients for text effects only */
--gradient-primary-text: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
--gradient-secondary-text: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
/* Playful fonts */
--font-playful: 'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', 'Nunito', sans-serif;
--font-body: 'Nunito', 'Roboto', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--color-bg);
color: var(--color-text);
}
.app { .app {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@ -62,19 +22,19 @@ body {
.error-container h1 { .error-container h1 {
font-size: 24px; font-size: 24px;
margin-bottom: 16px; margin-bottom: 16px;
color: var(--color-primary-dark); color: var(--primary);
} }
.error-container p { .error-container p {
font-size: 14px; font-size: 14px;
color: var(--color-muted); color: var(--muted-foreground);
margin-bottom: 24px; margin-bottom: 24px;
} }
.error-container button { .error-container button {
padding: 10px 20px; padding: 10px 20px;
background: var(--color-primary); background: var(--primary);
color: white; color: var(--primary-foreground);
border: none; border: none;
border-radius: 999px; border-radius: 999px;
font-size: 14px; font-size: 14px;

3
frontend/src/App.tsx

@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './hooks/useAuth'; import { AuthProvider } from './hooks/useAuth';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
import { Navbar } from './components/Navbar/Navbar'; import { Navbar } from './components/Navbar/Navbar';
import { Footer } from './components/Footer/Footer';
import { ProtectedRoute } from './components/ProtectedRoute'; import { ProtectedRoute } from './components/ProtectedRoute';
import { LandingPage } from './pages/LandingPage'; import { LandingPage } from './pages/LandingPage';
import { AdminPage } from './pages/AdminPage'; import { AdminPage } from './pages/AdminPage';
@ -9,6 +10,7 @@ import { VideosAdminPage } from './pages/VideosAdminPage';
import { SpeechSoundsAdminPage } from './pages/SpeechSoundsAdminPage'; import { SpeechSoundsAdminPage } from './pages/SpeechSoundsAdminPage';
import { LoginPage } from './pages/LoginPage'; import { LoginPage } from './pages/LoginPage';
import { APPS } from './config/apps'; import { APPS } from './config/apps';
import './globals.css';
import './App.css'; import './App.css';
function App() { function App() {
@ -57,6 +59,7 @@ function App() {
/> />
</Routes> </Routes>
</main> </main>
<Footer />
</div> </div>
</AuthProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>

60
frontend/src/components/Footer/Footer.tsx

@ -0,0 +1,60 @@
export function Footer() {
return (
<footer className="bg-muted border-t border-border mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 mb-8">
<div>
<h3 className="font-bold text-foreground mb-4">Rainbow, Cupcakes and Unicorns</h3>
<p className="text-sm text-muted-foreground">
Making education and fun accessible for every child, completely free.
</p>
</div>
<div>
<h3 className="font-bold text-foreground mb-4">Quick Links</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
<a href="/" className="hover:text-foreground transition-colors">
All Games
</a>
</li>
<li>
<a href="/" className="hover:text-foreground transition-colors">
Categories
</a>
</li>
<li>
<a href="/" className="hover:text-foreground transition-colors">
Top Rated
</a>
</li>
</ul>
</div>
<div>
<h3 className="font-bold text-foreground mb-4">Legal</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
<a href="#" className="hover:text-foreground transition-colors">
Privacy Policy
</a>
</li>
<li>
<a href="#" className="hover:text-foreground transition-colors">
Terms of Service
</a>
</li>
<li>
<a href="#" className="hover:text-foreground transition-colors">
Contact Us
</a>
</li>
</ul>
</div>
</div>
<div className="border-t border-border pt-8 text-center text-sm text-muted-foreground">
<p>© 2025 PlayLearn. Free learning for all children. No ads, no logins, no worries! 🎓</p>
</div>
</div>
</footer>
);
}

68
frontend/src/components/Navbar/Navbar.tsx

@ -3,7 +3,6 @@ import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { useChannels } from '../../hooks/useChannels'; import { useChannels } from '../../hooks/useChannels';
import { APPS } from '../../config/apps'; import { APPS } from '../../config/apps';
import './Navbar.css';
export function Navbar() { export function Navbar() {
const { isAuthenticated, logout } = useAuth(); const { isAuthenticated, logout } = useAuth();
@ -68,58 +67,82 @@ export function Navbar() {
(searchParams.get('sort') && searchParams.get('sort') !== 'newest'); (searchParams.get('sort') && searchParams.get('sort') !== 'newest');
return ( return (
<nav className="navbar"> <>
<div className="navbar-container"> <header className="bg-white border-b-4 border-primary sticky top-0 z-50">
<Link to="/" className="navbar-logo"> <div className="max-w-5xl mx-auto px-4 py-5">
<span className="logo-text">Rainbows, Cupcakes and Unicorns</span> <div className="flex items-center gap-3 justify-between">
<Link to="/" className="flex items-center gap-3">
<span className="text-4xl">🌈</span>
<h1 className="text-3xl md:text-4xl font-bold text-foreground">Rainbow, Cupcakes & Unicorns</h1>
<span className="text-4xl">🧁</span>
</Link> </Link>
<div className="navbar-menu"> <div className="flex items-center gap-3">
<Link to="/" className={`navbar-link ${location.pathname === '/' ? 'active' : ''}`}> <Link
to="/"
className={`text-sm font-semibold px-3 py-2 rounded-full transition-all active:scale-95 ${
location.pathname === '/'
? 'bg-primary text-primary-foreground shadow-md'
: 'bg-white text-foreground border-2 border-primary hover:bg-pink-50'
}`}
>
Home Home
</Link> </Link>
{isAuthenticated && ( {isAuthenticated && (
<Link to="/admin" className="navbar-link"> <Link
to="/admin"
className="text-sm font-semibold px-3 py-2 rounded-full bg-white text-foreground border-2 border-primary hover:bg-pink-50 transition-all active:scale-95"
>
Admin Admin
</Link> </Link>
)} )}
{isAuthenticated ? ( {isAuthenticated ? (
<div className="navbar-user"> <button
<button onClick={handleLogout} className="navbar-button"> onClick={handleLogout}
className="px-4 py-2 bg-primary text-primary-foreground rounded-full font-semibold text-sm hover:bg-primary/90 transition-all active:scale-95 shadow-md"
>
Logout Logout
</button> </button>
</div>
) : ( ) : (
<Link to="/login" className="navbar-button"> <Link
to="/login"
className="px-4 py-2 bg-primary text-primary-foreground rounded-full font-semibold text-sm hover:bg-primary/90 transition-all active:scale-95 shadow-md"
>
Login Login
</Link> </Link>
)} )}
</div> </div>
</div> </div>
</div>
</header>
{isVideoApp && ( {isVideoApp && (
<div className="navbar-filters"> <div className="bg-muted border-b border-border">
<div className="navbar-filters-container"> <div className="max-w-5xl mx-auto px-4 py-4">
<form onSubmit={handleSearchSubmit} className="navbar-search-form"> <div className="flex flex-col sm:flex-row gap-4 items-stretch sm:items-center">
<form onSubmit={handleSearchSubmit} className="flex gap-2 flex-1 max-w-md">
<input <input
type="text" type="text"
placeholder="Search videos..." placeholder="Search videos..."
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
className="navbar-search-input" className="flex-1 px-4 py-2 border border-border rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<button type="submit" className="navbar-search-button"> <button
type="submit"
className="px-4 py-2 bg-primary text-primary-foreground rounded-full hover:bg-primary/90 transition-colors text-sm font-semibold"
>
🔍 🔍
</button> </button>
</form> </form>
<div className="navbar-filter-controls"> <div className="flex gap-2 flex-wrap sm:flex-nowrap">
<select <select
value={searchParams.get('sort') || 'newest'} value={searchParams.get('sort') || 'newest'}
onChange={handleSortChange} onChange={handleSortChange}
className="navbar-filter-select" className="px-4 py-2 border border-border rounded-full bg-white text-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
> >
<option value="newest">Newest</option> <option value="newest">Newest</option>
<option value="oldest">Oldest</option> <option value="oldest">Oldest</option>
@ -129,7 +152,7 @@ export function Navbar() {
<select <select
value={searchParams.get('channel') || ''} value={searchParams.get('channel') || ''}
onChange={handleChannelChange} onChange={handleChannelChange}
className="navbar-filter-select" className="px-4 py-2 border border-border rounded-full bg-white text-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
> >
<option value="">All Channels</option> <option value="">All Channels</option>
{channels.map(channel => ( {channels.map(channel => (
@ -142,7 +165,7 @@ export function Navbar() {
{hasFilters && ( {hasFilters && (
<button <button
onClick={handleClearFilters} onClick={handleClearFilters}
className="navbar-clear-button" className="px-4 py-2 border border-border rounded-full bg-white text-sm hover:bg-muted transition-colors whitespace-nowrap"
> >
Clear Filters Clear Filters
</button> </button>
@ -150,7 +173,8 @@ export function Navbar() {
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
</nav> </>
); );
} }

125
frontend/src/globals.css

@ -0,0 +1,125 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
/* Clean, bright colors for kids */
--background: #fef8f3;
--foreground: #2d2d2d;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #ff6b9d;
--primary-foreground: #fff;
--secondary: #ffa500;
--secondary-foreground: #fff;
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: #7c3aed;
--accent-foreground: #fff;
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: #e0e0e0;
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 1.5rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: #1a1a1a;
--foreground: #f5f5f5;
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: #ff6b9d;
--primary-foreground: #fff;
--secondary: #ffa500;
--secondary-foreground: #fff;
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: #a78bfa;
--accent-foreground: #1a1a1a;
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: #e0e0e0;
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

65
frontend/src/pages/LandingPage.tsx

@ -1,26 +1,57 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { APPS } from '../config/apps'; import { APPS } from '../config/apps';
import './LandingPage.css';
const categoryEmojis: { [key: string]: string } = {
videos: '📺',
speechsounds: '🗣',
all: '🎮',
};
const categoryColors: { [key: string]: string } = {
videos: 'pink',
speechsounds: 'purple',
};
const colorMap: { [key: string]: string } = {
pink: 'bg-pink-100 hover:bg-pink-200',
purple: 'bg-purple-100 hover:bg-purple-200',
blue: 'bg-blue-100 hover:bg-blue-200',
green: 'bg-green-100 hover:bg-green-200',
indigo: 'bg-indigo-100 hover:bg-indigo-200',
amber: 'bg-amber-100 hover:bg-amber-200',
};
export function LandingPage() { export function LandingPage() {
return ( return (
<div className="landing-page menu"> <div className="bg-background">
<section className="app-grid"> <section className="px-4 py-8">
{APPS.map(app => ( <div className="max-w-5xl mx-auto">
<article key={app.id} className={`app-card ${app.disabled ? 'disabled' : ''}`}> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
<header> {APPS.map(app => {
<h2>{app.name}</h2> const color = categoryColors[app.id] || 'pink';
</header> const emoji = categoryEmojis[app.id] || '🎮';
<p>{app.description}</p>
<div className="app-actions"> return (
{app.disabled ? ( <Link
<button disabled>{app.cta}</button> key={app.id}
) : ( to={app.disabled ? '#' : app.link}
<Link to={app.link}>{app.cta}</Link> className={`${colorMap[color]} w-full p-6 rounded-3xl font-semibold text-foreground transition-all active:scale-95 hover:shadow-lg flex flex-col items-center text-center ${
)} app.disabled ? 'opacity-50 cursor-not-allowed' : ''
}`}
onClick={(e) => {
if (app.disabled) {
e.preventDefault();
}
}}
>
<div className="mb-3 text-5xl">{emoji}</div>
<h3 className="text-xl font-bold mb-1">{app.name}</h3>
<p className="text-sm opacity-75">{app.description}</p>
</Link>
);
})}
</div>
</div> </div>
</article>
))}
</section> </section>
</div> </div>
); );

39
frontend/src/pages/LoginPage.tsx

@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth'; import { useAuth } from '../hooks/useAuth';
import './LoginPage.css';
export function LoginPage() { export function LoginPage() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -28,18 +27,24 @@ export function LoginPage() {
}; };
return ( return (
<div className="login-page"> <div className="min-h-screen flex items-center justify-center bg-background px-4 py-8">
<div className="login-container"> <div className="w-full max-w-md bg-card rounded-3xl shadow-lg overflow-hidden border border-border">
<div className="login-header"> <div className="px-8 pt-8 pb-6 text-center border-b border-border">
<h1>Admin Login</h1> <h1 className="text-2xl font-bold text-foreground mb-2">Admin Login</h1>
<p>Sign in to manage channels</p> <p className="text-sm text-muted-foreground">Sign in to manage channels</p>
</div> </div>
<form onSubmit={handleSubmit} className="login-form"> <form onSubmit={handleSubmit} className="px-8 py-8">
{error && <div className="login-error">{error}</div>} {error && (
<div className="mb-6 p-3 bg-destructive/10 text-destructive border border-destructive/20 rounded-xl text-sm">
{error}
</div>
)}
<div className="form-group"> <div className="mb-5">
<label htmlFor="username">Username</label> <label htmlFor="username" className="block mb-2 text-sm font-semibold text-foreground">
Username
</label>
<input <input
id="username" id="username"
type="text" type="text"
@ -48,11 +53,14 @@ export function LoginPage() {
disabled={loading} disabled={loading}
required required
autoFocus autoFocus
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50"
/> />
</div> </div>
<div className="form-group"> <div className="mb-6">
<label htmlFor="password">Password</label> <label htmlFor="password" className="block mb-2 text-sm font-semibold text-foreground">
Password
</label>
<input <input
id="password" id="password"
type="password" type="password"
@ -60,10 +68,15 @@ export function LoginPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
disabled={loading} disabled={loading}
required required
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50"
/> />
</div> </div>
<button type="submit" disabled={loading} className="login-button"> <button
type="submit"
disabled={loading}
className="w-full px-4 py-3 bg-primary text-primary-foreground rounded-xl font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-md"
>
{loading ? 'Signing in...' : 'Sign In'} {loading ? 'Signing in...' : 'Sign In'}
</button> </button>
</form> </form>

147
frontend/src/pages/SpeechSoundsApp.css

@ -1,6 +1,6 @@
.speech-sounds-app { .speech-sounds-app {
min-height: calc(100vh - 60px); min-height: calc(100vh - 60px);
background: var(--color-bg); background: var(--background);
padding: 24px; padding: 24px;
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
@ -15,17 +15,17 @@
margin: 0 0 12px 0; margin: 0 0 12px 0;
font-size: 42px; font-size: 42px;
font-weight: 800; font-weight: 800;
background: var(--gradient-primary-text); color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
font-family: var(--font-playful);
} }
.app-header p { .app-header p {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
color: var(--color-text); color: var(--foreground);
font-weight: 600; font-weight: 600;
opacity: 0.8; opacity: 0.8;
} }
@ -33,23 +33,22 @@
.back-to-groups-button { .back-to-groups-button {
margin-bottom: 16px; margin-bottom: 16px;
padding: 12px 24px; padding: 12px 24px;
background: var(--color-surface); background: var(--card);
border: 3px solid var(--color-primary); border: 3px solid var(--primary);
border-radius: 20px; border-radius: 20px;
color: var(--color-primary); color: var(--primary);
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
box-shadow: 0 4px 8px var(--color-shadow); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
font-family: var(--font-playful);
} }
.back-to-groups-button:hover { .back-to-groups-button:hover {
background: var(--color-primary); background: var(--primary);
color: white; color: var(--primary-foreground);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(124, 58, 237, 0.3); box-shadow: 0 6px 12px rgba(255, 107, 157, 0.3);
} }
.groups-grid { .groups-grid {
@ -60,16 +59,16 @@
} }
.group-card { .group-card {
background: var(--color-surface); background: var(--card);
border: 4px solid var(--color-primary); border: 4px solid var(--primary);
border-radius: 24px; border-radius: 24px;
padding: 32px 24px; padding: 32px 24px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
text-decoration: none; text-decoration: none;
color: var(--color-text); color: var(--foreground);
box-shadow: 0 8px 16px var(--color-shadow); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@ -83,7 +82,7 @@
height: 200%; height: 200%;
background: radial-gradient( background: radial-gradient(
circle, circle,
rgba(124, 58, 237, 0.1) 0%, rgba(255, 107, 157, 0.1) 0%,
transparent 70% transparent 70%
); );
opacity: 0; opacity: 0;
@ -92,8 +91,8 @@
.group-card:hover { .group-card:hover {
transform: translateY(-8px) scale(1.05); transform: translateY(-8px) scale(1.05);
box-shadow: 0 12px 32px rgba(124, 58, 237, 0.3); box-shadow: 0 12px 32px rgba(255, 107, 157, 0.3);
border-color: var(--color-secondary); border-color: var(--secondary);
} }
.group-card:hover::before { .group-card:hover::before {
@ -104,11 +103,11 @@
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: 28px; font-size: 28px;
font-weight: 800; font-weight: 800;
background: var(--gradient-primary-text); color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
font-family: var(--font-playful);
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
@ -116,18 +115,18 @@
.group-card-count { .group-card-count {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
color: var(--color-muted); color: var(--muted-foreground);
font-weight: 700; font-weight: 700;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.practice-area { .practice-area {
background: var(--color-surface); background: var(--card);
border-radius: 32px; border-radius: 32px;
padding: 40px; padding: 40px;
box-shadow: 0 8px 32px var(--color-shadow); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 4px solid var(--color-primary); border: 4px solid var(--primary);
} }
.word-display { .word-display {
@ -138,13 +137,13 @@
.word-text { .word-text {
font-size: 72px; font-size: 72px;
font-weight: 900; font-weight: 900;
background: var(--gradient-primary-text); color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
margin: 0 0 20px 0; margin: 0 0 20px 0;
letter-spacing: 4px; letter-spacing: 4px;
font-family: var(--font-playful);
animation: wordBounce 0.5s ease-out; animation: wordBounce 0.5s ease-out;
} }
@ -171,27 +170,26 @@
font-weight: 700; font-weight: 700;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 25px;
box-shadow: 0 4px 8px var(--color-shadow); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border: 3px solid; border: 3px solid;
font-family: var(--font-playful);
} }
.stat-pass { .stat-pass {
background: var(--color-accent); background: #10b981;
border-color: var(--color-accent); border-color: #10b981;
color: white; color: white;
} }
.stat-fail { .stat-fail {
background: var(--color-accent-alt); background: #ef4444;
border-color: var(--color-accent-alt); border-color: #ef4444;
color: white; color: white;
} }
.stat-total { .stat-total {
background: var(--color-secondary); background: var(--secondary);
border-color: var(--color-secondary); border-color: var(--secondary);
color: white; color: var(--secondary-foreground);
} }
.practice-container { .practice-container {
@ -202,9 +200,8 @@
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: var(--color-text); color: var(--foreground);
margin-bottom: 24px; margin-bottom: 24px;
font-family: var(--font-playful);
} }
.practice-grid { .practice-grid {
@ -220,30 +217,30 @@
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 20px 16px; padding: 20px 16px;
border: 3px solid var(--color-border); border: 3px solid var(--border);
border-radius: 20px; border-radius: 20px;
background: var(--color-surface); background: var(--card);
transition: all 0.3s; transition: all 0.3s;
} }
.practice-item:hover { .practice-item:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 8px 16px var(--color-shadow); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-color: var(--color-primary); border-color: var(--primary);
} }
.practice-number { .practice-number {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: white; color: var(--primary-foreground);
width: 40px; width: 40px;
height: 40px; height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; border-radius: 50%;
background: var(--color-primary); background: var(--primary);
box-shadow: 0 4px 8px var(--color-shadow); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
} }
.practice-buttons { .practice-buttons {
@ -277,16 +274,16 @@
} }
.pass-button:hover { .pass-button:hover {
background: var(--color-accent); background: #10b981;
border-color: var(--color-accent); border-color: #10b981;
color: white; color: white;
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.3); box-shadow: 0 6px 12px rgba(16, 185, 129, 0.3);
} }
.pass-button.active { .pass-button.active {
background: var(--color-accent); background: #10b981;
color: white; color: white;
border-color: var(--color-accent); border-color: #10b981;
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4); box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
} }
@ -297,16 +294,16 @@
} }
.fail-button:hover { .fail-button:hover {
background: var(--color-accent-alt); background: #ef4444;
border-color: var(--color-accent-alt); border-color: #ef4444;
color: white; color: white;
box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3); box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3);
} }
.fail-button.active { .fail-button.active {
background: var(--color-accent-alt); background: #ef4444;
color: white; color: white;
border-color: var(--color-accent-alt); border-color: #ef4444;
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4); box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
} }
@ -322,22 +319,21 @@
.nav-button { .nav-button {
padding: 16px 32px; padding: 16px 32px;
background: var(--color-primary); background: var(--primary);
color: white; color: var(--primary-foreground);
border: 3px solid var(--color-primary); border: 3px solid var(--primary);
border-radius: 25px; border-radius: 25px;
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4px 12px var(--color-shadow); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-family: var(--font-playful);
} }
.nav-button:hover:not(:disabled) { .nav-button:hover:not(:disabled) {
transform: translateY(-4px) scale(1.05); transform: translateY(-4px) scale(1.05);
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.4); box-shadow: 0 8px 20px rgba(255, 107, 157, 0.4);
border-color: var(--color-secondary); border-color: var(--secondary);
} }
.nav-button:disabled { .nav-button:disabled {
@ -348,14 +344,13 @@
.word-counter { .word-counter {
font-size: 20px; font-size: 20px;
color: white; color: var(--secondary-foreground);
font-weight: 700; font-weight: 700;
font-family: var(--font-playful);
padding: 12px 24px; padding: 12px 24px;
background: var(--color-secondary); background: var(--secondary);
border-radius: 20px; border-radius: 20px;
border: 3px solid var(--color-secondary); border: 3px solid var(--secondary);
box-shadow: 0 4px 8px rgba(245, 158, 11, 0.3); box-shadow: 0 4px 8px rgba(255, 165, 0, 0.3);
} }
.word-actions { .word-actions {
@ -364,20 +359,19 @@
.reset-button { .reset-button {
padding: 12px 24px; padding: 12px 24px;
background: var(--color-surface); background: var(--card);
color: var(--color-accent-alt); color: #ef4444;
border: 3px solid var(--color-accent-alt); border: 3px solid #ef4444;
border-radius: 20px; border-radius: 20px;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.2); box-shadow: 0 4px 8px rgba(239, 68, 68, 0.2);
font-family: var(--font-playful);
} }
.reset-button:hover { .reset-button:hover {
background: var(--color-accent-alt); background: #ef4444;
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3); box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3);
@ -388,20 +382,19 @@
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 48px 24px; padding: 48px 24px;
background: var(--color-surface); background: var(--card);
border-radius: 24px; border-radius: 24px;
margin-top: 32px; margin-top: 32px;
border: 4px solid var(--color-primary); border: 4px solid var(--primary);
box-shadow: 0 8px 16px var(--color-shadow); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
} }
.empty-state h2 { .empty-state h2 {
margin: 0 0 12px 0; margin: 0 0 12px 0;
font-size: 32px; font-size: 32px;
color: var(--color-text); color: var(--primary);
font-family: var(--font-playful);
font-weight: 800; font-weight: 800;
background: var(--gradient-primary-text); background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@ -409,7 +402,7 @@
.empty-state p { .empty-state p {
margin: 0; margin: 0;
color: var(--color-muted); color: var(--muted-foreground);
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
} }

Loading…
Cancel
Save