├── .gitattributes ├── .firebaserc ├── .prettierrc.json ├── jsconfig.json ├── .vscode └── extensions.json ├── postcss.config.js ├── src ├── firebase │ ├── firebase.json │ ├── firestore.indexes.json │ ├── nodejs │ │ ├── firebase-node.js │ │ └── import-level-data.js │ ├── index.js │ ├── storage.rules │ ├── journeys.js │ ├── analytics-utils.js │ ├── firestore.rules │ ├── storage-utils.js │ └── legal-utils.js ├── views │ ├── AboutView.vue │ ├── learn │ │ ├── CssLearnView.vue │ │ ├── HtmlLearnView.vue │ │ └── JavaScriptLearnView.vue │ ├── TermsOfServiceView.vue │ ├── PrivacyPolicyView.vue │ ├── ActivitiesView.vue │ ├── LoginView.vue │ ├── JourneyDetailView.vue │ ├── LeaderboardView.vue │ ├── LevelDetailView.vue │ ├── JourneysView.vue │ ├── LevelsView.vue │ ├── HomeView.vue │ └── RegisterView.vue ├── main.js ├── components │ ├── icons │ │ ├── IconSupport.vue │ │ ├── IconTooling.vue │ │ ├── IconCommunity.vue │ │ ├── IconDocumentation.vue │ │ └── IconEcosystem.vue │ ├── HelloWorld.vue │ ├── AutoFeedbackPrompt.vue │ ├── HtmlContentViewer.vue │ ├── FloatingFeedbackButton.vue │ ├── WelcomeItem.vue │ ├── TheWelcome.vue │ ├── CodePreview.vue │ ├── AdminBreadcrumbs.vue │ └── ActivityFeed.vue ├── stores │ ├── counter.js │ ├── initStores.js │ ├── feedbackStore.js │ └── userStore.js ├── assets │ ├── text-logo.svg │ ├── main.css │ ├── admin-forms.css │ ├── base.css │ └── logo.svg ├── test-activity.js └── router │ ├── adminGuard.js │ └── index.js ├── .editorconfig ├── deployment ├── bucket-policy.json ├── dns-records.json └── AWS-S3-CF.md ├── .env.example ├── .gitignore ├── index.html ├── eslint.config.js ├── tailwind.config.js ├── vite.config.js ├── package.json ├── public └── logo.svg ├── docs ├── LEVEL.md └── ADMINUI.md └── CREATOR.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "production": "codequest-8d9ac" 4 | }, 5 | "targets": {}, 6 | "etags": {} 7 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from 'tailwindcss'; 2 | import autoprefixer from 'autoprefixer'; 3 | 4 | export default { 5 | plugins: [ 6 | tailwindcss, 7 | autoprefixer, 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /src/firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "storage": { 7 | "rules": "storage.rules" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | end_of_line = lf 9 | max_line_length = 100 10 | -------------------------------------------------------------------------------- /src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(createPinia()) 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | -------------------------------------------------------------------------------- /deployment/bucket-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "PublicReadGetObject", 6 | "Effect": "Allow", 7 | "Principal": "*", 8 | "Action": "s3:GetObject", 9 | "Resource": "arn:aws:s3:::code4u.innoaya.org/*" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Firebase Configuration 2 | VITE_FIREBASE_API_KEY=your-api-key 3 | VITE_FIREBASE_AUTH_DOMAIN=your-auth-domain 4 | VITE_FIREBASE_PROJECT_ID=your-project-id 5 | VITE_FIREBASE_STORAGE_BUCKET=your-storage-bucket 6 | VITE_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id 7 | VITE_FIREBASE_APP_ID=your-app-id 8 | 9 | # Add any other environment variables needed for the project below 10 | -------------------------------------------------------------------------------- /deployment/dns-records.json: -------------------------------------------------------------------------------- 1 | { 2 | "Changes": [ 3 | { 4 | "Action": "UPSERT", 5 | "ResourceRecordSet": { 6 | "Name": "code4u.innoaya.org", 7 | "Type": "A", 8 | "AliasTarget": { 9 | "HostedZoneId": "Z2FDTNDATAQYW2", 10 | "DNSName": "d9m6g9wzp7avr.cloudfront.net.", 11 | "EvaluateTargetHealth": false 12 | } 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | pnpm-debug.log* 10 | lerna-debug.log* 11 | 12 | node_modules 13 | .DS_Store 14 | dist 15 | dist-ssr 16 | coverage 17 | *.local 18 | 19 | /cypress/videos/ 20 | /cypress/screenshots/ 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | *.tsbuildinfo 33 | -------------------------------------------------------------------------------- /src/assets/text-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | Code4U 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Code4U - Learn Coding Through Play 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/learn/CssLearnView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /src/views/learn/HtmlLearnView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /src/firebase/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "user_activities", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "timestamp", 9 | "order": "DESCENDING" 10 | } 11 | ] 12 | }, 13 | { 14 | "collectionGroup": "user_activities", 15 | "queryScope": "COLLECTION", 16 | "fields": [ 17 | { 18 | "fieldPath": "userId", 19 | "order": "ASCENDING" 20 | }, 21 | { 22 | "fieldPath": "timestamp", 23 | "order": "DESCENDING" 24 | } 25 | ] 26 | } 27 | ], 28 | "fieldOverrides": [] 29 | } 30 | -------------------------------------------------------------------------------- /src/views/learn/JavaScriptLearnView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from 'eslint/config' 2 | import globals from 'globals' 3 | import js from '@eslint/js' 4 | import pluginVue from 'eslint-plugin-vue' 5 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' 6 | 7 | export default defineConfig([ 8 | { 9 | name: 'app/files-to-lint', 10 | files: ['**/*.{js,mjs,jsx,vue}'], 11 | }, 12 | 13 | globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), 14 | 15 | { 16 | languageOptions: { 17 | globals: { 18 | ...globals.browser, 19 | }, 20 | }, 21 | }, 22 | 23 | js.configs.recommended, 24 | ...pluginVue.configs['flat/essential'], 25 | skipFormatting, 26 | ]) 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: '#4F46E5', 11 | secondary: '#10B981', 12 | accent: '#F59E0B', 13 | danger: '#EF4444', 14 | success: '#34D399', 15 | info: '#3B82F6', 16 | warning: '#F59E0B', 17 | background: '#F3F4F6', 18 | 'text-primary': '#1F2937', 19 | 'text-secondary': '#6B7280', 20 | }, 21 | fontFamily: { 22 | sans: ['Poppins', 'sans-serif'], 23 | heading: ['Nunito', 'sans-serif'], 24 | }, 25 | }, 26 | }, 27 | plugins: [], 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Custom styles */ 6 | @layer base { 7 | body { 8 | @apply bg-background text-text-primary; 9 | font-family: 'Roboto Mono', monospace; 10 | } 11 | } 12 | 13 | @layer components { 14 | .btn { 15 | @apply px-4 py-2 rounded-lg font-medium transition-all duration-200; 16 | } 17 | 18 | .btn-primary { 19 | @apply bg-primary text-white hover:bg-primary/90; 20 | } 21 | 22 | .btn-secondary { 23 | @apply bg-secondary text-white hover:bg-secondary/90; 24 | } 25 | 26 | .card { 27 | @apply bg-white rounded-xl shadow-md p-6; 28 | } 29 | 30 | .input { 31 | @apply w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/stores/initStores.js: -------------------------------------------------------------------------------- 1 | // Store initialization helper 2 | import { useJourneyStore } from './journeyStore'; 3 | import { useUserStore } from './userStore'; 4 | 5 | /** 6 | * Pre-load critical data for the application 7 | * This should be called early in the app lifecycle 8 | */ 9 | export async function initializeStores() { 10 | // Initialize the journey store data 11 | const journeyStore = useJourneyStore(); 12 | 13 | // Initialize the user store - initialization happens in the store itself 14 | useUserStore(); 15 | 16 | try { 17 | // Pre-fetch journeys 18 | await journeyStore.fetchAllJourneys(); 19 | console.log('Journeys pre-loaded successfully'); 20 | } catch (error) { 21 | console.error('Error pre-loading journeys:', error); 22 | } 23 | 24 | // Add other store initializations here as needed 25 | } 26 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 45 | -------------------------------------------------------------------------------- /src/components/AutoFeedbackPrompt.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /src/firebase/nodejs/firebase-node.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { initializeApp } from 'firebase/app'; 3 | import { getAuth } from 'firebase/auth'; 4 | import { getFirestore } from 'firebase/firestore'; 5 | import dotenv from 'dotenv'; 6 | 7 | // Load environment variables from .env file for Node.js environment 8 | dotenv.config(); 9 | 10 | // Your web app's Firebase configuration using Node.js environment variables 11 | const firebaseConfig = { 12 | apiKey: process.env.VITE_FIREBASE_API_KEY, 13 | authDomain: process.env.VITE_FIREBASE_AUTH_DOMAIN, 14 | projectId: process.env.VITE_FIREBASE_PROJECT_ID, 15 | storageBucket: process.env.VITE_FIREBASE_STORAGE_BUCKET, 16 | messagingSenderId: process.env.VITE_FIREBASE_MESSAGING_SENDER_ID, 17 | appId: process.env.VITE_FIREBASE_APP_ID 18 | }; 19 | 20 | // Initialize Firebase 21 | const app = initializeApp(firebaseConfig); 22 | const auth = getAuth(app); 23 | const db = getFirestore(app); 24 | 25 | export { auth, db }; 26 | -------------------------------------------------------------------------------- /src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /src/components/HtmlContentViewer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 41 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueDevTools from 'vite-plugin-vue-devtools' 6 | import terser from '@rollup/plugin-terser' 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig(({ command }) => ({ 10 | plugins: [ 11 | vue(), 12 | // Only enable devTools in development mode 13 | command === 'serve' ? vueDevTools() : null, 14 | // Use terser for production builds 15 | command === 'build' ? terser() : null, 16 | ].filter(Boolean), 17 | resolve: { 18 | alias: { 19 | '@': fileURLToPath(new URL('./src', import.meta.url)) 20 | }, 21 | }, 22 | // Production build optimizations 23 | build: { 24 | minify: 'terser', 25 | terserOptions: { 26 | compress: { 27 | // Remove console.log in production 28 | drop_console: true, 29 | drop_debugger: true, 30 | pure_funcs: ['console.log', 'console.info', 'console.debug'] 31 | } 32 | } 33 | }, 34 | })) 35 | -------------------------------------------------------------------------------- /src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code4u", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "deploy": "npm run build && npm run sync-to-s3 && npm run invalidate-cloudfront", 9 | "build": "vite build", 10 | "sync-to-s3": "aws s3 sync dist/ s3://code4u.innoaya.org/ --delete", 11 | "invalidate-cloudfront": "aws cloudfront create-invalidation --distribution-id E1CT62X5SD8JRU --paths /*", 12 | "preview": "vite preview", 13 | "lint": "eslint . --fix", 14 | "format": "prettier --write src/" 15 | }, 16 | "dependencies": { 17 | "dotenv": "^16.5.0", 18 | "firebase": "^11.6.1", 19 | "pinia": "^3.0.1", 20 | "vue": "^3.5.13", 21 | "vue-router": "^4.5.0" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.22.0", 25 | "@rollup/plugin-terser": "^0.4.4", 26 | "@vitejs/plugin-vue": "^5.2.3", 27 | "@vue/eslint-config-prettier": "^10.2.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^9.22.0", 30 | "eslint-plugin-vue": "~10.0.0", 31 | "globals": "^16.0.0", 32 | "postcss": "^8.4.23", 33 | "prettier": "3.5.3", 34 | "tailwindcss": "^3.3.0", 35 | "vite": "^6.2.4", 36 | "vite-plugin-vue-devtools": "^7.7.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/assets/admin-forms.css: -------------------------------------------------------------------------------- 1 | /* Admin Form Styles - Consistent form elements */ 2 | 3 | .admin-form-control { 4 | @apply py-2 px-3 border border-gray-300 shadow-sm focus:ring-2 focus:ring-offset-0 block w-full sm:text-sm rounded-md; 5 | } 6 | 7 | .admin-form-control:focus { 8 | @apply outline-none border-blue-400; 9 | } 10 | 11 | .admin-form-group { 12 | @apply mb-4; 13 | } 14 | 15 | .admin-form-label { 16 | @apply block text-sm font-medium text-gray-700 mb-1; 17 | } 18 | 19 | .admin-form-input-wrapper { 20 | @apply mt-1 relative rounded-md shadow-sm; 21 | } 22 | 23 | .admin-form-help { 24 | @apply mt-1 text-sm text-gray-500; 25 | } 26 | 27 | /* Input type specific styles */ 28 | .admin-form-control[type="text"], 29 | .admin-form-control[type="number"], 30 | .admin-form-control[type="email"] { 31 | @apply focus:ring-blue-500 focus:border-blue-500; 32 | } 33 | 34 | textarea.admin-form-control { 35 | @apply focus:ring-blue-500 focus:border-blue-500; 36 | } 37 | 38 | select.admin-form-control { 39 | @apply focus:ring-blue-500 focus:border-blue-500 bg-white pr-10; 40 | } 41 | 42 | /* Form layout adjustments */ 43 | .admin-grid-form { 44 | @apply grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4; 45 | } 46 | 47 | .admin-grid-form-full { 48 | @apply col-span-1 md:col-span-2; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/FloatingFeedbackButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /src/test-activity.js: -------------------------------------------------------------------------------- 1 | // This is a test script to manually create a user activity 2 | import { auth, db } from './firebase' 3 | import { collection, addDoc, serverTimestamp } from 'firebase/firestore' 4 | 5 | // Function to test adding an activity 6 | async function testAddActivity() { 7 | try { 8 | console.log('Starting test: Adding user activity...') 9 | 10 | if (!auth.currentUser) { 11 | console.error('No user is logged in! Please log in first.') 12 | return 13 | } 14 | 15 | console.log('Current user:', auth.currentUser.uid) 16 | 17 | const activityData = { 18 | userId: auth.currentUser.uid, 19 | type: 'test_activity', 20 | details: { 21 | message: 'This is a test activity', 22 | timestamp: new Date().toISOString() 23 | }, 24 | timestamp: serverTimestamp() 25 | } 26 | 27 | console.log('Attempting to add activity:', activityData) 28 | 29 | const docRef = await addDoc(collection(db, 'user_activities'), activityData) 30 | 31 | console.log('Activity added successfully with ID:', docRef.id) 32 | return docRef.id 33 | } catch (error) { 34 | console.error('Error adding test activity:', error) 35 | throw error 36 | } 37 | } 38 | 39 | // Export for use in Vue components 40 | export { testAddActivity } 41 | -------------------------------------------------------------------------------- /src/firebase/index.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app'; 2 | import { getAuth } from 'firebase/auth'; 3 | import { getFirestore } from 'firebase/firestore'; 4 | import { getStorage } from 'firebase/storage'; 5 | import { getAnalytics, logEvent } from 'firebase/analytics'; 6 | 7 | // Your web app's Firebase configuration using Vite's environment variables 8 | const firebaseConfig = { 9 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 10 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, 11 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, 12 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, 13 | messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, 14 | appId: import.meta.env.VITE_FIREBASE_APP_ID 15 | }; 16 | 17 | // Initialize Firebase 18 | const app = initializeApp(firebaseConfig); 19 | const auth = getAuth(app); 20 | const db = getFirestore(app); 21 | const storage = getStorage(app); 22 | const analytics = getAnalytics(app); 23 | 24 | // Export a function to log analytics events 25 | const logAnalyticsEvent = (eventName, eventParams) => { 26 | try { 27 | logEvent(analytics, eventName, eventParams); 28 | console.debug(`Analytics event logged: ${eventName}`, eventParams); 29 | } catch (error) { 30 | console.error('Error logging analytics event:', error); 31 | } 32 | }; 33 | 34 | export { auth, db, storage, analytics, logAnalyticsEvent }; 35 | -------------------------------------------------------------------------------- /src/stores/feedbackStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useFeedbackStore = defineStore('feedback', () => { 5 | // State 6 | const showFeedbackModal = ref(false) 7 | const feedbackLevel = ref(null) 8 | const feedbackCategory = ref('') 9 | const isAutoPrompt = ref(false) 10 | 11 | // Check if feedback should be requested for this level and category 12 | function shouldRequestFeedback(levelNum, category) { 13 | // Request feedback after completing second level of each category 14 | // We check the level is 2 for any category (HTML, CSS, JavaScript) 15 | return levelNum === 2 && ['HTML', 'CSS', 'JavaScript'].includes(category) 16 | } 17 | 18 | // Show feedback prompt for a level 19 | function showFeedbackFor(levelId, levelNum, category) { 20 | if (shouldRequestFeedback(levelNum, category)) { 21 | feedbackLevel.value = levelId 22 | feedbackCategory.value = category 23 | isAutoPrompt.value = true 24 | showFeedbackModal.value = true 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | // Close feedback modal 31 | function closeFeedback() { 32 | showFeedbackModal.value = false 33 | isAutoPrompt.value = false 34 | } 35 | 36 | // Show feedback manually (from floating button) 37 | function showFeedbackManually() { 38 | isAutoPrompt.value = false 39 | showFeedbackModal.value = true 40 | } 41 | 42 | return { 43 | // State 44 | showFeedbackModal, 45 | feedbackLevel, 46 | feedbackCategory, 47 | isAutoPrompt, 48 | 49 | // Actions 50 | showFeedbackFor, 51 | closeFeedback, 52 | showFeedbackManually 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /deployment/AWS-S3-CF.md: -------------------------------------------------------------------------------- 1 | ## Initial Configuration & Deployment 2 | 1. Build Your Vue 3 App 3 | ```bash 4 | npm run build 5 | ``` 6 | 7 | 2. Create an S3 Bucket for Hosting 8 | ```bash 9 | aws s3api create-bucket --bucket code4u.innoaya.org --create-bucket-configuration LocationConstraint=ap-southeast-1 10 | ``` 11 | 12 | 3. Configure S3 Bucket for Static Website Hosting (Make it publicly readable) 13 | ```bash 14 | aws s3 website s3://code4u.innoaya.org/ --index-document index.html --error-document index.html 15 | 16 | aws s3api put-public-access-block --bucket code4u.innoaya.org --public-access-block-configuration BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false 17 | 18 | cd deployment 19 | 20 | aws s3api put-bucket-policy --bucket code4u.innoaya.org --policy file://bucket-policy.json 21 | 22 | cd .. 23 | ``` 24 | 25 | 4. Deploy Your App (Sync Your Build Folder to S3) 26 | ```bash 27 | aws s3 sync dist/ s3://code4u.innoaya.org/ --delete 28 | ``` 29 | 30 | 5. Create CloudFront Distribution 31 | ```bash 32 | aws cloudfront create-distribution --origin-domain-name code4u.innoaya.org.s3-website-ap-southeast-1.amazonaws.com 33 | ``` 34 | 35 | 6. Update your DNS record to point to the CloudFront distribution domain name. Route53 36 | ```bash 37 | cd deployment 38 | 39 | aws route53 change-resource-record-sets --hosted-zone-id Z0130296EQP34SB7RMM2 --change-batch file://dns-records.json 40 | 41 | cd .. 42 | ``` 43 | 44 | 7. Set Up SSL (HTTPS) with ACM 45 | ```bash 46 | aws acm request-certificate --domain-name code4u.innoaya.org --validation-method DNS 47 | ``` 48 | 49 | ### Deploy Updates after code changes 50 | ```bash 51 | aws s3 sync dist/ s3://code4u.innoaya.org/ --delete 52 | aws cloudfront create-invalidation --distribution-id E2GFBUCWIF2RUF --paths "/*" 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /src/firebase/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | // Helper functions 5 | function isSignedIn() { 6 | return request.auth != null; 7 | } 8 | 9 | // Allow authenticated users to upload to screenshots folder 10 | match /screenshots/{fileName} { 11 | // Allow anyone to read screenshot images 12 | allow read: if true; 13 | 14 | // Allow authenticated users to create images, with size and type constraints 15 | allow create: if isSignedIn() || true; // Allow both authenticated and anonymous uploads for feedback 16 | 17 | // Validate file is an image and under 5MB 18 | allow create: if request.resource.contentType.matches('image/.*') 19 | && request.resource.size < 5 * 1024 * 1024; 20 | 21 | // No updating or deleting images 22 | allow update, delete: if false; 23 | } 24 | 25 | // Profile pictures - different rules than screenshots 26 | match /profile-pictures/{userId}/{filename} { 27 | // Public read access (for profile display) 28 | allow read: if true; 29 | 30 | // Only allow users to upload their own profile picture 31 | allow create, update: if isSignedIn() && request.auth.uid == userId; 32 | 33 | // Allow users to delete their own profile picture 34 | allow delete: if isSignedIn() && request.auth.uid == userId; 35 | 36 | // Validate file is an image and under 2MB 37 | allow create, update: if request.resource.contentType.matches('image/.*') 38 | && request.resource.size < 2 * 1024 * 1024; 39 | } 40 | 41 | // Restrict access to all other files 42 | match /{allPaths=**} { 43 | allow read, write: if false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/stores/userStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref, computed } from 'vue'; 3 | import { auth, db } from '@/firebase'; 4 | import { doc, getDoc } from 'firebase/firestore'; 5 | import { onAuthStateChanged } from 'firebase/auth'; 6 | 7 | export const useUserStore = defineStore('user', () => { 8 | // State 9 | const currentUser = ref(null); 10 | const userRole = ref(null); 11 | const userProfile = ref(null); 12 | const isLoading = ref(true); 13 | 14 | // Computed properties 15 | const isAdmin = computed(() => userRole.value === 'admin'); 16 | const isCreator = computed(() => userRole.value === 'creator'); 17 | const isAdminOrCreator = computed(() => isAdmin.value || isCreator.value); 18 | const isLoggedIn = computed(() => !!currentUser.value); 19 | 20 | // Actions 21 | function init() { 22 | onAuthStateChanged(auth, async (user) => { 23 | isLoading.value = true; 24 | 25 | if (user) { 26 | currentUser.value = user; 27 | await fetchUserRole(user.uid); 28 | await fetchUserProfile(user.uid); 29 | } else { 30 | currentUser.value = null; 31 | userRole.value = null; 32 | userProfile.value = null; 33 | } 34 | 35 | isLoading.value = false; 36 | }); 37 | } 38 | 39 | async function fetchUserRole(userId) { 40 | try { 41 | const userDoc = await getDoc(doc(db, 'users', userId)); 42 | if (userDoc.exists()) { 43 | userRole.value = userDoc.data().role || 'user'; 44 | } else { 45 | userRole.value = 'user'; // Default role 46 | } 47 | } catch (error) { 48 | console.error('Error fetching user role:', error); 49 | userRole.value = 'user'; // Default to user on error 50 | } 51 | } 52 | 53 | async function fetchUserProfile(userId) { 54 | try { 55 | const userDoc = await getDoc(doc(db, 'users', userId)); 56 | if (userDoc.exists()) { 57 | userProfile.value = userDoc.data(); 58 | } 59 | } catch (error) { 60 | console.error('Error fetching user profile:', error); 61 | } 62 | } 63 | 64 | // Initialize auth listener on store creation 65 | init(); 66 | 67 | return { 68 | currentUser, 69 | userRole, 70 | userProfile, 71 | isLoading, 72 | isAdmin, 73 | isCreator, 74 | isAdminOrCreator, 75 | isLoggedIn, 76 | fetchUserRole, 77 | fetchUserProfile 78 | }; 79 | }); 80 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: 66 | color 0.5s, 67 | background-color 0.5s; 68 | line-height: 1.6; 69 | font-family: 70 | Inter, 71 | -apple-system, 72 | BlinkMacSystemFont, 73 | 'Segoe UI', 74 | Roboto, 75 | Oxygen, 76 | Ubuntu, 77 | Cantarell, 78 | 'Fira Sans', 79 | 'Droid Sans', 80 | 'Helvetica Neue', 81 | sans-serif; 82 | font-size: 15px; 83 | text-rendering: optimizeLegibility; 84 | -webkit-font-smoothing: antialiased; 85 | -moz-osx-font-smoothing: grayscale; 86 | } 87 | -------------------------------------------------------------------------------- /src/views/TermsOfServiceView.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /src/views/PrivacyPolicyView.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /src/firebase/journeys.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Journeys Configuration for Code4U 3 | * 4 | * This file provides client-side functionality for journeyStore.js 5 | * to fetch and manage journey data from Firestore. 6 | */ 7 | 8 | import { db, auth } from './index.js' 9 | import { collection, doc, getDocs, getDoc, query, where } from 'firebase/firestore' 10 | 11 | /** 12 | * Fetch available journeys based on user role 13 | * @param {boolean} includeUnpublished - Whether to include unpublished journeys (for admin UI) 14 | * @returns {Array} Array of journey objects 15 | */ 16 | export async function fetchJourneys(includeUnpublished = false) { 17 | try { 18 | let journeysSnapshot; 19 | const currentUser = auth.currentUser; 20 | 21 | if (includeUnpublished && currentUser) { 22 | // For admin views - fetch all journeys 23 | journeysSnapshot = await getDocs(collection(db, 'journeys')); 24 | } else { 25 | // For public views - fetch only published journeys 26 | const journeysQuery = query(collection(db, 'journeys'), where('isPublished', '==', true)); 27 | journeysSnapshot = await getDocs(journeysQuery); 28 | } 29 | 30 | return journeysSnapshot.docs.map(doc => ({ 31 | id: doc.id, 32 | ...doc.data() 33 | })).sort((a, b) => a.order - b.order); 34 | } catch (error) { 35 | console.error('Error fetching journeys:', error); 36 | return []; 37 | } 38 | } 39 | 40 | /** 41 | * Fetch levels for a specific journey 42 | * @param {string} journeyId - ID of the journey 43 | * @returns {Array} Array of level objects sorted by number 44 | */ 45 | export async function fetchJourneyLevels(journeyId) { 46 | try { 47 | const journey = await getDoc(doc(db, 'journeys', journeyId)); 48 | 49 | if (!journey.exists()) { 50 | throw new Error(`Journey with ID ${journeyId} not found`); 51 | } 52 | 53 | const journeyData = journey.data(); 54 | 55 | // If no levels in the journey, return empty array 56 | if (!journeyData.levelIds || !Array.isArray(journeyData.levelIds) || journeyData.levelIds.length === 0) { 57 | return []; 58 | } 59 | 60 | // Get all levels for this journey 61 | const levelsPromises = journeyData.levelIds.map(levelId => 62 | getDoc(doc(db, 'levels', levelId)) 63 | ); 64 | 65 | const levelDocs = await Promise.all(levelsPromises); 66 | 67 | // Map to array of level objects 68 | const levels = levelDocs 69 | .filter(doc => doc.exists()) 70 | .map(doc => ({ 71 | id: doc.id, 72 | ...doc.data() 73 | })); 74 | 75 | // Sort levels by their number property 76 | return levels.sort((a, b) => a.number - b.number); 77 | } catch (error) { 78 | console.error(`Error fetching levels for journey ${journeyId}:`, error); 79 | return []; 80 | } 81 | } 82 | 83 | // Export all client-side journey functions 84 | export default { 85 | fetchJourneys, 86 | fetchJourneyLevels 87 | }; 88 | -------------------------------------------------------------------------------- /src/views/ActivitiesView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 68 | -------------------------------------------------------------------------------- /src/firebase/analytics-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Firebase Analytics utilities for Code4U 3 | * This file contains functions for tracking user interactions and educational metrics 4 | */ 5 | 6 | import { logAnalyticsEvent } from './index'; 7 | 8 | // User engagement events 9 | export const trackLogin = (method) => { 10 | logAnalyticsEvent('login', { method }); 11 | }; 12 | 13 | export const trackSignUp = (method) => { 14 | logAnalyticsEvent('sign_up', { method }); 15 | }; 16 | 17 | export const trackProfileUpdate = (fields) => { 18 | logAnalyticsEvent('profile_update', { fields }); 19 | }; 20 | 21 | // Learning path events 22 | export const trackPathSelected = (path) => { 23 | logAnalyticsEvent('learning_path_selected', { path }); 24 | }; 25 | 26 | // Level interactions 27 | export const trackLevelStarted = (levelId, levelNumber, category) => { 28 | logAnalyticsEvent('level_started', { 29 | level_id: levelId, 30 | level_number: levelNumber, 31 | category 32 | }); 33 | }; 34 | 35 | export const trackLevelCompleted = (levelId, levelNumber, category, timeSpentSeconds) => { 36 | logAnalyticsEvent('level_completed', { 37 | level_id: levelId, 38 | level_number: levelNumber, 39 | category, 40 | time_spent_seconds: timeSpentSeconds 41 | }); 42 | }; 43 | 44 | export const trackCodeSubmitted = (levelId, success) => { 45 | logAnalyticsEvent('code_submitted', { 46 | level_id: levelId, 47 | success 48 | }); 49 | }; 50 | 51 | export const trackHintViewed = (levelId, hintNumber) => { 52 | logAnalyticsEvent('hint_viewed', { 53 | level_id: levelId, 54 | hint_number: hintNumber 55 | }); 56 | }; 57 | 58 | // Badge and achievement events 59 | export const trackBadgeEarned = (badgeId, badgeName) => { 60 | logAnalyticsEvent('badge_earned', { 61 | badge_id: badgeId, 62 | badge_name: badgeName 63 | }); 64 | }; 65 | 66 | // Content interaction events 67 | export const trackContentView = (contentType, contentId) => { 68 | logAnalyticsEvent('content_view', { 69 | content_type: contentType, 70 | content_id: contentId 71 | }); 72 | }; 73 | 74 | // Page view tracking 75 | export const trackPageView = (pageName) => { 76 | logAnalyticsEvent('page_view', { 77 | page_name: pageName, 78 | page_location: window.location.href, 79 | page_path: window.location.pathname 80 | }); 81 | }; 82 | 83 | // Legal document events 84 | export const trackLegalDocumentView = (documentType) => { 85 | logAnalyticsEvent('legal_document_view', { 86 | document_type: documentType 87 | }); 88 | }; 89 | 90 | // Feature usage events 91 | export const trackFeatureUsed = (featureName) => { 92 | logAnalyticsEvent('feature_used', { 93 | feature_name: featureName 94 | }); 95 | }; 96 | 97 | // Error tracking 98 | export const trackError = (errorCode, errorMessage, context) => { 99 | logAnalyticsEvent('app_error', { 100 | error_code: errorCode, 101 | error_message: errorMessage, 102 | context 103 | }); 104 | }; 105 | 106 | // Custom conversion events 107 | export const trackMilestoneReached = (milestoneName) => { 108 | logAnalyticsEvent('milestone_reached', { 109 | milestone_name: milestoneName 110 | }); 111 | }; 112 | -------------------------------------------------------------------------------- /src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 95 | -------------------------------------------------------------------------------- /src/firebase/nodejs/import-level-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Firebase level data importer for Code4U 3 | * This script imports level data from JSON files in the /docs directory into Firestore 4 | */ 5 | 6 | import { db } from './firebase-node.js' 7 | import { doc, writeBatch, getDoc } from 'firebase/firestore' 8 | import fs from 'fs' 9 | import path from 'path' 10 | import { fileURLToPath } from 'url' 11 | 12 | // Get the current file's directory 13 | const __filename = fileURLToPath(import.meta.url) 14 | const __dirname = path.dirname(__filename) 15 | const rootDir = path.resolve(__dirname, '../../../') 16 | 17 | // Path to JSON files 18 | const docsDir = path.join(rootDir, 'docs') 19 | 20 | // Files to import 21 | const levelFiles = [ 22 | 'JavaScript-Levels.json', 23 | 'Python-Levels.json', 24 | 'CSS-Levels.json' 25 | ] 26 | 27 | /** 28 | * Import levels from JSON files to Firestore 29 | */ 30 | async function importLevelsFromJson() { 31 | console.log('Starting level import process...') 32 | 33 | // Counter for statistics 34 | let totalLevels = 0 35 | let importedLevels = 0 36 | let skippedLevels = 0 37 | let failedLevels = 0 38 | 39 | try { 40 | // Process each JSON file 41 | for (const file of levelFiles) { 42 | const filePath = path.join(docsDir, file) 43 | 44 | // Check if file exists 45 | if (!fs.existsSync(filePath)) { 46 | console.log(`File not found: ${file}. Skipping.`) 47 | continue 48 | } 49 | 50 | // Read and parse the JSON file 51 | console.log(`\nReading ${file}...`) 52 | const jsonData = fs.readFileSync(filePath, 'utf8') 53 | const levels = JSON.parse(jsonData) 54 | 55 | totalLevels += levels.length 56 | console.log(`Found ${levels.length} levels in ${file}`) 57 | 58 | // Use batched writes for more efficient imports 59 | // Firestore allows up to 500 operations per batch 60 | const batchSize = 450 61 | let batchCount = 0 62 | 63 | for (let i = 0; i < levels.length; i += batchSize) { 64 | const batch = writeBatch(db) 65 | const currentBatch = levels.slice(i, i + batchSize) 66 | batchCount++ 67 | 68 | console.log(`Processing batch ${batchCount} (${currentBatch.length} levels)`) 69 | 70 | for (const level of currentBatch) { 71 | // Check if the level already exists 72 | const levelDocRef = doc(db, 'levels', level.id) 73 | const levelDoc = await getDoc(levelDocRef) 74 | 75 | if (levelDoc.exists()) { 76 | console.log(`Level ${level.id} already exists. Skipping.`) 77 | skippedLevels++ 78 | continue 79 | } 80 | 81 | // Add the level to the batch 82 | batch.set(levelDocRef, { 83 | ...level, 84 | createdAt: new Date(), 85 | isPublished: true // Default to published 86 | }) 87 | 88 | importedLevels++ 89 | } 90 | 91 | // Commit the batch 92 | await batch.commit() 93 | console.log(`Batch ${batchCount} committed successfully`) 94 | } 95 | } 96 | 97 | console.log('\n=== Import Summary ===') 98 | console.log(`Total levels found: ${totalLevels}`) 99 | console.log(`Levels imported: ${importedLevels}`) 100 | console.log(`Levels skipped (already exist): ${skippedLevels}`) 101 | console.log(`Failed imports: ${failedLevels}`) 102 | console.log('Import process complete!') 103 | 104 | } catch (error) { 105 | console.error('Error importing levels:', error) 106 | process.exit(1) 107 | } 108 | } 109 | 110 | // Run the import function 111 | importLevelsFromJson().then(() => { 112 | console.log('Script execution completed') 113 | process.exit(0) 114 | }).catch(error => { 115 | console.error('Script execution failed:', error) 116 | process.exit(1) 117 | }) 118 | -------------------------------------------------------------------------------- /src/components/CodePreview.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 112 | 113 | 142 | -------------------------------------------------------------------------------- /src/components/AdminBreadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 128 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | 49 | </c4u> 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/router/adminGuard.js: -------------------------------------------------------------------------------- 1 | import { auth, db } from '../firebase' 2 | import { onAuthStateChanged } from 'firebase/auth' 3 | import { doc, getDoc } from 'firebase/firestore' 4 | 5 | // Keep track of auth state initialization 6 | let authInitialized = false 7 | let currentUser = null 8 | 9 | // Initialize auth state tracking once 10 | onAuthStateChanged(auth, (user) => { 11 | currentUser = user 12 | authInitialized = true 13 | console.log('Auth state changed in adminGuard:', user ? 'User logged in' : 'No user') 14 | }) 15 | 16 | /** 17 | * Navigation guard to check if a user has admin role 18 | * This guard will redirect non-admin users to the home page 19 | */ 20 | export const requireAdmin = (to, from, next) => { 21 | console.log('Admin guard running, auth initialized:', authInitialized) 22 | 23 | // If auth isn't initialized yet, wait briefly before checking 24 | if (!authInitialized) { 25 | console.log('Auth not initialized, waiting...') 26 | // Create a function to wait for auth initialization 27 | const waitForAuthInit = () => { 28 | if (authInitialized) { 29 | checkAdminStatus(currentUser, to, next) 30 | } else { 31 | // Still not initialized, wait a bit longer 32 | setTimeout(waitForAuthInit, 50) 33 | } 34 | } 35 | // Start waiting 36 | waitForAuthInit() 37 | return 38 | } 39 | 40 | // Auth is already initialized, check admin status 41 | checkAdminStatus(currentUser, to, next) 42 | } 43 | 44 | /** 45 | * Navigation guard to check if a user has creator or admin role 46 | * This guard will redirect regular users to the home page 47 | */ 48 | export const requireCreatorOrAdmin = (to, from, next) => { 49 | console.log('Creator guard running, auth initialized:', authInitialized) 50 | 51 | // If auth isn't initialized yet, wait briefly before checking 52 | if (!authInitialized) { 53 | console.log('Auth not initialized, waiting...') 54 | // Create a function to wait for auth initialization 55 | const waitForAuthInit = () => { 56 | if (authInitialized) { 57 | checkCreatorOrAdminStatus(currentUser, to, next) 58 | } else { 59 | // Still not initialized, wait a bit longer 60 | setTimeout(waitForAuthInit, 50) 61 | } 62 | } 63 | // Start waiting 64 | waitForAuthInit() 65 | return 66 | } 67 | 68 | // Auth is already initialized, check creator status 69 | checkCreatorOrAdminStatus(currentUser, to, next) 70 | } 71 | 72 | /** 73 | * Helper function to check if user has admin role 74 | */ 75 | async function checkAdminStatus(user, to, next) { 76 | // First check if the user is authenticated 77 | if (!user) { 78 | console.log('No authenticated user, redirecting to login') 79 | next({ name: 'login', query: { redirect: to.fullPath } }) 80 | return 81 | } 82 | 83 | try { 84 | console.log('Checking admin status for user:', user.uid) 85 | // Get the user document and check for admin role 86 | const userDoc = await getDoc(doc(db, 'users', user.uid)) 87 | 88 | if (userDoc.exists() && userDoc.data().role === 'admin') { 89 | // User is an admin, allow access 90 | console.log('User is admin, allowing access') 91 | next() 92 | } else { 93 | // User is not an admin, redirect to home 94 | console.warn('Non-admin user attempted to access admin page') 95 | next({ name: 'home' }) 96 | } 97 | } catch (error) { 98 | console.error('Error checking admin status:', error) 99 | next({ name: 'home' }) 100 | } 101 | } 102 | 103 | /** 104 | * Helper function to check if user has creator or admin role 105 | */ 106 | async function checkCreatorOrAdminStatus(user, to, next) { 107 | // First check if the user is authenticated 108 | if (!user) { 109 | console.log('No authenticated user, redirecting to login') 110 | next({ name: 'login', query: { redirect: to.fullPath } }) 111 | return 112 | } 113 | 114 | try { 115 | console.log('Checking creator/admin status for user:', user.uid) 116 | // Get the user document and check for creator or admin role 117 | const userDoc = await getDoc(doc(db, 'users', user.uid)) 118 | 119 | if (userDoc.exists() && (userDoc.data().role === 'admin' || userDoc.data().role === 'creator')) { 120 | // User is a creator or admin, allow access 121 | console.log('User is creator or admin, allowing access') 122 | next() 123 | } else { 124 | // User is not a creator or admin, redirect to home 125 | console.warn('Non-creator/admin user attempted to access creator page') 126 | next({ name: 'home' }) 127 | } 128 | } catch (error) { 129 | console.error('Error checking creator/admin status:', error) 130 | next({ name: 'home' }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 107 | 108 | 109 | </> 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/LEVEL.md: -------------------------------------------------------------------------------- 1 | # Level Data Definition 2 | 3 | This document defines the structure of the data used to represent a Level consisting of multiple tasks. 4 | 5 | --- 6 | 7 | ## Level Structure 8 | 9 | - The curriculum is an **array** of **Level** objects. 10 | - Each **Level** contains metadata about the learning module and an array of **Tasks**. 11 | - Tasks represent interactive coding exercises within a level. 12 | 13 | --- 14 | 15 | ## Level Object 16 | 17 | Each **Level** object has the following fields: 18 | 19 | | Field Name | Type | Description | 20 | |----------------------|----------------------------|----------------------------------------------------------------------------------------------------| 21 | | `id` | `string` | Unique identifier for the level (e.g., `"level-11"`) | 22 | | `number` | `number` | Numeric sequence/order of the level | 23 | | `title` | `string` | The title of the level/module | 24 | | `description` | `string` | A brief summary describing the purpose and content of the level | 25 | | `category` | `string` | The subject category this level belongs to (e.g., `"JavaScript"`) | 26 | | `difficulty` | `string` | Difficulty rating (e.g., `"Beginner"`, `"Intermediate"`, `"Advanced"`) | 27 | | `pointsToEarn` | `number` | Number of points awarded for completing this level | 28 | | `estimatedTime` | `string` | Approximate time to complete the level (e.g., `"45 minutes"`) | 29 | | `prerequisites` | `string[]` | Array of prerequisite topics or prior levels the learner should know before starting this level | 30 | | `learningObjectives` | `string[]` | Array of learning goals that the learner should achieve by completing the level | 31 | | `realWorldApplications` | `string[]` | Examples of practical uses or applications of the concepts taught in the level | 32 | | `references` | `{ title: string, url: string }[]` | Array of reference resources, each with a title and URL for further study | 33 | | `tags` | `string[]` | Keywords or tags for categorizing and searching levels | 34 | | `tasks` | `Task[]` | Array of tasks (interactive coding exercises) contained in the level | 35 | 36 | --- 37 | 38 | ## Task Object 39 | 40 | Each **Task** object within a level includes: 41 | 42 | | Field Name | Type | Description | 43 | |--------------------|------------------|-------------------------------------------------------------------------------------------------| 44 | | `id` | `string` | Unique identifier for the task within the level (e.g., `"task1"`) | 45 | | `title` | `string` | Title of the coding exercise | 46 | | `description` | `string` | Detailed instructions describing what the learner needs to do | 47 | | `initialCode` | `string` | Starter code provided to the learner as a base for their solution | 48 | | `solution` | `string` | Partial code or hint representing the expected solution or key part of it | 49 | | `expectedOutput` | `string` | Message or output expected upon successful completion of the task | 50 | | `errorHint` | `string` | Helpful hints or suggestions to guide learners if their solution is incorrect | 51 | 52 | --- 53 | 54 | ## Example: Level and Task (Simplified) 55 | 56 | ```json 57 | { 58 | "id": "level-11", 59 | "number": 11, 60 | "title": "JavaScript Basics: Start Coding!", 61 | "description": "Begin your journey into programming! Learn the building blocks of JavaScript including variables, data types, and simple operations.", 62 | "category": "JavaScript", 63 | "difficulty": "Beginner", 64 | "pointsToEarn": 300, 65 | "estimatedTime": "45 minutes", 66 | "prerequisites": ["HTML Fundamentals"], 67 | "learningObjectives": [ 68 | "Understand what JavaScript is and why it's important", 69 | "Learn about variables and different data types", 70 | "Use basic operators to perform calculations" 71 | ], 72 | "realWorldApplications": [ 73 | "Creating interactive elements on your website", 74 | "Building simple calculators and tools", 75 | "Making decisions in your programs" 76 | ], 77 | "references": [ 78 | { 79 | "title": "MDN Web Docs: JavaScript First Steps", 80 | "url": "https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps" 81 | }, 82 | { 83 | "title": "W3Schools JavaScript Tutorial", 84 | "url": "https://www.w3schools.com/js/js_intro.asp" 85 | } 86 | ], 87 | "tags": ["JavaScript", "Programming", "Variables", "Data Types"], 88 | "tasks": [ 89 | { 90 | "id": "task1", 91 | "title": "Create your first variables", 92 | "description": "Variables are like containers that store information in your program. Create variables for name, age, and isStudent using the appropriate data types (string, number, and boolean).", 93 | "initialCode": "// Create your variables below\n// Example: let color = \"blue\";\n\n// Your code here:\n", 94 | "solution": "let name", 95 | "expectedOutput": "Variables created successfully!", 96 | "errorHint": "You need to declare variables using let or const. Try: let name = \"Alex\"; let age = 14; let isStudent = true;" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /src/firebase/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | // Helper functions 5 | function isSignedIn() { 6 | return request.auth != null; 7 | } 8 | 9 | function isOwner(userId) { 10 | return isSignedIn() && request.auth.uid == userId; 11 | } 12 | 13 | // Helper function to check if user is an admin 14 | function isAdmin() { 15 | return isSignedIn() && 16 | exists(/databases/$(database)/documents/users/$(request.auth.uid)) && 17 | get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'; 18 | } 19 | 20 | // Helper function to check if user is a creator 21 | function isCreator() { 22 | return isSignedIn() && 23 | exists(/databases/$(database)/documents/users/$(request.auth.uid)) && 24 | get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'creator'; 25 | } 26 | 27 | // Helper function to check if user is an admin or creator 28 | function isAdminOrCreator() { 29 | return isAdmin() || isCreator(); 30 | } 31 | 32 | // Helper function to check if user is the creator of a document 33 | function isDocumentCreator(docData) { 34 | return isSignedIn() && docData.createdBy == request.auth.uid; 35 | } 36 | 37 | // User profiles - public read access for leaderboard functionality 38 | match /users/{userId} { 39 | // Allow public read access for all users to support leaderboard 40 | allow read: if true; 41 | 42 | // But only allow users to create/update their own data 43 | allow create: if isSignedIn() && request.auth.uid == userId; 44 | allow update: if isSignedIn() && ( 45 | // Normal users can only update specific fields 46 | (request.auth.uid == userId && 47 | request.resource.data.diff(resource.data).affectedKeys() 48 | .hasOnly(['displayName', 'photoURL', 'level', 'points', 'badges', 'completedLevels', 'lastLogin', 'isFirstLogin'])) || 49 | // Admins can update any user 50 | isAdmin() 51 | ); 52 | allow delete: if false; 53 | } 54 | 55 | // Level data - filtered by isPublished for regular users, admins and creators can modify, only admins can delete 56 | match /levels/{levelId} { 57 | // Allow everyone (including unauthenticated users) to read published levels 58 | allow read: if request.auth == null || resource.data.isPublished == true || isAdmin() || 59 | (isCreator() && resource.data.createdBy == request.auth.uid); 60 | allow create: if isAdminOrCreator(); // Admins and creators can create levels 61 | allow update: if isAdmin() || (isCreator() && resource.data.createdBy == request.auth.uid); // Admins can edit any level, creators can only edit their own 62 | allow delete: if isAdmin(); // Only admins can delete levels 63 | } 64 | 65 | // User activities - public read access for activity feed, restricted write 66 | match /user_activities/{activityId} { 67 | allow read: if true; // Public read access for activity feed 68 | allow create: if isSignedIn() && request.resource.data.userId == request.auth.uid; 69 | allow update, delete: if isAdmin(); // Allow admins to update or delete activities 70 | } 71 | 72 | // Badges - read-only for all authenticated users 73 | match /badges/{badgeId} { 74 | allow read: if true; 75 | allow create: if isAdminOrCreator(); // Admins and creators can create badges 76 | allow update: if isAdmin() || (isCreator() && resource.data.createdBy == request.auth.uid); // Admins can edit any badge, creators can only edit their own 77 | allow delete: if isAdmin(); // Only admins can delete badges 78 | } 79 | 80 | // Journeys - read filtered by isPublished, admins and creators can write, only admins can delete 81 | match /journeys/{journeyId} { 82 | // Allow everyone (including unauthenticated users) to read published journeys 83 | allow read: if request.auth == null || resource.data.isPublished == true || isAdmin() || 84 | (isCreator() && resource.data.createdBy == request.auth.uid); 85 | allow create: if isAdminOrCreator(); // Admins and creators can create journeys 86 | allow update: if isAdmin() || (isCreator() && resource.data.createdBy == request.auth.uid); // Admins can edit any journey, creators can only edit their own 87 | allow delete: if isAdmin(); // Only admins can delete journeys 88 | } 89 | 90 | // Leaderboard data - public read access 91 | match /leaderboard/{entryId} { 92 | allow read: if true; // Allow everyone to read leaderboard data 93 | allow write: if isAdmin(); // Allow admins to update leaderboard 94 | } 95 | 96 | // Feedback collection - allow users to submit and read their own feedback, admins can read all 97 | match /feedback/{feedbackId} { 98 | allow read: if isSignedIn() && ( 99 | resource.data.userId == request.auth.uid || // User can read their own feedback 100 | isAdmin() // Admins can read all feedback 101 | ); 102 | allow create: if true; // Allow both signed-in and anonymous feedback 103 | allow update: if isAdmin(); // Admins can update feedback status 104 | allow delete: if isAdmin(); // Admins can delete feedback if needed 105 | } 106 | 107 | // Feedback comments - allow admins to manage, users to read their own 108 | match /feedback_comments/{commentId} { 109 | allow read: if isSignedIn() && ( 110 | exists(/databases/$(database)/documents/feedback/$(resource.data.feedbackId)) && 111 | ( 112 | get(/databases/$(database)/documents/feedback/$(resource.data.feedbackId)).data.userId == request.auth.uid || // User can read comments on their feedback 113 | isAdmin() // Admins can read all comments 114 | ) 115 | ); 116 | allow create: if isSignedIn() && ( 117 | request.resource.data.authorId == request.auth.uid || // User creating comment with their ID 118 | isAdmin() // Admins can create comments 119 | ); 120 | allow update, delete: if isAdmin(); // Only admins can update or delete comments 121 | } 122 | 123 | // Legal content - public read access, admin-only write access 124 | match /legal_content/{documentId} { 125 | allow read: if true; // Allow everyone to read legal documents 126 | allow write: if isAdmin(); // Allow admins to update legal content 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/views/LoginView.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 157 | -------------------------------------------------------------------------------- /CREATOR.md: -------------------------------------------------------------------------------- 1 | # Code4U Creator Documentation 2 | 3 | This guide is designed for content creators developing educational material for the Code4U platform. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Introduction](#introduction) 8 | 2. [Creator Role Access](#creator-role-access) 9 | 3. [Content Structure](#content-structure) 10 | 4. [Solution Checking](#solution-checking) 11 | 5. [Creating Effective Levels](#creating-effective-levels) 12 | 6. [Journey Organization](#journey-organization) 13 | 7. [Badge Creation](#badge-creation) 14 | 8. [Content Management Workflow](#content-management-workflow) 15 | 16 | ## Introduction 17 | 18 | Code4U is an interactive educational web application designed to teach middle and high school students web development fundamentals through a gamified learning experience. As a creator, you'll be developing content that engages students while teaching them valuable coding skills in HTML, CSS, and JavaScript. 19 | 20 | ## Creator Role Access 21 | 22 | As a creator, you have the following permissions: 23 | 24 | - Create and edit your own journeys, levels, and badges 25 | - Manage the visibility of your content through the `isPublished` field 26 | - View analytics for your created content 27 | - Organize your content into meaningful learning paths 28 | 29 | Note that creators cannot permanently delete content - instead, use the `isPublished` toggle to hide content that should not be publicly visible. 30 | 31 | ## Content Structure 32 | 33 | The platform organizes content into a hierarchy: 34 | 35 | ``` 36 | Journeys 37 | └── Levels 38 | └── Tasks 39 | ``` 40 | 41 | - **Journeys**: Collections of related levels forming a complete learning path 42 | - **Levels**: Individual coding challenges with specific learning objectives 43 | - **Tasks**: Step-by-step exercises within a level that build toward the learning objective 44 | 45 | ## Solution Checking 46 | 47 | ### How Solutions are Verified 48 | 49 | Code4U uses pattern matching to verify student solutions. Each task has a `solution` field containing a string that must be present in the student's code to be considered correct. 50 | 51 | ```javascript 52 | // From gameStore.js 53 | function runCode(code) { 54 | if (!currentLevel.value || !currentLevel.value.tasks) return false 55 | 56 | const task = currentLevel.value.tasks[currentTask.value] 57 | userCode.value = code 58 | 59 | // Check if the solution pattern exists in the student's code 60 | if (code.includes(task.solution)) { 61 | codeOutput.value = task.expectedOutput 62 | return true 63 | } else { 64 | codeOutput.value = 'Error: ' + task.errorHint 65 | return false 66 | } 67 | } 68 | ``` 69 | 70 | ### Task Structure 71 | 72 | When creating a task, include the following fields: 73 | 74 | | Field | Description | Example | 75 | |-------|-------------|---------| 76 | | `id` | Unique identifier for the task | `"task1"` | 77 | | `title` | Short, descriptive title | `"Create a simple if statement"` | 78 | | `description` | Clear instructions for the student | `"Write a program that checks if a user is old enough (13 or older)."` | 79 | | `initialCode` | Starter code for the student | `"let age = 12;\nlet message = \"\";\n// Your code here:"` | 80 | | `solution` | Pattern to check for in student's answer | `"if (age >= 13)"` | 81 | | `expectedOutput` | Success message when correct | `"Conditional statement created successfully!"` | 82 | | `errorHint` | Helpful hint when incorrect | `"Use an if/else statement to check if age is greater than or equal to 13."` | 83 | 84 | ### Solution Pattern Best Practices 85 | 86 | 1. **Be specific but flexible**: Choose a solution pattern that must be present in a correct answer but allows for variations in implementation 87 | 88 | 2. **Focus on key concepts**: Your solution pattern should verify the student understands the core concept being taught 89 | 90 | 3. **Consider edge cases**: Think about different ways students might correctly solve the problem 91 | 92 | 4. **Multiple solutions**: If there are multiple valid approaches, consider using several solution patterns and checking if any match 93 | 94 | 5. **Avoid overly strict checking**: Don't require exact whitespace or formatting matches 95 | 96 | ## Creating Effective Levels 97 | 98 | ### Level Structure 99 | 100 | Each level includes: 101 | 102 | - Core metadata (ID, number, title, description) 103 | - Category (HTML, CSS, or JavaScript) 104 | - Difficulty rating 105 | - Points awarded on completion 106 | - Learning objectives 107 | - Real-world applications 108 | - Reference materials 109 | - Tags for searchability 110 | - A series of tasks that build toward the learning objective 111 | 112 | ### Best Practices 113 | 114 | 1. **Clear progression**: Design levels that build on previous knowledge 115 | 2. **Engaging context**: Provide real-world scenarios for applying coding concepts 116 | 3. **Scaffolded learning**: Start with guided examples and gradually reduce support 117 | 4. **Clear success criteria**: Make it obvious what students need to accomplish 118 | 5. **Helpful feedback**: Provide specific guidance when students make mistakes 119 | 120 | ## Journey Organization 121 | 122 | Journeys are collections of levels that form a complete learning path. When creating a journey: 123 | 124 | 1. Group related levels that build toward a cohesive skill set 125 | 2. Provide a clear description of what students will learn 126 | 3. Set appropriate difficulty and prerequisites 127 | 4. Include varied content to maintain engagement 128 | 5. Culminate with a project or challenge that integrates all skills 129 | 130 | ## Badge Creation 131 | 132 | Badges are awarded for achievements within the platform. As a creator, you can define badges that are awarded for: 133 | 134 | 1. Completing specific levels 135 | 2. Finishing entire categories (HTML, CSS, JavaScript) 136 | 3. Achieving special milestones 137 | 138 | Each badge needs: 139 | - A unique identifier 140 | - A descriptive name 141 | - Category classification 142 | - Icon representation 143 | - Requirements for earning 144 | 145 | ## Content Management Workflow 146 | 147 | ### Creating New Content 148 | 149 | 1. **Plan your content**: Define learning objectives, tasks, and progression 150 | 2. **Draft in the admin interface**: Create your journeys, levels, and tasks 151 | 3. **Test thoroughly**: Verify all solutions work as expected 152 | 4. **Review and polish**: Refine descriptions, hints, and examples 153 | 5. **Publish when ready**: Toggle the `isPublished` field to make content available 154 | 155 | ### Updating Existing Content 156 | 157 | 1. Only published content is visible to regular users 158 | 2. You can toggle publication status at any time 159 | 3. Make content changes and test thoroughly before republishing 160 | 161 | ### Collaboration Considerations 162 | 163 | 1. While you can only edit your own content, you can view others' published content for inspiration 164 | 2. Organize your content with clear naming conventions 165 | 3. Use tags effectively to help users discover your content 166 | 167 | --- 168 | 169 | By following these guidelines, you'll create engaging, effective learning experiences that help students develop their coding skills within the Code4U platform. 170 | -------------------------------------------------------------------------------- /src/firebase/storage-utils.js: -------------------------------------------------------------------------------- 1 | import { storage } from './index.js'; 2 | import { ref as storageRef, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'; 3 | 4 | /** 5 | * Resize an image to a maximum width/height while maintaining aspect ratio 6 | * @param {File|Blob} file - The image file to resize 7 | * @param {number} maxSize - The maximum width or height in pixels 8 | * @returns {Promise} - A promise that resolves with the resized image blob 9 | */ 10 | const resizeImage = (file, maxSize = 256) => { 11 | return new Promise((resolve, reject) => { 12 | // Create a FileReader to read the image 13 | const reader = new FileReader(); 14 | reader.onload = (event) => { 15 | // Create an image element to load the file 16 | const img = new Image(); 17 | img.onload = () => { 18 | // Calculate new dimensions while maintaining aspect ratio 19 | let width = img.width; 20 | let height = img.height; 21 | 22 | if (width > height) { 23 | if (width > maxSize) { 24 | height = Math.round(height * (maxSize / width)); 25 | width = maxSize; 26 | } 27 | } else { 28 | if (height > maxSize) { 29 | width = Math.round(width * (maxSize / height)); 30 | height = maxSize; 31 | } 32 | } 33 | 34 | // Create a canvas to draw the resized image 35 | const canvas = document.createElement('canvas'); 36 | canvas.width = width; 37 | canvas.height = height; 38 | 39 | // Draw the resized image on the canvas 40 | const ctx = canvas.getContext('2d'); 41 | ctx.drawImage(img, 0, 0, width, height); 42 | 43 | // Convert the canvas to a Blob 44 | canvas.toBlob((blob) => { 45 | resolve(blob); 46 | }, file.type || 'image/jpeg', 0.85); // 0.85 quality is a good balance 47 | }; 48 | img.onerror = () => { 49 | reject(new Error('Failed to load image for resizing')); 50 | }; 51 | img.src = event.target.result; 52 | }; 53 | reader.onerror = () => { 54 | reject(new Error('Failed to read file for image resizing')); 55 | }; 56 | reader.readAsDataURL(file); 57 | }); 58 | }; 59 | 60 | /** 61 | * Upload a profile picture to Firebase Storage 62 | * @param {string} userId - The user ID to use for the storage path 63 | * @param {File|Blob} file - The file to upload 64 | * @returns {Promise} - The download URL of the uploaded image 65 | */ 66 | export const uploadProfilePicture = async (userId, file) => { 67 | try { 68 | // Resize the image to 256px 69 | let fileToUpload; 70 | 71 | // Only resize if we're in a browser environment 72 | if (typeof document !== 'undefined') { 73 | try { 74 | fileToUpload = await resizeImage(file, 256); 75 | console.log('Image resized successfully to 256px'); 76 | } catch (resizeError) { 77 | console.warn('Failed to resize image, using original:', resizeError); 78 | fileToUpload = file; // Fallback to original if resizing fails 79 | } 80 | } else { 81 | fileToUpload = file; // Use original in non-browser environments 82 | } 83 | 84 | // Create a reference to the file in Firebase Storage 85 | const profilePicRef = storageRef(storage, `profile-pictures/${userId}/profile-image`); 86 | 87 | // Upload the file 88 | const snapshot = await uploadBytes(profilePicRef, fileToUpload); 89 | 90 | // Get and return the download URL 91 | const downloadURL = await getDownloadURL(snapshot.ref); 92 | return downloadURL; 93 | } catch (error) { 94 | console.error('Error uploading profile picture:', error); 95 | 96 | // If this is a CORS error or any storage error, return a temporary URL 97 | // that works as a placeholder until the proper configuration is set up 98 | if (error.code === 'storage/unauthorized' || error.message?.includes('CORS')) { 99 | console.warn('CORS issue detected. Using local placeholder image.'); 100 | 101 | // Create a data URL from the file (works in the browser without CORS issues) 102 | return new Promise((resolve) => { 103 | const reader = new FileReader(); 104 | reader.onloadend = () => resolve(reader.result); 105 | reader.readAsDataURL(file); 106 | }); 107 | } 108 | 109 | throw error; 110 | } 111 | }; 112 | 113 | /** 114 | * Upload a profile picture from an external URL to Firebase Storage 115 | * @param {string} userId - The user ID to use for the storage path 116 | * @param {string} imageUrl - The URL of the image to download and upload 117 | * @returns {Promise} - The download URL of the uploaded image 118 | */ 119 | export const uploadProfilePictureFromUrl = async (userId, imageUrl) => { 120 | try { 121 | // For Google profile pictures, we should use a proxy or modify the URL 122 | // to avoid CORS issues. Google profile pictures typically come from 123 | // lh3.googleusercontent.com or similar domains 124 | 125 | // Option 1: Just return the original URL to bypass CORS issues 126 | // We can't upload the image directly due to CORS, so we'll just use the original URL 127 | return imageUrl; 128 | 129 | // Note: In a production environment, you would typically: 130 | // 1. Use a server-side proxy to download the image and then upload it to Firebase 131 | // 2. Or use a CORS proxy service (though this has security implications) 132 | // 3. Or set up proper CORS headers on your Firebase Storage bucket 133 | 134 | /* Commented out the problematic code: 135 | // Fetch the image from the URL 136 | const response = await fetch(imageUrl); 137 | if (!response.ok) { 138 | throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); 139 | } 140 | 141 | // Convert response to blob 142 | const imageBlob = await response.blob(); 143 | 144 | // Upload the blob to Firebase Storage 145 | return await uploadProfilePicture(userId, imageBlob); 146 | */ 147 | } catch (error) { 148 | console.error('Error uploading profile picture from URL:', error); 149 | throw error; 150 | } 151 | }; 152 | 153 | /** 154 | * Delete a profile picture from Firebase Storage 155 | * @param {string} userId - The user ID to delete the picture for 156 | * @returns {Promise} 157 | */ 158 | export const deleteProfilePicture = async (userId) => { 159 | try { 160 | // Create a reference to the file in Firebase Storage 161 | const profilePicRef = storageRef(storage, `profile-pictures/${userId}/profile-image`); 162 | 163 | // Delete the file 164 | await deleteObject(profilePicRef); 165 | } catch (error) { 166 | console.error('Error deleting profile picture:', error); 167 | 168 | // If this is a CORS error or an unauthorized error, don't throw the error 169 | // This allows the rest of the profile update to continue even if storage access fails 170 | if (error.code === 'storage/unauthorized' || error.code === 'storage/object-not-found' || error.message?.includes('CORS')) { 171 | console.warn('CORS issue or file not found. Continuing with profile update.'); 172 | return; 173 | } 174 | 175 | throw error; 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /src/views/JourneyDetailView.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 190 | -------------------------------------------------------------------------------- /src/firebase/legal-utils.js: -------------------------------------------------------------------------------- 1 | import { db } from './index' 2 | import { doc, getDoc } from 'firebase/firestore' 3 | 4 | // Collection name for legal documents 5 | const LEGAL_COLLECTION = 'legal_content' 6 | 7 | // Default content for Terms of Service - copied here to avoid circular imports 8 | const DEFAULT_TERMS = { 9 | title: 'Terms of Service', 10 | content: [ 11 | { 12 | heading: 'Acceptance of Terms', 13 | text: 'By accessing and using the Code4U platform, you agree to be bound by these Terms of Service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site.' 14 | }, 15 | { 16 | heading: 'Use License', 17 | text: 'Permission is granted to temporarily use the Code4U platform for personal, educational, and non-commercial purposes only. This is the grant of a license, not a transfer of title, and under this license you may not: modify or copy the materials except as required for normal platform usage; use the materials for any commercial purpose; attempt to decompile or reverse engineer any software; remove any copyright or other proprietary notations; transfer the materials to another person or "mirror" the materials on any other server.' 18 | }, 19 | { 20 | heading: 'User Accounts', 21 | text: 'To access certain features of the platform, you must create an account. You are responsible for maintaining the confidentiality of your account information and for all activities that occur under your account. You agree to immediately notify Code4U of any unauthorized use of your account or any other breach of security.' 22 | }, 23 | { 24 | heading: 'User Content', 25 | text: 'Any code or content you submit, post, or display on or through Code4U is your responsibility. You retain ownership of your content, but grant Code4U a worldwide, royalty-free license to use, copy, reproduce, process, adapt, modify, publish, transmit, display, and distribute such content for educational and platform improvement purposes.' 26 | }, 27 | { 28 | heading: 'Disclaimer', 29 | text: 'The materials on the Code4U platform are provided on an \'as is\' basis. Code4U makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.' 30 | }, 31 | { 32 | heading: 'Limitations', 33 | text: 'In no event shall Code4U or its suppliers be liable for any damages arising out of the use or inability to use the materials on the platform, even if Code4U or an authorized representative has been notified orally or in writing of the possibility of such damage.' 34 | }, 35 | { 36 | heading: 'Governing Law', 37 | text: 'These terms and conditions are governed by and construed in accordance with local laws, and you irrevocably submit to the exclusive jurisdiction of the courts in that location.' 38 | }, 39 | { 40 | heading: 'Changes to Terms', 41 | text: 'Code4U reserves the right, at its sole discretion, to modify or replace these Terms at any time. It is your responsibility to check these Terms periodically for changes. Your continued use of the platform following the posting of any changes constitutes acceptance of those changes.' 42 | } 43 | ], 44 | lastUpdated: new Date().toISOString().split('T')[0] 45 | } 46 | 47 | // Default content for Privacy Policy - copied here to avoid circular imports 48 | const DEFAULT_PRIVACY = { 49 | title: 'Privacy Policy', 50 | content: [ 51 | { 52 | heading: 'Introduction', 53 | text: 'At Code4U, we take your privacy seriously. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our platform.' 54 | }, 55 | { 56 | heading: 'Information We Collect', 57 | text: 'We may collect information about you in various ways, including: Personal Data (name, email address, and profile picture obtained through Google authentication), Usage Data (information on how you interact with our platform, including completed levels, code solutions, and achievement records), and Technical Data (IP address, browser type, device information, and cookies to improve your experience).' 58 | }, 59 | { 60 | heading: 'How We Use Your Information', 61 | text: 'We may use the information we collect about you for various purposes: to provide and maintain our platform, to personalize your experience, to improve our platform, to track your progress and achievements, to communicate with you, and to ensure the security of our platform.' 62 | }, 63 | { 64 | heading: 'Storage and Protection', 65 | text: 'Your data is stored securely in Firebase, including Firebase Authentication, Firestore, and Firebase Storage. We implement measures designed to protect your information from unauthorized access, alteration, disclosure, or destruction.' 66 | }, 67 | { 68 | heading: 'Data Sharing', 69 | text: 'We do not sell, trade, or otherwise transfer your personal information to third parties without your consent, except as described in this Privacy Policy or as required by law.' 70 | }, 71 | { 72 | heading: 'Third-Party Services', 73 | text: 'We use Google services for authentication. Google may collect information as governed by their privacy policy. We encourage you to review Google\'s privacy practices.' 74 | }, 75 | { 76 | heading: 'Your Rights', 77 | text: 'You have the right to access, update, or delete your personal information. You can manage your profile through the platform\'s profile settings or contact us for assistance.' 78 | }, 79 | { 80 | heading: 'Children\'s Privacy', 81 | text: 'Our platform is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13.' 82 | }, 83 | { 84 | heading: 'Changes to This Privacy Policy', 85 | text: 'We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date.' 86 | }, 87 | { 88 | heading: 'Contact Us', 89 | text: 'If you have any questions about this Privacy Policy, please contact us at privacy@Code4U.example.com.' 90 | } 91 | ], 92 | lastUpdated: new Date().toISOString().split('T')[0] 93 | } 94 | 95 | /** 96 | * Fetch legal document content from Firestore 97 | * @param {string} documentId - 'terms_of_service' or 'privacy_policy' 98 | * @returns {Promise} - The document data 99 | */ 100 | export async function getLegalDocument(documentId) { 101 | try { 102 | const docSnap = await getDoc(doc(db, LEGAL_COLLECTION, documentId)) 103 | if (docSnap.exists()) { 104 | return docSnap.data() 105 | } else { 106 | console.warn(`Legal document ${documentId} not found in Firestore, using default`) 107 | // Return default content if document doesn't exist in Firestore 108 | return documentId === 'terms_of_service' ? DEFAULT_TERMS : DEFAULT_PRIVACY 109 | } 110 | } catch (error) { 111 | console.warn('Error accessing Firestore, using default legal content:', error) 112 | // Return default content on error 113 | return documentId === 'terms_of_service' ? DEFAULT_TERMS : DEFAULT_PRIVACY 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/views/LeaderboardView.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 220 | -------------------------------------------------------------------------------- /src/views/LevelDetailView.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 219 | -------------------------------------------------------------------------------- /src/components/ActivityFeed.vue: -------------------------------------------------------------------------------- 1 | 226 | 227 | 282 | -------------------------------------------------------------------------------- /src/views/JourneysView.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 233 | -------------------------------------------------------------------------------- /src/views/LevelsView.vue: -------------------------------------------------------------------------------- 1 | 131 | 132 | 241 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 203 | -------------------------------------------------------------------------------- /src/views/RegisterView.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 274 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | import HtmlLearnView from '../views/learn/HtmlLearnView.vue' 4 | import CssLearnView from '../views/learn/CssLearnView.vue' 5 | import JavaScriptLearnView from '../views/learn/JavaScriptLearnView.vue' 6 | import { auth } from '../firebase' 7 | import { onAuthStateChanged } from 'firebase/auth' 8 | import { requireAdmin, requireCreatorOrAdmin } from './adminGuard' 9 | 10 | // Keep track of the auth initialization state 11 | let authInitialized = false 12 | let currentUser = null 13 | 14 | // Initialize auth state tracking 15 | onAuthStateChanged(auth, (user) => { 16 | currentUser = user 17 | authInitialized = true 18 | }) 19 | 20 | // Navigation guard to check authentication - waits for initialization 21 | const requireAuth = (to, from, next) => { 22 | // If auth isn't initialized yet, wait briefly before checking 23 | if (!authInitialized) { 24 | const waitForAuthInit = () => { 25 | // Check again after a small delay 26 | if (authInitialized) { 27 | // Now we can properly check the auth state 28 | if (currentUser) { 29 | next() // User is logged in, proceed 30 | } else { 31 | next({ name: 'login', query: { redirect: to.fullPath } }) // Not logged in, redirect 32 | } 33 | } else { 34 | // Still not initialized, wait a bit longer 35 | setTimeout(waitForAuthInit, 50) 36 | } 37 | } 38 | // Start the waiting process 39 | waitForAuthInit() 40 | } else { 41 | // Auth is already initialized, check current state 42 | if (currentUser) { 43 | next() // User is logged in, proceed 44 | } else { 45 | next({ name: 'login', query: { redirect: to.fullPath } }) // Not logged in, redirect 46 | } 47 | } 48 | } 49 | 50 | const router = createRouter({ 51 | history: createWebHistory(import.meta.env.BASE_URL), 52 | routes: [ 53 | { 54 | path: '/', 55 | name: 'home', 56 | component: HomeView, 57 | }, 58 | { 59 | path: '/journeys', 60 | name: 'journeys', 61 | component: () => import('../views/JourneysView.vue'), 62 | meta: { 63 | title: 'Journeys - Code4U' 64 | } 65 | }, 66 | { 67 | path: '/learning-paths', 68 | redirect: '/journeys' 69 | }, 70 | { 71 | path: '/journey/:id', 72 | name: 'journey-detail', 73 | component: () => import('../views/JourneyDetailView.vue'), 74 | props: true, 75 | //beforeEnter: requireAuth, 76 | meta: { 77 | title: 'Journey Details - Code4U' 78 | } 79 | }, 80 | { 81 | path: '/learning-path/:id', 82 | redirect: to => { 83 | // Redirect old path format to new format 84 | return { path: `/journey/${to.params.id}/levels` } 85 | } 86 | }, 87 | { 88 | path: '/levels', 89 | name: 'levels', 90 | component: () => import('../views/LevelsView.vue'), 91 | beforeEnter: requireAuth 92 | }, 93 | { 94 | path: '/journey/:pathId/levels', 95 | name: 'journey-levels', 96 | component: () => import('../views/LevelsView.vue'), 97 | props: true, 98 | beforeEnter: requireAuth, 99 | meta: { 100 | title: 'Journey Levels - Code4U' 101 | } 102 | }, 103 | { 104 | path: '/learning-path/:pathId/levels', 105 | redirect: to => { 106 | return { path: `/journey/${to.params.pathId}/levels` } 107 | } 108 | }, 109 | { 110 | path: '/level/:id', 111 | name: 'level-detail', 112 | component: () => import('../views/LevelDetailView.vue'), 113 | beforeEnter: requireAuth, 114 | props: true 115 | }, 116 | { 117 | path: '/game/:levelId', 118 | name: 'game', 119 | component: () => import('../views/GameView.vue'), 120 | beforeEnter: requireAuth, 121 | props: true 122 | }, 123 | { 124 | path: '/leaderboard', 125 | name: 'leaderboard', 126 | component: () => import('../views/LeaderboardView.vue') 127 | }, 128 | { 129 | path: '/login', 130 | name: 'login', 131 | component: () => import('../views/LoginView.vue') 132 | }, 133 | // Register route removed - using Google SSO only 134 | { 135 | path: '/profile', 136 | name: 'profile', 137 | component: () => import('../views/ProfileView.vue'), 138 | beforeEnter: requireAuth 139 | }, 140 | { 141 | path: '/profile/edit', 142 | name: 'edit-profile', 143 | component: () => import('../views/EditProfileView.vue'), 144 | beforeEnter: requireAuth 145 | }, 146 | { 147 | path: '/about', 148 | name: 'about', 149 | component: () => import('../views/AboutView.vue') 150 | }, 151 | // Learning routes 152 | { 153 | path: '/learn/html', 154 | name: 'learn-html', 155 | component: HtmlLearnView, 156 | meta: { 157 | title: 'Learn HTML - Code4U' 158 | } 159 | }, 160 | { 161 | path: '/learn/css', 162 | name: 'learn-css', 163 | component: CssLearnView, 164 | meta: { 165 | title: 'Learn CSS - Code4U' 166 | } 167 | }, 168 | { 169 | path: '/learn/javascript', 170 | name: 'learn-javascript', 171 | component: JavaScriptLearnView, 172 | meta: { 173 | title: 'Learn JavaScript - Code4U' 174 | } 175 | }, 176 | { 177 | path: '/terms', 178 | name: 'terms', 179 | component: () => import('../views/TermsOfServiceView.vue'), 180 | }, 181 | { 182 | path: '/privacy', 183 | name: 'privacy', 184 | component: () => import('../views/PrivacyPolicyView.vue'), 185 | }, 186 | { 187 | path: '/activities', 188 | name: 'activities', 189 | component: () => import('../views/ActivitiesView.vue'), 190 | }, 191 | // Admin routes 192 | { 193 | path: '/admin', 194 | name: 'admin-dashboard', 195 | component: () => import('../views/admin/AdminDashboard.vue'), 196 | beforeEnter: requireCreatorOrAdmin, // Allow creators to access the dashboard 197 | meta: { 198 | title: 'Admin Dashboard - Code4U' 199 | } 200 | }, 201 | { 202 | path: '/admin/journeys', 203 | name: 'admin-journeys', 204 | component: () => import('../views/admin/journeys/JourneyList.vue'), 205 | beforeEnter: requireCreatorOrAdmin, // Allow creators to manage journeys 206 | meta: { 207 | title: 'Journey Management - Code4U Admin' 208 | } 209 | }, 210 | { 211 | path: '/admin/badges', 212 | name: 'admin-badges', 213 | component: () => import('../views/admin/badges/BadgeList.vue'), 214 | beforeEnter: requireCreatorOrAdmin, // Allow creators to manage badges 215 | meta: { 216 | title: 'Badge Management - Code4U Admin' 217 | } 218 | }, 219 | { 220 | path: '/admin/users', 221 | name: 'admin-users', 222 | component: () => import('../views/admin/users/UserList.vue'), 223 | beforeEnter: requireAdmin, 224 | meta: { 225 | title: 'User Management - Code4U Admin' 226 | } 227 | }, 228 | { 229 | path: '/admin/users/new', 230 | name: 'admin-create-user', 231 | component: () => import('../views/admin/users/UserForm.vue'), 232 | beforeEnter: requireAdmin, 233 | meta: { 234 | title: 'Create User - Code4U Admin' 235 | } 236 | }, 237 | { 238 | path: '/admin/users/:id', 239 | name: 'admin-edit-user', 240 | component: () => import('../views/admin/users/UserForm.vue'), 241 | props: true, 242 | beforeEnter: requireAdmin, 243 | meta: { 244 | title: 'Edit User - Code4U Admin' 245 | } 246 | }, 247 | { 248 | path: '/admin/badges/new', 249 | name: 'admin-create-badge', 250 | component: () => import('../views/admin/badges/BadgeForm.vue'), 251 | beforeEnter: requireCreatorOrAdmin, // Allow creators to create badges 252 | meta: { 253 | title: 'Create Badge - Code4U Admin' 254 | } 255 | }, 256 | { 257 | path: '/admin/badges/:id/edit', 258 | name: 'admin-edit-badge', 259 | component: () => import('../views/admin/badges/BadgeForm.vue'), 260 | props: true, 261 | beforeEnter: requireCreatorOrAdmin, // Allow creators to edit badges 262 | meta: { 263 | title: 'Edit Badge - Code4U Admin' 264 | } 265 | }, 266 | { 267 | path: '/admin/journeys/new', 268 | name: 'admin-create-journey', 269 | component: () => import('../views/admin/journeys/JourneyForm.vue'), 270 | beforeEnter: requireCreatorOrAdmin, // Allow creators to create journeys 271 | meta: { 272 | title: 'Create Journey - Code4U Admin' 273 | } 274 | }, 275 | { 276 | path: '/admin/journeys/:id/edit', 277 | name: 'admin-edit-journey', 278 | component: () => import('../views/admin/journeys/JourneyForm.vue'), 279 | props: true, 280 | beforeEnter: requireCreatorOrAdmin, // Allow creators to edit journeys 281 | meta: { 282 | title: 'Edit Journey - Code4U Admin' 283 | } 284 | }, 285 | { 286 | path: '/admin/levels', 287 | name: 'admin-levels', 288 | component: () => import('../views/admin/journeys/LevelList.vue'), 289 | beforeEnter: requireCreatorOrAdmin, // Allow creators to manage levels 290 | meta: { 291 | title: 'Level Management - Code4U Admin' 292 | } 293 | }, 294 | { 295 | path: '/admin/levels/new', 296 | name: 'admin-create-level', 297 | component: () => import('../views/admin/journeys/LevelForm.vue'), 298 | beforeEnter: requireCreatorOrAdmin, // Allow creators to create levels 299 | meta: { 300 | title: 'Create Level - Code4U Admin' 301 | } 302 | }, 303 | { 304 | path: '/admin/levels/:id/edit', 305 | name: 'admin-edit-level', 306 | component: () => import('../views/admin/journeys/LevelForm.vue'), 307 | props: true, 308 | beforeEnter: requireCreatorOrAdmin, // Allow creators to edit levels 309 | meta: { 310 | title: 'Edit Level - Code4U Admin' 311 | } 312 | }, 313 | { 314 | path: '/admin/feedback', 315 | name: 'admin-feedback-list', 316 | component: () => import('../views/admin/FeedbackList.vue'), 317 | beforeEnter: requireAdmin, 318 | meta: { 319 | title: 'Feedback Management - Code4U Admin' 320 | } 321 | }, 322 | { 323 | path: '/admin/feedback/:id', 324 | name: 'admin-feedback-detail', 325 | component: () => import('../views/admin/FeedbackDetail.vue'), 326 | beforeEnter: requireAdmin, 327 | props: true, 328 | meta: { 329 | title: 'Feedback Details - Code4U Admin' 330 | } 331 | }, 332 | ], 333 | }) 334 | 335 | export default router 336 | -------------------------------------------------------------------------------- /docs/ADMINUI.md: -------------------------------------------------------------------------------- 1 | # Code4U Admin UI Documentation 2 | 3 | This document provides a complete guide to the Admin UI implementation for Code4U, which allows administrators to manage journeys, levels, tasks, and other learning content. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Overview](#overview) 8 | 2. [Data Model](#data-model) 9 | 3. [Component Structure](#component-structure) 10 | 4. [User Interface](#user-interface) 11 | 5. [Access Control](#access-control) 12 | 6. [Implementation Details](#implementation-details) 13 | 7. [Future Enhancements](#future-enhancements) 14 | 15 | ## Overview 16 | 17 | The Code4U Admin UI provides a comprehensive interface for managing the application's learning content. It enables administrators to: 18 | 19 | - Create, read, update, and delete journeys 20 | - Create, read, update, and delete levels with their associated tasks 21 | - View and manage the relationships between journeys and levels 22 | - Track administration activities 23 | 24 | This interface follows the same data model as the main application, ensuring consistent data across the platform. 25 | 26 | ## Data Model 27 | 28 | The Admin UI works with three main data types, stored in Firebase Firestore: 29 | 30 | ### Journey Schema 31 | 32 | | Field Name | Type | Description | 33 | |------------------|------------|----------------------------------------------------------| 34 | | `id` | `string` | Unique identifier (e.g., `"web-fundamentals"`) | 35 | | `title` | `string` | Display title | 36 | | `description` | `string` | Detailed description | 37 | | `icon` | `string` | Emoji icon | 38 | | `difficulty` | `string` | Difficulty level (Beginner, Intermediate, Advanced) | 39 | | `estimatedHours` | `number` | Estimated completion time | 40 | | `levelIds` | `string[]` | Array of level IDs included in this journey | 41 | | `prerequisites` | `string[]` | Array of prerequisite journey IDs | 42 | | `badgeId` | `string` | Badge earned on completion | 43 | | `categories` | `string[]` | Technology categories (HTML, CSS, JavaScript, etc.) | 44 | | `tags` | `string[]` | Searchable tags | 45 | | `featured` | `boolean` | Featured on homepage | 46 | | `order` | `number` | Display order | 47 | 48 | ### Level Schema 49 | 50 | | Field Name | Type | Description | 51 | |-------------------------|--------------------------------------|-----------------------------------------------------------| 52 | | `id` | `string` | Unique identifier (e.g., `"level-1"`) | 53 | | `number` | `number` | Sequential number for ordering | 54 | | `title` | `string` | Display title | 55 | | `description` | `string` | Detailed description | 56 | | `category` | `string` | Technology category (HTML, CSS, JavaScript) | 57 | | `difficulty` | `string` | Difficulty level | 58 | | `pointsToEarn` | `number` | Points awarded on completion | 59 | | `estimatedTime` | `string` | Estimated completion time | 60 | | `prerequisites` | `string[]` | Array of prerequisite level names | 61 | | `learningObjectives` | `string[]` | Array of learning objectives | 62 | | `realWorldApplications` | `string[]` | Array of practical applications | 63 | | `references` | `{ title: string, url: string }[]` | Array of reference resources | 64 | | `tags` | `string[]` | Searchable tags | 65 | | `tasks` | `Task[]` | Array of task objects | 66 | 67 | ### Task Schema 68 | 69 | | Field Name | Type | Description | 70 | |------------------|----------|-----------------------------------------------------------| 71 | | `id` | `string` | Unique identifier within the level (e.g., `"task1"`) | 72 | | `title` | `string` | Display title | 73 | | `description` | `string` | Detailed instructions | 74 | | `initialCode` | `string` | Starting code for the challenge | 75 | | `solution` | `string` | Solution pattern to check for | 76 | | `expectedOutput` | `string` | Success message | 77 | | `errorHint` | `string` | Error guidance for incorrect solutions | 78 | 79 | ## Component Structure 80 | 81 | The Admin UI consists of the following Vue components: 82 | 83 | ### Dashboard Components 84 | 85 | - **AdminDashboard.vue**: Main entry point for the admin interface, showing statistics and quick access buttons 86 | 87 | ### Journey Management 88 | 89 | - **JourneyList.vue**: Lists all journeys with actions (view, edit, delete) 90 | - **JourneyForm.vue**: Form for creating and editing journeys, including level selection 91 | 92 | ### Level Management 93 | 94 | - **LevelList.vue**: Lists all levels with actions (view, edit, delete) 95 | - **LevelForm.vue**: Form for creating and editing levels, including task management 96 | 97 | ## User Interface 98 | 99 | ### Admin Dashboard 100 | 101 | The dashboard provides: 102 | 103 | - Overview of content metrics (journey count, level count, badge count) 104 | - Quick action buttons to create new content 105 | - Recent admin activity feed 106 | 107 | ### Journey Management 108 | 109 | The journey management interface allows administrators to: 110 | 111 | - View all journeys in a table format with key information 112 | - Create new journeys with all required fields 113 | - Edit existing journeys, including adding/removing levels 114 | - Delete journeys (with confirmation) 115 | 116 | ### Level Management 117 | 118 | The level management interface allows administrators to: 119 | 120 | - View all levels with their category, difficulty, and task count 121 | - Create new levels with a comprehensive form for all fields 122 | - Edit existing levels, including adding/removing tasks 123 | - Delete levels (with confirmation and warning about journey dependencies) 124 | 125 | ### Task Management 126 | 127 | Tasks are managed within the level form, allowing administrators to: 128 | 129 | - Add multiple tasks to a level 130 | - Configure each task with title, description, initial code, and solution 131 | - Set expected output and error hints for the learning experience 132 | 133 | ## Access Control 134 | 135 | The Admin UI is protected by an authentication guard that ensures only users with the admin role can access these pages: 136 | 137 | 1. When a user navigates to an admin route, the `requireAdmin` navigation guard is triggered 138 | 2. The guard checks if the user is authenticated 139 | 3. If authenticated, it verifies the user has the admin role in Firestore 140 | 4. If the user lacks admin privileges, they are redirected to the home page 141 | 142 | The admin role is stored in the user's document in Firestore: 143 | 144 | ```javascript 145 | { 146 | uid: 'user-id', 147 | role: 'admin', 148 | // other user properties 149 | } 150 | ``` 151 | 152 | ## Implementation Details 153 | 154 | ### Router Configuration 155 | 156 | The Admin UI routes are defined in `router/index.js`: 157 | 158 | ```javascript 159 | // Admin routes 160 | { 161 | path: '/admin', 162 | name: 'admin-dashboard', 163 | component: () => import('../views/admin/AdminDashboard.vue'), 164 | beforeEnter: requireAdmin, 165 | meta: { 166 | title: 'Admin Dashboard - Code4U' 167 | } 168 | }, 169 | // Journey management routes 170 | { 171 | path: '/admin/journeys', 172 | name: 'admin-journey-list', 173 | component: () => import('../views/admin/journeys/JourneyList.vue'), 174 | beforeEnter: requireAdmin 175 | }, 176 | // Additional routes for creating/editing journeys and levels 177 | ``` 178 | 179 | ### Firebase Integration 180 | 181 | The Admin UI uses Firebase Firestore for data management: 182 | 183 | - Read operations use `getDocs` and `getDoc` to retrieve data 184 | - Write operations use `setDoc` for creating/updating documents 185 | - Delete operations use `deleteDoc` to remove documents 186 | 187 | Example of saving a journey: 188 | 189 | ```javascript 190 | await setDoc(doc(db, 'journeys', journey.value.id), journey.value); 191 | ``` 192 | 193 | ### Form Validation 194 | 195 | The Admin UI implements basic form validation to ensure data integrity: 196 | 197 | - Required fields must be filled in before submission 198 | - IDs are formatted appropriately (lowercase, no spaces) 199 | - Arrays are cleaned of empty values before saving 200 | - Related IDs are validated for existence 201 | 202 | ## Future Enhancements 203 | 204 | Potential improvements for the Admin UI include: 205 | 206 | 1. **Badge Management**: Add dedicated interface for creating and managing achievement badges 207 | 2. **User Management**: Interface for assigning admin roles to users 208 | 3. **Educational Content Management**: Interface for creating and editing interactive HTML, CSS, and JavaScript tutorials accessible via the Learn menu 209 | 4. **Bulk Operations**: Tools for batch editing or importing/exporting data 210 | 5. **Preview Mode**: Allow viewing content as it would appear to students 211 | 6. **Version History**: Track changes to learning content 212 | 7. **Media Management**: Tools for uploading and managing images or videos for levels 213 | 8. **Analytics Dashboard**: View student progress and engagement metrics 214 | 9. **Multi-language Support**: Interface for managing content in multiple languages 215 | 216 | ## Usage Guide 217 | 218 | To access the Admin UI: 219 | 220 | 1. Navigate to `/admin` in the application 221 | 2. Authenticate with an account that has the admin role 222 | 3. Use the dashboard to navigate to journey or level management 223 | 4. Create, edit, or delete content as needed 224 | 225 | When creating a new journey or level, ensure all required fields are completed and that the content follows the established patterns for optimal student experience. 226 | --------------------------------------------------------------------------------