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
+
+
+
+
+
+
+
+
+
+ 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')
+
+
+
+
+
+ @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')
+
+@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())
+
+
+
+ | Title |
+ Questions |
+ Actions |
+
+
+
+ @foreach($quizzes as $quiz)
+
+
+ {{ $quiz->title }}
+ @if($quiz->description)
+ {{ $quiz->description }}
+ @endif
+ |
+ {{ $quiz->questions_count }} questions |
+
+ View
+ |
+
+ @endforeach
+
+
+
+
+ {{ $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
+
+
+
+
+@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)
+
+ @else
+
+ No questions yet. Add some questions to make the quiz playable.
+
+ @endif
+
+
+
+@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