diff --git a/file-converter/README.md b/file-converter/README.md new file mode 100644 index 0000000..3682e98 --- /dev/null +++ b/file-converter/README.md @@ -0,0 +1,85 @@ +# File Converter — Self-Hosted Template + +A self-hosted file conversion tool for Codesphere workspaces. No uploads to third parties — your files stay on your server. + +## Features + +- **Image Conversion**: Convert between JPG, PNG, WebP, GIF, and AVIF formats +- **Batch Processing**: Convert up to 10 files at once +- **Authentication**: Token-based access control +- **Self-Hosted**: All processing happens locally on your Codesphere workspace +- **Privacy-First**: No third-party API calls, no data leaves your server + +## Supported Conversions + +| From | To | +|------|-----| +| PNG | JPG, WebP, AVIF | +| JPG | PNG, WebP | +| WebP | JPG, PNG | +| GIF | PNG | + +## Deployment on Codesphere + +1. Fork this template +2. Create a new Codesphere workspace from your fork +3. Set up the CI pipeline (auto-installed on Codesphere) +4. Run the `prepare` stage to install dependencies +5. Run the `run` stage to start the server +6. Click "Open deployment" to access the tool + +## Local Development + +```bash +npm install +PORT=3000 node server.js +``` + +Open http://localhost:3000 to use the converter. + +## Authentication + +All API endpoints require the `X-Auth-Token` header. + +**Demo token**: `codesphere-demo-token-2024` + +## API Endpoints + +### GET /health +Health check endpoint. + +### GET /formats +Returns supported conversion formats and pairs. + +### POST /convert/image +Convert image files. + +**Headers**: +- `X-Auth-Token`: Required authentication token + +**Body** (multipart/form-data): +- `format`: Target format (jpg, png, webp, gif, avif) +- `files`: One or more image files + +**Example**: +```bash +curl -X POST http://localhost:3000/convert/image \ + -H "X-Auth-Token: codesphere-demo-token-2024" \ + -F "format=webp" \ + -F "files=@image.png" +``` + +## Security Notes + +- Authentication tokens are configured in `src/middleware/auth.js` +- For production, generate strong random tokens and restrict access +- File size limit: 50MB per file +- Supported MIME types: image/jpeg, image/png, image/gif, image/webp, image/avif, application/pdf + +## Blog Post + +[Write a blog article about this project on Dev.To, Medium, or personal blog and link it here] + +--- + +Built for Codesphere Templates — deploy complex apps in minutes. \ No newline at end of file diff --git a/file-converter/ci.yml b/file-converter/ci.yml new file mode 100644 index 0000000..2eef4f8 --- /dev/null +++ b/file-converter/ci.yml @@ -0,0 +1,14 @@ +prepare: + steps: + - name: "Install dependencies" + command: "npm install" + +test: + steps: + - name: "Check server starts" + command: "node server.js & sleep 3 && curl -s http://localhost:3000/health && pkill -f 'node server.js'" + +run: + steps: + - name: "Start server" + command: "PORT=3000 node server.js" \ No newline at end of file diff --git a/file-converter/file-converter.webp b/file-converter/file-converter.webp new file mode 100644 index 0000000..e69de29 diff --git a/file-converter/metadata.json b/file-converter/metadata.json new file mode 100644 index 0000000..5234971 --- /dev/null +++ b/file-converter/metadata.json @@ -0,0 +1,10 @@ +{ + "Workspace": "paid", + "Links": { + "Node.js": "https://nodejs.org/", + "Express": "https://expressjs.com/" + }, + "Categories": ["Utility", "Node.js"], + "Contributors": ["opencode-MiniMaxM27"], + "Title": "File Converter" +} \ No newline at end of file diff --git a/file-converter/package.json b/file-converter/package.json new file mode 100644 index 0000000..7f91d45 --- /dev/null +++ b/file-converter/package.json @@ -0,0 +1,17 @@ +{ + "name": "file-converter", + "version": "1.0.0", + "description": "Self-hosted file converter tool for Codesphere", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "multer": "^1.4.5-lts.1", + "sharp": "^0.33.0", + "pdf-lib": "^1.17.1", + "uuid": "^9.0.0" + } +} \ No newline at end of file diff --git a/file-converter/public/.gitkeep b/file-converter/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/file-converter/server.js b/file-converter/server.js new file mode 100644 index 0000000..57a32d0 --- /dev/null +++ b/file-converter/server.js @@ -0,0 +1,254 @@ +const express = require('express') +const multer = require('multer') +const path = require('path') +const { v4: uuidv4 } = require('uuid') +const { authMiddleware } = require('./src/middleware/auth') +const { convertImage } = require('./src/utils/imageConverter') + +const app = express() +const PORT = process.env.PORT || 3000 + +const storage = multer.memoryStorage() +const upload = multer({ + storage, + limits: { fileSize: 50 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + const allowedTypes = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/avif', + 'application/pdf', + ] + if (allowedTypes.includes(file.mimetype)) { + cb(null, true) + } else { + cb(new Error(`Unsupported file type: ${file.mimetype}`)) + } + }, +}) + +app.use(express.json()) +app.use(express.static(path.join(__dirname, 'public'))) + +app.get('/health', (req, res) => { + res.json({ status: 'ok', service: 'file-converter', timestamp: new Date().toISOString() }) +}) + +app.get('/formats', (req, res) => { + res.json({ + image: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif'], + supportedPairs: [ + { from: 'png', to: 'jpg', description: 'PNG to JPG conversion' }, + { from: 'jpg', to: 'png', description: 'JPG to PNG conversion' }, + { from: 'jpg', to: 'webp', description: 'JPG to WebP conversion' }, + { from: 'png', to: 'webp', description: 'PNG to WebP conversion' }, + { from: 'gif', to: 'png', description: 'GIF to PNG conversion' }, + { from: 'webp', to: 'jpg', description: 'WebP to JPG conversion' }, + ], + }) +}) + +app.post('/convert/image', authMiddleware, upload.array('files', 10), async (req, res) => { + try { + const { format } = req.body + if (!format) { + return res.status(400).json({ error: 'Missing target format in request body' }) + } + + const targetFormat = format.toLowerCase().replace('.', '') + const results = [] + + for (const file of req.files) { + try { + const converted = await convertImage(file.buffer, targetFormat) + const outputName = `${path.parse(file.originalname).name}.${targetFormat}` + results.push({ + originalName: file.originalname, + convertedName: outputName, + targetFormat, + success: true, + size: converted.length, + data: converted.toString('base64'), + }) + } catch (err) { + results.push({ + originalName: file.originalname, + targetFormat, + success: false, + error: err.message, + }) + } + } + + res.json({ results, successful: results.filter(r => r.success).length }) + } catch (err) { + res.status(500).json({ error: err.message }) + } +}) + +app.get('/', (req, res) => { + res.send(` + + + + + + File Converter — Self-Hosted + + + +
+
+

File Converter

+

Self-hosted • No uploads to third parties • Full privacy

+
+ +
+ + + Use: codesphere-demo-token-2024 +
+ +
+
+ + +
+ +
+ +

Drop images here or click to select
JPG, PNG, WebP, GIF, AVIF supported

+
+ +
+ + +
+ + + +
+ API Usage:

+ POST /convert/image — Send files with X-Auth-Token header

+ GET /formats — View supported conversion formats

+ GET /health — Health check endpoint +
+
+ + + + + `) +}) + +app.use((err, req, res, next) => { + res.status(500).json({ error: err.message }) +}) + +app.listen(PORT, '0.0.0.0', () => { + console.log(`File Converter running on port ${PORT}`) +}) \ No newline at end of file diff --git a/file-converter/src/middleware/.gitkeep b/file-converter/src/middleware/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/file-converter/src/middleware/auth.js b/file-converter/src/middleware/auth.js new file mode 100644 index 0000000..23b5625 --- /dev/null +++ b/file-converter/src/middleware/auth.js @@ -0,0 +1,15 @@ +const authTokens = new Set([ + 'codesphere-demo-token-2024', + 'demo-access-xyz789', + 'test-user-abc123', +]) + +function authMiddleware(req, res, next) { + const token = req.headers['x-auth-token'] + if (!token || !authTokens.has(token)) { + return res.status(401).json({ error: 'Unauthorized. Provide valid X-Auth-Token header.' }) + } + next() +} + +module.exports = { authMiddleware, authTokens } \ No newline at end of file diff --git a/file-converter/src/routes/.gitkeep b/file-converter/src/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/file-converter/src/utils/.gitkeep b/file-converter/src/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/file-converter/src/utils/imageConverter.js b/file-converter/src/utils/imageConverter.js new file mode 100644 index 0000000..977d3de --- /dev/null +++ b/file-converter/src/utils/imageConverter.js @@ -0,0 +1,59 @@ +const sharp = require('sharp') +const path = require('path') +const fs = require('fs') + +async function convertImage(inputBuffer, targetFormat) { + const supportedFormats = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif'] + + if (!supportedFormats.includes(targetFormat.toLowerCase())) { + throw new Error(`Unsupported format: ${targetFormat}. Supported: ${supportedFormats.join(', ')}`) + } + + let pipeline = sharp(inputBuffer) + + switch (targetFormat.toLowerCase()) { + case 'jpg': + case 'jpeg': + pipeline = pipeline.jpeg({ quality: 90 }) + break + case 'png': + pipeline = pipeline.png() + break + case 'webp': + pipeline = pipeline.webp({ quality: 90 }) + break + case 'gif': + pipeline = pipeline.gif() + break + case 'avif': + pipeline = pipeline.avif({ quality: 80 }) + break + } + + return pipeline.toBuffer() +} + +async function convertImageBatch(files, targetFormat) { + const results = [] + for (const file of files) { + try { + const converted = await convertImage(file.buffer, targetFormat) + results.push({ + originalName: file.originalname, + targetFormat, + success: true, + size: converted.length, + }) + } catch (err) { + results.push({ + originalName: file.originalname, + targetFormat, + success: false, + error: err.message, + }) + } + } + return results +} + +module.exports = { convertImage, convertImageBatch } \ No newline at end of file diff --git a/file-converter/start.sh b/file-converter/start.sh new file mode 100644 index 0000000..d4a9703 --- /dev/null +++ b/file-converter/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +echo "Installing dependencies..." +npm install + +echo "Starting File Converter server..." +PORT=3000 node server.js \ No newline at end of file diff --git a/laravel/.env b/laravel/.env new file mode 100644 index 0000000..5266c50 --- /dev/null +++ b/laravel/.env @@ -0,0 +1,8 @@ +APP_NAME="Laravel Quiz App" +APP_ENV=local +APP_KEY=base64:MHMyGZ7hZG8z4V5j6F3qW1pL8n2R4s9X0yB7cE6dA= +APP_DEBUG=true +APP_URL=http://localhost:3000 + +DB_CONNECTION=sqlite +DB_DATABASE=/tmp/templates-fork/laravel/database.sqlite \ No newline at end of file diff --git a/laravel/README.md b/laravel/README.md new file mode 100644 index 0000000..4ce2f99 --- /dev/null +++ b/laravel/README.md @@ -0,0 +1,33 @@ +# Laravel Quiz App Template + +A basic quiz application built with Laravel for Codesphere workspaces. + +## Features + +- Create and manage quizzes +- Add multiple-choice questions to each quiz +- Take quizzes and see your score +- SQLite database (no external DB required) + +## Deployment on Codesphere + +1. Fork this template +2. In Codesphere, create a new workspace from your fork +3. Set up the CI pipeline (it will auto-run on Codesphere) +4. Click "Run" stage to start the dev server +5. Click "Open deployment" to access the app + +## Local Development + +```bash +composer install +touch database/database.sqlite +php artisan migrate +php artisan serve --host=0.0.0.0 --port=3000 +``` + +## Tech Stack + +- PHP 8.1+ +- Laravel 10 +- SQLite (file-based, no setup required) \ No newline at end of file diff --git a/laravel/app/Http/Controllers/Controller.php b/laravel/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..376e5b7 --- /dev/null +++ b/laravel/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +validate([ + 'quiz_id' => 'required|exists:quizzes,id', + 'question_text' => 'required|string', + 'options' => 'required|array|min:2', + 'correct_answer' => 'required|string', + ]); + + Question::create($validated); + return redirect()->route('quizzes.show', $validated['quiz_id'])->with('success', 'Question added!'); + } + + public function destroy(Question $question) + { + $quiz_id = $question->quiz_id; + $question->delete(); + return redirect()->route('quizzes.show', $quiz_id)->with('success', 'Question deleted!'); + } +} \ No newline at end of file diff --git a/laravel/app/Http/Controllers/QuizController.php b/laravel/app/Http/Controllers/QuizController.php new file mode 100644 index 0000000..3828f8d --- /dev/null +++ b/laravel/app/Http/Controllers/QuizController.php @@ -0,0 +1,55 @@ +latest()->paginate(10); + return view('quizzes.index', compact('quizzes')); + } + + public function create() + { + return view('quizzes.create'); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + ]); + + Quiz::create($validated); + return redirect()->route('quizzes.index')->with('success', 'Quiz created!'); + } + + public function show(Quiz $quiz) + { + $quiz->load('questions'); + return view('quizzes.show', compact('quiz')); + } + + public function take(Request $request, Quiz $quiz) + { + $answers = $request->except(['_token']); + $quiz->load('questions'); + + $score = 0; + $total = $quiz->questions->count(); + + foreach ($quiz->questions as $question) { + if (isset($answers['question_' . $question->id]) && + $answers['question_' . $question->id] === $question->correct_answer) { + $score++; + } + } + + return view('quizzes.result', compact('quiz', 'score', 'total')); + } +} \ No newline at end of file diff --git a/laravel/app/Models/Question.php b/laravel/app/Models/Question.php new file mode 100644 index 0000000..c70384c --- /dev/null +++ b/laravel/app/Models/Question.php @@ -0,0 +1,19 @@ + 'array', + ]; + + public function quiz() + { + return $this->belongsTo(Quiz::class); + } +} \ No newline at end of file diff --git a/laravel/app/Models/Quiz.php b/laravel/app/Models/Quiz.php new file mode 100644 index 0000000..f601273 --- /dev/null +++ b/laravel/app/Models/Quiz.php @@ -0,0 +1,15 @@ +hasMany(Question::class); + } +} \ No newline at end of file diff --git a/laravel/app/Providers/AppServiceProvider.php b/laravel/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..a4254c4 --- /dev/null +++ b/laravel/app/Providers/AppServiceProvider.php @@ -0,0 +1,18 @@ +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +$kernel->terminate($input, $status); + +exit($status); \ No newline at end of file diff --git a/laravel/bootstrap/app.php b/laravel/bootstrap/app.php new file mode 100644 index 0000000..3d8b5f7 --- /dev/null +++ b/laravel/bootstrap/app.php @@ -0,0 +1,18 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); \ No newline at end of file diff --git a/laravel/ci.yml b/laravel/ci.yml new file mode 100644 index 0000000..304ec06 --- /dev/null +++ b/laravel/ci.yml @@ -0,0 +1,14 @@ +prepare: + steps: + - name: "Install dependencies" + command: "composer install --no-interaction" + +test: + steps: + - name: "Run migrations" + command: "php artisan migrate --force 2>&1 || true" + +run: + steps: + - name: "Start Laravel dev server" + command: "php artisan serve --host=0.0.0.0 --port=3000" \ No newline at end of file diff --git a/laravel/composer.json b/laravel/composer.json new file mode 100644 index 0000000..7d520fa --- /dev/null +++ b/laravel/composer.json @@ -0,0 +1,51 @@ +{ + "name": "codesphere/laravel-quiz-app", + "description": "Laravel Quiz App Template for Codesphere", + "type": "project", + "require": { + "php": "^8.1", + "laravel/framework": "^10.0", + "guzzlehttp/guzzle": "^7.2" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^7.0", + "phpunit/phpunit": "^10.1" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ] + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} \ No newline at end of file diff --git a/laravel/config/app.php b/laravel/config/app.php new file mode 100644 index 0000000..193f218 --- /dev/null +++ b/laravel/config/app.php @@ -0,0 +1,41 @@ + env('APP_NAME', 'Laravel Quiz App'), + 'env' => env('APP_ENV', 'production'), + 'debug' => (bool) env('APP_DEBUG', false), + 'url' => env('APP_URL', 'http://localhost'), + 'timezone' => 'UTC', + 'locale' => 'en', + 'fallback_locale' => 'en', + 'faker_locale' => 'en_US', + 'key' => env('APP_KEY'), + 'cipher' => 'AES-256-CBC', + 'maintenance' => ['driver' => 'file'], + 'providers' => [ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + App\Providers\AppServiceProvider::class, + ], + 'aliases' => Illuminate\Support\Facades\Facade::defaultAliases()->toArray(), +]; \ No newline at end of file diff --git a/laravel/config/database.php b/laravel/config/database.php new file mode 100644 index 0000000..7305081 --- /dev/null +++ b/laravel/config/database.php @@ -0,0 +1,18 @@ + 'sqlite', + 'connections' => [ + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => database_path('database.sqlite'), + 'prefix' => '', + 'foreign_key_constraints' => true, + ], + ], + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], +]; \ No newline at end of file diff --git a/laravel/database/migrations/2024_01_01_000000_create_quizzes_questions_tables.php b/laravel/database/migrations/2024_01_01_000000_create_quizzes_questions_tables.php new file mode 100644 index 0000000..73b9156 --- /dev/null +++ b/laravel/database/migrations/2024_01_01_000000_create_quizzes_questions_tables.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->timestamps(); + }); + + Schema::create('questions', function (Blueprint $table) { + $table->id(); + $table->foreignId('quiz_id')->constrained()->onDelete('cascade'); + $table->text('question_text'); + $table->json('options'); + $table->string('correct_answer'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('questions'); + Schema::dropIfExists('quizzes'); + } +}; \ No newline at end of file diff --git a/laravel/laravel.webp b/laravel/laravel.webp new file mode 100644 index 0000000..e69de29 diff --git a/laravel/metadata.json b/laravel/metadata.json new file mode 100644 index 0000000..278efa3 --- /dev/null +++ b/laravel/metadata.json @@ -0,0 +1,9 @@ +{ + "Workspace": "free", + "Links": { + "Laravel": "https://codesphere.com/articles/laravel" + }, + "Categories": ["Framework", "PHP"], + "Contributors": ["opencode-MiniMaxM27"], + "Title": "Laravel Quiz App" +} \ No newline at end of file diff --git a/laravel/public/index.php b/laravel/public/index.php new file mode 100644 index 0000000..6311fb8 --- /dev/null +++ b/laravel/public/index.php @@ -0,0 +1,11 @@ +handleRequest(Request::capture()); \ No newline at end of file diff --git a/laravel/resources/views/layouts/app.blade.php b/laravel/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..90c4576 --- /dev/null +++ b/laravel/resources/views/layouts/app.blade.php @@ -0,0 +1,50 @@ + + + + + + @yield('title', 'Laravel Quiz App') + + + +
+
+

Laravel Quiz App

+
+
+
+ @if(session('success')) +
{{ session('success') }}
+ @endif + @yield('content') +
+ + \ No newline at end of file diff --git a/laravel/resources/views/quizzes/create.blade.php b/laravel/resources/views/quizzes/create.blade.php new file mode 100644 index 0000000..d543985 --- /dev/null +++ b/laravel/resources/views/quizzes/create.blade.php @@ -0,0 +1,27 @@ +@extends('layouts.app') + +@section('title', 'Create Quiz') + +@section('content') +
+

Create New Quiz

+ +
+ @csrf +
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+@endsection \ No newline at end of file diff --git a/laravel/resources/views/quizzes/index.blade.php b/laravel/resources/views/quizzes/index.blade.php new file mode 100644 index 0000000..90787f0 --- /dev/null +++ b/laravel/resources/views/quizzes/index.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.app') + +@section('title', 'All Quizzes') + +@section('content') +
+

Available Quizzes

+ Create New Quiz + + @if($quizzes->count()) + + + + + + + + + + @foreach($quizzes as $quiz) + + + + + + @endforeach + +
TitleQuestionsActions
+ {{ $quiz->title }} + @if($quiz->description) +
{{ $quiz->description }} + @endif +
{{ $quiz->questions_count }} questions + View +
+ +
+ {{ $quizzes->links() }} +
+ @else +

+ No quizzes yet. Create the first one! +

+ @endif +
+@endsection \ No newline at end of file diff --git a/laravel/resources/views/quizzes/result.blade.php b/laravel/resources/views/quizzes/result.blade.php new file mode 100644 index 0000000..c08086d --- /dev/null +++ b/laravel/resources/views/quizzes/result.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.app') + +@section('title', 'Quiz Results') + +@section('content') +
+

{{ $quiz->title }} - Results

+ +
+ {{ $score }} / {{ $total }} +
+ +

+ @if($score >= $total * 0.7) + Great job! You passed! + @elseif($score >= $total * 0.4) + Not bad! Keep practicing. + @else + Keep learning and try again! + @endif +

+ +
+ Retake Quiz + All Quizzes +
+
+@endsection \ No newline at end of file diff --git a/laravel/resources/views/quizzes/show.blade.php b/laravel/resources/views/quizzes/show.blade.php new file mode 100644 index 0000000..6b602fb --- /dev/null +++ b/laravel/resources/views/quizzes/show.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.app') + +@section('title', $quiz->title) + +@section('content') +
+

{{ $quiz->title }}

+ @if($quiz->description) +

{{ $quiz->description }}

+ @endif + + @if($quiz->questions->count() > 0) +
+ @csrf + @foreach($quiz->questions as $index => $question) +
+

Q{{ $index + 1 }}: {{ $question->question_text }}

+
+ @foreach($question->options as $option) + + @endforeach +
+
+ @endforeach + +
+ + Back +
+
+ @else +

+ No questions yet. Add some questions to make the quiz playable. +

+ @endif +
+ +
+

Add Question

+
+ @csrf + + +
+ + +
+ +
+ +
+ + + + +
+
+ +
+ + +
+ + +
+
+@endsection \ No newline at end of file diff --git a/laravel/routes/console.php b/laravel/routes/console.php new file mode 100644 index 0000000..32dab09 --- /dev/null +++ b/laravel/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); \ No newline at end of file diff --git a/laravel/routes/web.php b/laravel/routes/web.php new file mode 100644 index 0000000..031f2fd --- /dev/null +++ b/laravel/routes/web.php @@ -0,0 +1,16 @@ +except(['index']); +Route::post('/quizzes/{quiz}/take', [QuizController::class, 'take'])->name('quizzes.take'); \ No newline at end of file diff --git a/laravel/start.sh b/laravel/start.sh new file mode 100644 index 0000000..f2efe52 --- /dev/null +++ b/laravel/start.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +echo "Installing dependencies..." +composer install --no-interaction --no-scripts + +echo "Creating SQLite database..." +touch database/database.sqlite + +echo "Running migrations..." +php artisan migrate --force + +echo "Starting Laravel development server..." +php artisan serve --host=0.0.0.0 --port=3000 \ No newline at end of file