summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-10-27 22:56:47 -0400
committergrothedev <grothedev@gmail.com>2025-10-27 22:56:47 -0400
commitfe59d936a7dbb32a95a4236bcdf286f540dd8fae (patch)
treecd66c69531539ae9e31ec66331ce8a19eabcd095
parentbe8540404fcf06694ac85fd88d168b0644707055 (diff)
getting back to work on this. dont know why i never pushed models. might as well include these artifacts from claude.
-rw-r--r--README.md16
-rw-r--r--artifacts-from-claude/dashboardctrler63
-rw-r--r--artifacts-from-claude/dashboardroutes7
-rw-r--r--artifacts-from-claude/select-examples.html126
-rw-r--r--artifacts-from-claude/userdashboard379
-rw-r--r--artifacts-from-claude/userfilemgmt591
-rw-r--r--models.txt21
-rwxr-xr-xresources/views/home.blade.php3
8 files changed, 1203 insertions, 3 deletions
diff --git a/README.md b/README.md
index 9bad143..aab7fed 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,18 @@ https://laravel.com/api/11.x/
https://laravel.com/docs/11.x/blade
#### TODO:
+- a site statistics page
+ - total visits, visit frequency, country of recent visits, average location makeup,
+ - current connections, time spent on site (average, min, max, )
+ -
+- views
+ - want to make a view that consolidates the two styles of files and links
+ - links
+ - add additional metadata on hover
+ - files
- htmx
- make /htmx/{thing}/{param} routes for sub-elements of pages? e.g. /links/# gives a blade page, but /htmx/links/# would give just the single html element (and maybe would be queried by /links/#, though it might be silly to have to require a double request like that, when the php can already do the necessary processing in blade)
+- error handling and validation for file upload and everything
- writing
- add drag-drop file to upload and then generate appropriate html to embed that file
- auto capitalization https://marked.js.org/using_pro
@@ -28,7 +38,11 @@ https://laravel.com/docs/11.x/blade
- import any model via json
- links
- break out the canvas cursor renderer from main.js
+ - and more js organizination: treat each js file like a "plugin" that provides some functionality to some page if the page has the appropriate elements on it. define what dom elems are needed and optional for some js file. this way it won't be coupled with the page as much.
- homepage image
-
+ the entire webapp downloads and stores an application in the users localstorage.
+ if connection lost, user can still interact with the website services while offline.
+ in addition, other users who are on the same network can still use the webapp if it was downloaded to their localstorage.
+
- user dashboard system
diff --git a/artifacts-from-claude/dashboardctrler b/artifacts-from-claude/dashboardctrler
new file mode 100644
index 0000000..4f41417
--- /dev/null
+++ b/artifacts-from-claude/dashboardctrler
@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class DashboardController extends Controller
+{
+ /**
+ * Create a new controller instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ $this->middleware('auth');
+ }
+
+ /**
+ * Show the user dashboard.
+ *
+ * @return \Illuminate\Contracts\Support\Renderable
+ */
+ public function index()
+ {
+ $user = Auth::user();
+
+ // You can add any additional data preparation here
+ // before passing to the view
+
+ return view('dashboard');
+ }
+
+ /**
+ * Show activity statistics.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function statistics()
+ {
+ $user = Auth::user();
+
+ $stats = [
+ 'files' => [
+ 'count' => $user->files()->count(),
+ 'size' => $user->files()->sum('size'),
+ 'recent' => $user->files()->latest()->take(5)->get()
+ ],
+ 'writings' => [
+ 'count' => $user->writings()->count(),
+ 'recent' => $user->writings()->latest()->take(5)->get()
+ ],
+ 'storage' => [
+ 'used' => $user->getStorageUsed(),
+ 'total' => $user->storage_quota,
+ 'percent' => min(100, round(($user->getStorageUsed() / max(1, $user->storage_quota)) * 100))
+ ]
+ ];
+
+ return response()->json($stats);
+ }
+} \ No newline at end of file
diff --git a/artifacts-from-claude/dashboardroutes b/artifacts-from-claude/dashboardroutes
new file mode 100644
index 0000000..91f97e0
--- /dev/null
+++ b/artifacts-from-claude/dashboardroutes
@@ -0,0 +1,7 @@
+// Add these routes to your routes/web.php file
+
+Route::middleware(['auth'])->group(function () {
+ // Dashboard
+ Route::get('/dashboard', [App\Http\Controllers\DashboardController::class, 'index'])->name('dashboard');
+ Route::get('/dashboard/stats', [App\Http\Controllers\DashboardController::class, 'statistics'])->name('dashboard.stats');
+}); \ No newline at end of file
diff --git a/artifacts-from-claude/select-examples.html b/artifacts-from-claude/select-examples.html
new file mode 100644
index 0000000..2142fa0
--- /dev/null
+++ b/artifacts-from-claude/select-examples.html
@@ -0,0 +1,126 @@
+import React, { useState } from 'react';
+
+export default function SelectExamples() {
+ const [basicValue, setBasicValue] = useState('');
+ const [multiValue, setMultiValue] = useState([]);
+ const [groupValue, setGroupValue] = useState('');
+
+ return (
+ <div className="space-y-8 p-6 max-w-2xl mx-auto">
+ {/* Basic Select */}
+ <div className="space-y-2">
+ <h3 className="text-lg font-semibold">Basic Select</h3>
+ <select
+ value={basicValue}
+ onChange={(e) => setBasicValue(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">Choose an option...</option>
+ <option value="1">Option 1</option>
+ <option value="2" disabled>Option 2 (Disabled)</option>
+ <option value="3">Option 3</option>
+ <option value="4" selected>Option 4 (Pre-selected)</option>
+ </select>
+ <p className="text-sm text-gray-600">Selected value: {basicValue}</p>
+
+ <pre className="bg-gray-100 p-4 rounded text-sm">
+{`<select>
+ <option value="">Choose an option...</option>
+ <option value="1">Option 1</option>
+ <option value="2" disabled>Option 2 (Disabled)</option>
+ <option value="3">Option 3</option>
+ <option value="4" selected>Option 4</option>
+</select>`}
+ </pre>
+ </div>
+
+ {/* Multiple Select */}
+ <div className="space-y-2">
+ <h3 className="text-lg font-semibold">Multiple Select</h3>
+ <select
+ multiple
+ value={multiValue}
+ onChange={(e) => setMultiValue(Array.from(e.target.selectedOptions, option => option.value))}
+ className="w-full p-2 border rounded"
+ size="4"
+ >
+ <option value="red">Red</option>
+ <option value="blue">Blue</option>
+ <option value="green">Green</option>
+ <option value="yellow">Yellow</option>
+ </select>
+ <p className="text-sm text-gray-600">Selected values: {multiValue.join(', ')}</p>
+
+ <pre className="bg-gray-100 p-4 rounded text-sm">
+{`<select multiple size="4">
+ <option value="red">Red</option>
+ <option value="blue">Blue</option>
+ <option value="green">Green</option>
+ <option value="yellow">Yellow</option>
+</select>`}
+ </pre>
+ </div>
+
+ {/* Option Groups */}
+ <div className="space-y-2">
+ <h3 className="text-lg font-semibold">Option Groups</h3>
+ <select
+ value={groupValue}
+ onChange={(e) => setGroupValue(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">Select a pet...</option>
+ <optgroup label="Dogs">
+ <option value="labrador">Labrador</option>
+ <option value="poodle">Poodle</option>
+ <option value="bulldog">Bulldog</option>
+ </optgroup>
+ <optgroup label="Cats">
+ <option value="siamese">Siamese</option>
+ <option value="persian">Persian</option>
+ <option value="maine-coon">Maine Coon</option>
+ </optgroup>
+ </select>
+ <p className="text-sm text-gray-600">Selected value: {groupValue}</p>
+
+ <pre className="bg-gray-100 p-4 rounded text-sm">
+{`<select>
+ <option value="">Select a pet...</option>
+ <optgroup label="Dogs">
+ <option value="labrador">Labrador</option>
+ <option value="poodle">Poodle</option>
+ <option value="bulldog">Bulldog</option>
+ </optgroup>
+ <optgroup label="Cats">
+ <option value="siamese">Siamese</option>
+ <option value="persian">Persian</option>
+ <option value="maine-coon">Maine Coon</option>
+ </optgroup>
+</select>`}
+ </pre>
+ </div>
+
+ {/* Key Attributes Reference */}
+ <div className="space-y-2">
+ <h3 className="text-lg font-semibold">Key Attributes Reference</h3>
+ <div className="bg-gray-100 p-4 rounded">
+ <p className="font-medium">Select attributes:</p>
+ <ul className="list-disc list-inside space-y-1 text-sm">
+ <li><code className="bg-gray-200 px-1">multiple</code> - Allow multiple selections</li>
+ <li><code className="bg-gray-200 px-1">size</code> - Number of visible options</li>
+ <li><code className="bg-gray-200 px-1">disabled</code> - Disable the entire select</li>
+ <li><code className="bg-gray-200 px-1">required</code> - Make selection required</li>
+ <li><code className="bg-gray-200 px-1">name</code> - Form field name</li>
+ </ul>
+
+ <p className="font-medium mt-4">Option attributes:</p>
+ <ul className="list-disc list-inside space-y-1 text-sm">
+ <li><code className="bg-gray-200 px-1">value</code> - Option's value</li>
+ <li><code className="bg-gray-200 px-1">selected</code> - Pre-select the option</li>
+ <li><code className="bg-gray-200 px-1">disabled</code> - Disable specific option</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/artifacts-from-claude/userdashboard b/artifacts-from-claude/userdashboard
new file mode 100644
index 0000000..09cddef
--- /dev/null
+++ b/artifacts-from-claude/userdashboard
@@ -0,0 +1,379 @@
+{{-- resources/views/dashboard.blade.php --}}
+@extends('template')
+
+@section('head')
+<style>
+ .dashboard-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-card {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ padding: 20px;
+ transition: transform 0.2s;
+ }
+
+ .dashboard-card:hover {
+ transform: translateY(-5px);
+ }
+
+ .dashboard-card h2 {
+ margin-top: 0;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 10px;
+ font-size: 1.4rem;
+ }
+
+ .dashboard-card-footer {
+ margin-top: 15px;
+ text-align: right;
+ }
+
+ .dashboard-stats {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 10px;
+ background-color: #f8f9fa;
+ border-radius: 5px;
+ flex-grow: 1;
+ margin: 0 5px;
+ }
+
+ .stat-number {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #4a69bd;
+ }
+
+ .stat-label {
+ font-size: 0.85rem;
+ color: #666;
+ }
+
+ .progress-container {
+ margin: 15px 0;
+ }
+
+ .progress-bar {
+ height: 10px;
+ background-color: #e9ecef;
+ border-radius: 5px;
+ overflow: hidden;
+ }
+
+ .progress-fill {
+ height: 100%;
+ background-color: #4a69bd;
+ transition: width 0.3s ease;
+ }
+
+ .item-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ .item-list li {
+ padding: 8px 0;
+ border-bottom: 1px solid #f1f1f1;
+ }
+
+ .item-list li:last-child {
+ border-bottom: none;
+ }
+
+ .badge {
+ display: inline-block;
+ padding: 3px 8px;
+ font-size: 0.8rem;
+ font-weight: 500;
+ border-radius: 12px;
+ }
+
+ .badge-primary {
+ background-color: #e1eaff;
+ color: #4a69bd;
+ }
+
+ .badge-success {
+ background-color: #e3fcef;
+ color: #00a86b;
+ }
+
+ .badge-warning {
+ background-color: #fff8e1;
+ color: #ffa000;
+ }
+
+ .empty-state {
+ text-align: center;
+ padding: 20px;
+ color: #999;
+ font-style: italic;
+ }
+
+ .avatar-container {
+ display: flex;
+ align-items: center;
+ }
+
+ .avatar {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background-color: #4a69bd;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ margin-right: 15px;
+ }
+
+ .profile-info {
+ flex-grow: 1;
+ }
+
+ .tag-list {
+ margin-top: 10px;
+ }
+
+ .tag {
+ display: inline-block;
+ font-size: 12px;
+ padding: 2px 8px;
+ margin-right: 5px;
+ margin-bottom: 5px;
+ background-color: #f1f1f1;
+ border-radius: 12px;
+ }
+</style>
+@endsection
+
+@section('body')
+<main>
+ <h1>Dashboard</h1>
+
+ @php
+ $user = Auth::user();
+
+ // Storage stats
+ $usedStorage = $user->getStorageUsed();
+ $totalStorage = $user->storage_quota;
+ $storagePercent = $totalStorage > 0 ? min(100, round(($usedStorage / $totalStorage) * 100)) : 0;
+
+ // File stats
+ $fileCount = $user->files()->count();
+ $recentFiles = $user->files()->orderBy('created_at', 'desc')->take(3)->get();
+
+ // Writing stats
+ $writingCount = $user->writings()->count();
+ $recentWritings = $user->writings()->orderBy('created_at', 'desc')->take(3)->get();
+
+ // Quest stats (if implemented)
+ $questCount = method_exists($user, 'quests') ? $user->quests()->count() : 0;
+ $activeQuests = method_exists($user, 'quests') ? $user->quests()
+ ->wherePivot('status', 'in_progress')
+ ->orderBy('created_at', 'desc')
+ ->take(3)
+ ->get() : collect();
+
+ // Inventory stats (placeholder)
+ $inventoryCount = 0; // Update when inventory is implemented
+ @endphp
+
+ <!-- Profile Overview -->
+ <div class="dashboard-card" style="margin-bottom: 20px;">
+ <div class="avatar-container">
+ <div class="avatar">{{ strtoupper(substr($user->name, 0, 1)) }}</div>
+ <div class="profile-info">
+ <h2>{{ $user->name }}</h2>
+ <p>{{ $user->email }}</p>
+ <p>
+ <span class="badge badge-primary">{{ $user->role == 0 ? 'Admin' : 'User' }}</span>
+ <span class="badge badge-success">Member since {{ $user->created_at->format('M Y') }}</span>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <!-- Activity Stats -->
+ <div class="dashboard-stats">
+ <div class="stat-item">
+ <div class="stat-number">{{ $fileCount }}</div>
+ <div class="stat-label">Files</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-number">{{ $writingCount }}</div>
+ <div class="stat-label">Writings</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-number">{{ $questCount }}</div>
+ <div class="stat-label">Quests</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-number">{{ $inventoryCount }}</div>
+ <div class="stat-label">Items</div>
+ </div>
+ </div>
+
+ <div class="dashboard-container">
+ <!-- Storage Card -->
+ <div class="dashboard-card">
+ <h2>Storage</h2>
+
+ <div class="progress-container">
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
+ <span>{{ round($usedStorage / 1024, 2) }} MB used</span>
+ <span>{{ round($totalStorage / 1024, 2) }} MB total</span>
+ </div>
+ <div class="progress-bar">
+ <div class="progress-fill" style="width: {{ $storagePercent }}%"></div>
+ </div>
+ </div>
+
+ <h3>Recent Files</h3>
+ @if($recentFiles->count() > 0)
+ <ul class="item-list">
+ @foreach($recentFiles as $file)
+ <li>
+ <div style="display: flex; justify-content: space-between;">
+ <div>
+ <a href="/f/{{ $file->filename }}">{{ Str::limit($file->filename, 25) }}</a>
+ <div style="font-size: 0.8rem; color: #666;">
+ {{ round($file->size / 1024 / 1024, 2) }} MB • {{ $file->created_at->diffForHumans() }}
+ </div>
+ </div>
+ <div>
+ <a href="?file={{ $file->path }}" title="Download">⬇️</a>
+ </div>
+ </div>
+ </li>
+ @endforeach
+ </ul>
+ @else
+ <div class="empty-state">No files uploaded yet</div>
+ @endif
+
+ <div class="dashboard-card-footer">
+ <a href="/f">Manage Files →</a>
+ </div>
+ </div>
+
+ <!-- Writings Card -->
+ <div class="dashboard-card">
+ <h2>Writings</h2>
+
+ @if($recentWritings->count() > 0)
+ <ul class="item-list">
+ @foreach($recentWritings as $writing)
+ <li>
+ <div>
+ <a href="{{ route('w.show', $writing->id) }}">{{ Str::limit($writing->title, 35) }}</a>
+ <div style="font-size: 0.8rem; color: #666;">
+ {{ Str::limit(strip_tags(Str::markdown($writing->content)), 60) }}
+ </div>
+ <div style="font-size: 0.8rem; color: #666;">
+ {{ $writing->created_at->diffForHumans() }}
+ </div>
+ </div>
+ </li>
+ @endforeach
+ </ul>
+ @else
+ <div class="empty-state">No writings created yet</div>
+ @endif
+
+ <div class="dashboard-card-footer">
+ <a href="{{ route('w.create') }}">New Writing</a> •
+ <a href="{{ route('w.index') }}">All Writings →</a>
+ </div>
+ </div>
+
+ <!-- Quests Card -->
+ <div class="dashboard-card">
+ <h2>Quests</h2>
+
+ @if(method_exists($user, 'quests') && $activeQuests->count() > 0)
+ <ul class="item-list">
+ @foreach($activeQuests as $quest)
+ <li>
+ <div>
+ <a href="{{ route('quests.show', $quest->id) }}">{{ Str::limit($quest->title, 35) }}</a>
+ <div style="font-size: 0.8rem; color: #666;">
+ Progress: {{ $quest->pivot->progress }}%
+ </div>
+ <div class="progress-bar" style="margin: 5px 0; height: 6px;">
+ <div class="progress-fill" style="width: {{ $quest->pivot->progress }}%; background-color: #00a86b;"></div>
+ </div>
+ <div style="font-size: 0.8rem; color: #666;">
+ <span class="badge badge-{{ $quest->difficulty == 'easy' ? 'success' : ($quest->difficulty == 'hard' ? 'warning' : 'primary') }}">
+ {{ ucfirst($quest->difficulty) }}
+ </span>
+ {{ $quest->reward_points }} points
+ </div>
+ </div>
+ </li>
+ @endforeach
+ </ul>
+ @else
+ <div class="empty-state">No active quests</div>
+ @endif
+
+ <div class="dashboard-card-footer">
+ @if(method_exists($user, 'quests'))
+ <a href="{{ route('quests.index') }}">View Quests →</a>
+ @else
+ <span style="color: #999;">Coming soon</span>
+ @endif
+ </div>
+ </div>
+
+ <!-- Inventory Card (Placeholder) -->
+ <div class="dashboard-card">
+ <h2>Inventory</h2>
+
+ <div class="empty-state">Inventory system coming soon</div>
+
+ <div class="dashboard-card-footer">
+ <span style="color: #999;">Under development</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Tags Used -->
+ <div class="dashboard-card">
+ <h2>Your Tags</h2>
+
+ @php
+ // Get all tags from user's content
+ $fileTags = $user->files()->with('tags')->get()->pluck('tags')->flatten();
+ $writingTags = method_exists($user->writings(), 'with') ?
+ $user->writings()->with('tags')->get()->pluck('tags')->flatten() :
+ collect();
+
+ $allTags = $fileTags->merge($writingTags)->unique('id');
+ @endphp
+
+ @if($allTags->count() > 0)
+ <div class="tag-list">
+ @foreach($allTags as $tag)
+ <span class="tag">{{ $tag->label }}</span>
+ @endforeach
+ </div>
+ @else
+ <div class="empty-state">No tags used yet</div>
+ @endif
+ </div>
+</main>
+@endsection \ No newline at end of file
diff --git a/artifacts-from-claude/userfilemgmt b/artifacts-from-claude/userfilemgmt
new file mode 100644
index 0000000..629a624
--- /dev/null
+++ b/artifacts-from-claude/userfilemgmt
@@ -0,0 +1,591 @@
+// 1. First, we'll update the User model to add file quota settings
+// app/Models/User.php - Add these methods and properties
+
+<?php
+
+namespace App\Models;
+
+// Existing imports...
+
+class User extends Authenticatable
+{
+ // Add to existing fillable array
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ 'role',
+ 'max_file_size', // Maximum file size in KB
+ 'storage_quota', // Total storage space in KB
+ ];
+
+ // Add to existing casts array
+ protected function casts(): array
+ {
+ return [
+ 'email_verified_at' => 'datetime',
+ 'password' => 'hashed',
+ 'max_file_size' => 'integer',
+ 'storage_quota' => 'integer',
+ ];
+ }
+
+ // Add this relation for files
+ public function files()
+ {
+ return $this->hasMany(File::class);
+ }
+
+ // Helper methods for file storage
+
+ /**
+ * Get used storage space in KB
+ */
+ public function getStorageUsed()
+ {
+ return $this->files()->sum('size') / 1024; // Convert bytes to KB
+ }
+
+ /**
+ * Get remaining storage quota in KB
+ */
+ public function getRemainingQuota()
+ {
+ $used = $this->getStorageUsed();
+ return max(0, $this->storage_quota - $used);
+ }
+
+ /**
+ * Check if user has enough quota for a file
+ */
+ public function hasQuotaFor($fileSize)
+ {
+ // Convert file size to KB for comparison
+ $fileSizeKB = $fileSize / 1024;
+ return $fileSizeKB <= $this->getRemainingQuota();
+ }
+
+ /**
+ * Check if file size is within user's allowed maximum
+ */
+ public function isFileSizeAllowed($fileSize)
+ {
+ // Convert to KB for comparison
+ $fileSizeKB = $fileSize / 1024;
+
+ // If max_file_size is not set (0), use system default
+ if (!$this->max_file_size) {
+ return $fileSizeKB <= config('filesystems.max_upload_size', 10240);
+ }
+
+ return $fileSizeKB <= $this->max_file_size;
+ }
+
+ /**
+ * Get default quota based on user role
+ */
+ public static function getDefaultQuota($role = 1)
+ {
+ // Define default quotas based on roles (in KB)
+ $quotas = [
+ 0 => 1024 * 1024, // Admin: 1GB
+ 1 => 100 * 1024, // Regular user: 100MB
+ 2 => 50 * 1024, // Basic user: 50MB
+ ];
+
+ return $quotas[$role] ?? $quotas[1]; // Default to regular user quota
+ }
+
+ /**
+ * Get default max file size based on user role
+ */
+ public static function getDefaultMaxFileSize($role = 1)
+ {
+ // Define default max file sizes based on roles (in KB)
+ $sizes = [
+ 0 => 100 * 1024, // Admin: 100MB per file
+ 1 => 10 * 1024, // Regular user: 10MB per file
+ 2 => 5 * 1024, // Basic user: 5MB per file
+ ];
+
+ return $sizes[$role] ?? $sizes[1]; // Default to regular user size
+ }
+}
+
+// 2. Create migration to add quota fields to users table
+// database/migrations/2025_02_22_000001_add_file_quotas_to_users_table.php
+
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use App\Models\User;
+
+return new class extends Migration
+{
+ public function up(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ if (!Schema::hasColumn('users', 'max_file_size')) {
+ $table->integer('max_file_size')->default(0)->after('role'); // in KB, 0 means use system default
+ }
+
+ if (!Schema::hasColumn('users', 'storage_quota')) {
+ $table->integer('storage_quota')->default(102400)->after('max_file_size'); // 100MB in KB
+ }
+ });
+
+ // Set default quotas for existing users based on their roles
+ $users = User::all();
+ foreach ($users as $user) {
+ $user->storage_quota = User::getDefaultQuota($user->role);
+ $user->max_file_size = User::getDefaultMaxFileSize($user->role);
+ $user->save();
+ }
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn(['max_file_size', 'storage_quota']);
+ });
+ }
+};
+
+// 3. Update FileController to check user quota and file size limits
+// Update the store method in app/Http/Controllers/FileController.php
+
+public function store(Request $request)
+{
+ // Get current user
+ $user = Auth::user();
+
+ // Guest uploads if no user is logged in
+ if (!$user) {
+ // You can either block uploads or create a guest user system
+ return response()->json([
+ 'success' => false,
+ 'message' => 'You must be logged in to upload files.'
+ ], 403);
+ }
+
+ $dir = 'uploads';
+ if ($request->has('current_dir')) {
+ // Extract relative directory path if provided
+ $dir = str_replace('storage/', '', $request->current_dir);
+ }
+
+ // We'll validate in multiple steps to provide better error messages
+
+ // Step 1: Basic validation
+ $validated = $request->validate([
+ 'f' => 'required|array',
+ 'f.*' => 'required|file'
+ ]);
+
+ $files = $request->file('f');
+ $totalSize = 0;
+
+ // Step 2: Check each file against user's max file size limit
+ foreach ($files as $file) {
+ $fileSize = $file->getSize();
+ $totalSize += $fileSize;
+
+ if (!$user->isFileSizeAllowed($fileSize)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'File "' . $file->getClientOriginalName() . '" exceeds your maximum allowed file size of ' .
+ ($user->max_file_size > 0 ? ($user->max_file_size / 1024) . ' MB' : (config('filesystems.max_upload_size', 10240) / 1024) . ' MB')
+ ], 413); // 413 Payload Too Large
+ }
+ }
+
+ // Step 3: Check total size against remaining quota
+ if (!$user->hasQuotaFor($totalSize)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Not enough storage quota. You have ' . round($user->getRemainingQuota() / 1024, 2) . ' MB remaining, but tried to upload ' . round($totalSize / 1024 / 1024, 2) . ' MB.'
+ ], 413);
+ }
+
+ $results = [
+ 'num_files' => count($files),
+ 'num_uploaded' => 0,
+ 'files' => []
+ ];
+
+ // Process tags
+ $tags = [];
+ if ($request->has('tags') && !empty($request->tags)) {
+ $tags = array_map('trim', explode(',', $request->tags));
+ }
+
+ foreach ($files as $file) {
+ $filename = $file->getClientOriginalName();
+
+ // Avoid duplicate filenames by adding timestamp
+ if (Storage::disk('public')->exists("${dir}/${filename}")) {
+ $filename = Carbon::now()->timestamp . '_' . $filename;
+ }
+
+ // Store the file
+ $path = $file->storeAs($dir, $filename, 'public');
+
+ if ($path) {
+ // Create file record in database
+ $fileModel = File::create([
+ 'filename' => $filename,
+ 'path' => $path,
+ 'source' => $request->ip(),
+ 'description' => '',
+ 'md5' => md5_file($file->getRealPath()),
+ 'size' => $file->getSize(),
+ 'mime_type' => $file->getMimeType(),
+ 'user_id' => $user->id
+ ]);
+
+ // Add tags if provided
+ if (!empty($tags)) {
+ $fileModel->addTags($tags);
+ }
+
+ $results['num_uploaded']++;
+ $results['files'][] = [
+ 'filename' => $filename,
+ 'path' => $path,
+ 'id' => $fileModel->id
+ ];
+ }
+ }
+
+ // Return based on requested response format
+ if ($request->input('response_format') === 'html') {
+ return redirect()->back()->with('success', "{$results['num_uploaded']} files uploaded successfully");
+ }
+
+ return response()->json($results);
+}
+
+// 4. Update the SiteController's uploadFiles method similarly
+// Update this method in app/Http/Controllers/SiteController.php
+
+public function uploadFiles(Request $request)
+{
+ // Get current user
+ $user = Auth::user();
+
+ // Guest uploads handling
+ if (!$user) {
+ if ($request->input('response_format') === 'html') {
+ return redirect()->back()->with('error', 'You must be logged in to upload files.');
+ }
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'You must be logged in to upload files.'
+ ], 403);
+ }
+
+ $files = $request->file('f');
+ $dir = config('filesystems.upload_dir', 'uploads');
+ $returnJSON = $request->input('response_format') !== 'html';
+
+ $res = [
+ 'num_files' => count($files),
+ 'num_failed' => 0,
+ 'num_uploaded' => 0,
+ 'success' => false,
+ 'files' => []
+ ];
+
+ // Validate files (basic validation)
+ $validated = $request->validate([
+ 'f' => 'required|array',
+ 'f.*' => 'required|file'
+ ]);
+
+ // Check individual file size limits and total quota
+ $totalSize = 0;
+
+ foreach ($files as $file) {
+ $fileSize = $file->getSize();
+ $totalSize += $fileSize;
+
+ if (!$user->isFileSizeAllowed($fileSize)) {
+ if ($returnJSON) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'File "' . $file->getClientOriginalName() . '" exceeds your maximum allowed file size.'
+ ], 413);
+ } else {
+ return redirect()->back()->with('error', 'File "' . $file->getClientOriginalName() . '" exceeds your maximum allowed file size.');
+ }
+ }
+ }
+
+ if (!$user->hasQuotaFor($totalSize)) {
+ if ($returnJSON) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Not enough storage quota. You have ' . round($user->getRemainingQuota() / 1024, 2) . ' MB remaining.'
+ ], 413);
+ } else {
+ return redirect()->back()->with('error', 'Not enough storage quota. You have ' . round($user->getRemainingQuota() / 1024, 2) . ' MB remaining.');
+ }
+ }
+
+ // Process tags
+ $tags = [];
+ if ($request->has('tags') && !empty($request->tags)) {
+ $tags = array_map('trim', explode(',', $request->tags));
+ }
+
+ foreach ($files as $f) {
+ $filename = $f->getClientOriginalName();
+
+ // Avoid duplicate filenames
+ if (Storage::disk('public')->exists("${dir}/${filename}")) {
+ $filename = Carbon::now()->timestamp . '_' . $filename;
+ }
+
+ $path = $f->storeAs($dir, $filename, 'public');
+
+ if ($path) {
+ // Create file record in database
+ $fileModel = File::create([
+ 'filename' => $filename,
+ 'path' => $path,
+ 'source' => $request->ip(),
+ 'description' => '',
+ 'md5' => md5_file($f->getRealPath()),
+ 'size' => $f->getSize(),
+ 'mime_type' => $f->getMimeType(),
+ 'user_id' => $user->id
+ ]);
+
+ // Add tags if provided
+ if (!empty($tags)) {
+ $fileModel->addTags($tags);
+ }
+
+ $res['num_uploaded']++;
+ $res['files'][] = [
+ 'filename' => $filename,
+ 'path' => $path,
+ 'id' => $fileModel->id
+ ];
+ } else {
+ $res['num_failed']++;
+ }
+ }
+
+ if ($res['num_uploaded'] === count($files)) {
+ $res['success'] = true;
+ }
+
+ if ($returnJSON) {
+ return response()->json($res);
+ } else {
+ return redirect("f/{$filename}");
+ }
+}
+
+// 5. Add a user storage quota dashboard
+// Create a new view file: resources/views/files/quota.blade.php
+
+@extends('template')
+
+@section('head')
+<style>
+ .quota-container {
+ margin: 20px 0;
+ }
+ .quota-bar {
+ height: 30px;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 10px;
+ }
+ .quota-progress {
+ height: 100%;
+ background-color: #4a69bd;
+ transition: width 0.3s ease;
+ }
+ .quota-stats {
+ display: flex;
+ justify-content: space-between;
+ font-size: 14px;
+ }
+ .quota-warning {
+ color: #e74c3c;
+ margin-bottom: 15px;
+ }
+ .file-stats {
+ margin: 30px 0;
+ }
+ .file-stats-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 10px;
+ }
+ .file-stats-table th,
+ .file-stats-table td {
+ padding: 8px 12px;
+ text-align: left;
+ border-bottom: 1px solid #e1e1e1;
+ }
+ .file-stats-table th {
+ background-color: #f5f5f5;
+ }
+</style>
+@endsection
+
+@section('body')
+<main>
+ <h1>Your Storage Quota</h1>
+
+ <div class="quota-container">
+ @php
+ $user = Auth::user();
+ $usedKB = $user->getStorageUsed();
+ $totalKB = $user->storage_quota;
+ $percentUsed = $totalKB > 0 ? min(100, round(($usedKB / $totalKB) * 100)) : 0;
+ $usedMB = round($usedKB / 1024, 2);
+ $totalMB = round($totalKB / 1024, 2);
+ $maxFileSizeMB = $user->max_file_size > 0 ? round($user->max_file_size / 1024, 2) : round(config('filesystems.max_upload_size', 10240) / 1024, 2);
+ @endphp
+
+ @if ($percentUsed > 90)
+ <div class="quota-warning">
+ <strong>Warning:</strong> You are using {{ $percentUsed }}% of your storage quota.
+ </div>
+ @endif
+
+ <div class="quota-bar">
+ <div class="quota-progress" style="width: {{ $percentUsed }}%;"></div>
+ </div>
+
+ <div class="quota-stats">
+ <div><strong>Used:</strong> {{ $usedMB }} MB ({{ $percentUsed }}%)</div>
+ <div><strong>Total:</strong> {{ $totalMB }} MB</div>
+ </div>
+
+ <div style="margin-top: 10px;">
+ <strong>Maximum File Size Allowed:</strong> {{ $maxFileSizeMB }} MB per file
+ </div>
+ </div>
+
+ <div class="file-stats">
+ <h2>Your Files</h2>
+
+ @php
+ $userFiles = $user->files()->orderBy('created_at', 'desc')->limit(10)->get();
+ $totalFiles = $user->files()->count();
+ $largestFile = $user->files()->orderBy('size', 'desc')->first();
+ $largestFileMB = $largestFile ? round($largestFile->size / 1024 / 1024, 2) : 0;
+ @endphp
+
+ <div style="margin-bottom: 20px;">
+ <div><strong>Total Files:</strong> {{ $totalFiles }}</div>
+ <div><strong>Largest File:</strong> {{ $largestFileMB }} MB ({{ $largestFile ? $largestFile->filename : 'None' }})</div>
+ </div>
+
+ <h3>Recently Uploaded Files</h3>
+
+ @if ($userFiles->count() > 0)
+ <table class="file-stats-table">
+ <thead>
+ <tr>
+ <th>File Name</th>
+ <th>Size</th>
+ <th>Uploaded</th>
+ <th>Tags</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach ($userFiles as $file)
+ <tr>
+ <td><a href="/f/{{ $file->filename }}">{{ $file->filename }}</a></td>
+ <td>{{ round($file->size / 1024 / 1024, 2) }} MB</td>
+ <td>{{ $file->created_at->diffForHumans() }}</td>
+ <td>{{ $file->getTagsString() }}</td>
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+
+ @if ($totalFiles > 10)
+ <div style="margin-top: 10px; text-align: right;">
+ <a href="/files?user={{ $user->id }}">View all files</a>
+ </div>
+ @endif
+ @else
+ <p>You haven't uploaded any files yet.</p>
+ @endif
+ </div>
+</main>
+@endsection
+
+// 6. Add routes for the quota dashboard
+// Add this to routes/web.php
+
+Route::middleware('auth')->group(function () {
+ Route::get('/files/quota', [FileController::class, 'quotaDashboard'])->name('files.quota');
+});
+
+// 7. Add the quota dashboard method to FileController
+// Add this to app/Http/Controllers/FileController.php
+
+/**
+ * Display the user's storage quota dashboard
+ */
+public function quotaDashboard()
+{
+ return view('files.quota');
+}
+
+// 8. Update the f.blade.php file to show file ownership
+// Add this to your existing file listing table
+
+<th>Owner</th>
+
+<!-- And in the row display -->
+<td>
+ @php
+ $fileOwner = null;
+ if (isset($fileModel) && $fileModel && $fileModel->user_id) {
+ $fileOwner = App\Models\User::find($fileModel->user_id);
+ }
+ @endphp
+ {{ $fileOwner ? $fileOwner->name : 'Unknown' }}
+</td>
+
+// 9. Update the file upload form to show quota information
+// Add this just before your upload form in f.blade.php
+
+@auth
+ @php
+ $user = Auth::user();
+ $usedKB = $user->getStorageUsed();
+ $totalKB = $user->storage_quota;
+ $percentUsed = $totalKB > 0 ? min(100, round(($usedKB / $totalKB) * 100)) : 0;
+ $usedMB = round($usedKB / 1024, 2);
+ $totalMB = round($totalKB / 1024, 2);
+ $maxFileSizeMB = $user->max_file_size > 0 ? round($user->max_file_size / 1024, 2) : round(config('filesystems.max_upload_size', 10240) / 1024, 2);
+ @endphp
+
+ <div style="margin-bottom: 15px; background-color: #f8f9fa; padding: 10px; border-radius: 4px;">
+ <div style="margin-bottom: 5px;">
+ <strong>Your Storage:</strong> {{ $usedMB }} MB used of {{ $totalMB }} MB ({{ $percentUsed }}%)
+ </div>
+ <div style="height: 10px; background-color: #e9ecef; border-radius: 5px; overflow: hidden;">
+ <div style="width: {{ $percentUsed }}%; height: 100%; background-color: #4a69bd;"></div>
+ </div>
+ <div style="margin-top: 5px; font-size: 12px;">
+ Maximum file size: {{ $maxFileSizeMB }} MB per file
+ | <a href="{{ route('files.quota') }}">View Details</a>
+ </div>
+ </div>
+@endauth \ No newline at end of file
diff --git a/models.txt b/models.txt
new file mode 100644
index 0000000..340b8e0
--- /dev/null
+++ b/models.txt
@@ -0,0 +1,21 @@
+Link
+@defaults
+-label:string
+-value:string
+-tags:Tag[]
+-desc:string
+Tag
+@defaults
+-label:string
+-refs:int #how many objects have this tag associated with them
+File
+-filename:string
+-path:string
+-source:string #ip, user, link?
+-desc:string
+-tags:Tag[]
+User
+-name:string
+-email:string
+-description:string
+-password:string
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
index 0771532..c99d1c6 100755
--- a/resources/views/home.blade.php
+++ b/resources/views/home.blade.php
@@ -14,8 +14,7 @@
Welcome
</p>
<div id = "site-status">
- <h6>Current Status</h6>
- <div id = "statusWS"></div>
+ Current Status <div id = "statusWS"></div>
<p>Number of friends connected: <b id = "numFriendsConnected"></b></p> <!-- show number of other connections to webserver -->
<br>
<h6>Who am I?</h6>