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 @@ @@ -4,9 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>
<body>
<body class="font-sans antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

703
frontend/package-lock.json generated

@ -19,6 +19,26 @@ @@ -19,6 +19,26 @@
"react-router-dom": "^6.21.1",
"typescript": "^5.3.3",
"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": {
@ -998,6 +1018,277 @@ @@ -998,6 +1018,277 @@
"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": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -1132,6 +1423,44 @@ @@ -1132,6 +1423,44 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"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": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
@ -1268,6 +1597,16 @@ @@ -1268,6 +1597,16 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1288,6 +1627,20 @@ @@ -1288,6 +1627,20 @@
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1416,6 +1769,20 @@ @@ -1416,6 +1769,20 @@
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1497,6 +1864,13 @@ @@ -1497,6 +1864,13 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1536,6 +1910,16 @@ @@ -1536,6 +1910,16 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1566,6 +1950,267 @@ @@ -1566,6 +1950,267 @@
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -1587,6 +2232,16 @@ @@ -1587,6 +2232,16 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1647,6 +2302,16 @@ @@ -1647,6 +2302,16 @@
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1681,6 +2346,13 @@ @@ -1681,6 +2346,13 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -1821,6 +2493,37 @@ @@ -1821,6 +2493,37 @@
"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": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

19
frontend/package.json

@ -9,18 +9,23 @@ @@ -9,18 +9,23 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"axios": "^1.6.0",
"@types/node": "^20.10.5",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/react-router-dom": "^5.3.3",
"@types/node": "^20.10.5",
"@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",
"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 @@ @@ -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 @@ @@ -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 {
min-height: 100vh;
display: flex;
@ -62,19 +22,19 @@ body { @@ -62,19 +22,19 @@ body {
.error-container h1 {
font-size: 24px;
margin-bottom: 16px;
color: var(--color-primary-dark);
color: var(--primary);
}
.error-container p {
font-size: 14px;
color: var(--color-muted);
color: var(--muted-foreground);
margin-bottom: 24px;
}
.error-container button {
padding: 10px 20px;
background: var(--color-primary);
color: white;
background: var(--primary);
color: var(--primary-foreground);
border: none;
border-radius: 999px;
font-size: 14px;

3
frontend/src/App.tsx

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

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

@ -0,0 +1,60 @@ @@ -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'; @@ -3,7 +3,6 @@ import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import { useChannels } from '../../hooks/useChannels';
import { APPS } from '../../config/apps';
import './Navbar.css';
export function Navbar() {
const { isAuthenticated, logout } = useAuth();
@ -68,58 +67,82 @@ export function Navbar() { @@ -68,58 +67,82 @@ export function Navbar() {
(searchParams.get('sort') && searchParams.get('sort') !== 'newest');
return (
<nav className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo">
<span className="logo-text">Rainbows, Cupcakes and Unicorns</span>
<>
<header className="bg-white border-b-4 border-primary sticky top-0 z-50">
<div className="max-w-5xl mx-auto px-4 py-5">
<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>
<div className="navbar-menu">
<Link to="/" className={`navbar-link ${location.pathname === '/' ? 'active' : ''}`}>
<div className="flex items-center gap-3">
<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
</Link>
{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
</Link>
)}
{isAuthenticated ? (
<div className="navbar-user">
<button onClick={handleLogout} className="navbar-button">
<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
</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
</Link>
)}
</div>
</div>
</div>
</header>
{isVideoApp && (
<div className="navbar-filters">
<div className="navbar-filters-container">
<form onSubmit={handleSearchSubmit} className="navbar-search-form">
<div className="bg-muted border-b border-border">
<div className="max-w-5xl mx-auto px-4 py-4">
<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
type="text"
placeholder="Search videos..."
value={searchInput}
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>
</form>
<div className="navbar-filter-controls">
<div className="flex gap-2 flex-wrap sm:flex-nowrap">
<select
value={searchParams.get('sort') || 'newest'}
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="oldest">Oldest</option>
@ -129,7 +152,7 @@ export function Navbar() { @@ -129,7 +152,7 @@ export function Navbar() {
<select
value={searchParams.get('channel') || ''}
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>
{channels.map(channel => (
@ -142,7 +165,7 @@ export function Navbar() { @@ -142,7 +165,7 @@ export function Navbar() {
{hasFilters && (
<button
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
</button>
@ -150,7 +173,8 @@ export function Navbar() { @@ -150,7 +173,8 @@ export function Navbar() {
</div>
</div>
</div>
</div>
)}
</nav>
</>
);
}

125
frontend/src/globals.css

@ -0,0 +1,125 @@ @@ -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 @@ @@ -1,26 +1,57 @@
import { Link } from 'react-router-dom';
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() {
return (
<div className="landing-page menu">
<section className="app-grid">
{APPS.map(app => (
<article key={app.id} className={`app-card ${app.disabled ? 'disabled' : ''}`}>
<header>
<h2>{app.name}</h2>
</header>
<p>{app.description}</p>
<div className="app-actions">
{app.disabled ? (
<button disabled>{app.cta}</button>
) : (
<Link to={app.link}>{app.cta}</Link>
)}
<div className="bg-background">
<section className="px-4 py-8">
<div className="max-w-5xl mx-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{APPS.map(app => {
const color = categoryColors[app.id] || 'pink';
const emoji = categoryEmojis[app.id] || '🎮';
return (
<Link
key={app.id}
to={app.disabled ? '#' : app.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>
</article>
))}
</section>
</div>
);

39
frontend/src/pages/LoginPage.tsx

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

147
frontend/src/pages/SpeechSoundsApp.css

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

Loading…
Cancel
Save