Commit fd08eae7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

init

parents
.git
.github
.vscode
.idea
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
tests
.env
.env.local
.env.testing
docs/
*.md
!CLAUDE.md
docker-compose.yml
docker-compose.override.yml
# Local Development Override
# These values override the hardcoded production defaults in config files.
# Copy this to .env for local development.
APP_NAME="UGC Heaven"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_KEY=base64:eaoG4Gp567JOfBeAlQ0qCOknVsQ531ZJATEMFN9gj6w=
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=ugc_heaven
DB_USERNAME=postgres
DB_PASSWORD=postgres
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=sync
LOG_CHANNEL=daily
MAIL_MAILER=log
PEERTUBE_URL=https://ugcvideoserver.caprover.al-arcade.com
PEERTUBE_CLIENT_ID=b148jgk5ffimzszm8cqa4fut2o5u2jf7
PEERTUBE_CLIENT_SECRET=sRXMboZI4941vn1cs3GSJIhtyCAVli32
PEERTUBE_ADMIN_USERNAME=root
PEERTUBE_ADMIN_PASSWORD=Alarcade123#
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.env.local
.env.testing
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/.claude/memory
database/database.sqlite
# UGC Heaven — Project Rules
Read `docs/` for full specifications. These rules govern ALL code written in this repo.
---
## ABSOLUTE LAWS (NEVER VIOLATE)
### 1. ZERO EMOJIS
No emoji characters anywhere in source code, templates, lang files, notifications, system messages, or UI strings. Every visual indicator uses Lucide SVG icons via `<x-icon name="..." />`. If you write an emoji, you have failed. Grep check: `grep -rPn '[\x{1F300}-\x{1F9FF}]' resources/ app/ lang/` must return 0.
### 2. EVERYTHING ANIMATED
No element appears, disappears, changes state, or responds to interaction without a CSS/JS transition. Minimum: 150ms ease on color/opacity/transform for interactive elements. Enter animations on all page content (fade-up, stagger for lists). Exit animations on all removals. Hover states on all clickable elements. Use `transform` and `opacity` only (GPU-accelerated). Respect `prefers-reduced-motion`. See `docs/23-design-rules.md` for full animation spec.
### 3. RTL-FIRST CSS
Never use physical properties (`margin-left`, `padding-right`, `text-align: left`). Always use logical properties (`margin-inline-start`, `padding-inline-end`, `text-align: start`) or Tailwind equivalents (`ms-4`, `pe-6`, `text-start`). The layout must work perfectly in both `dir="ltr"` and `dir="rtl"`.
### 4. NO HARDCODED STRINGS
Every user-facing string goes through `__('file.key')` or `@lang('file.key')`. No English text directly in Blade templates. This includes: button labels, form labels, placeholders, error messages, empty states, toasts, page titles, tooltips, alt text.
---
## ARCHITECTURE
### Module Structure
```
app/Modules/{ModuleName}/
├── Models/
├── Controllers/
├── Services/
├── Requests/
├── Resources/
├── Policies/
├── Events/
├── Listeners/
├── Jobs/
├── Routes/
├── Database/Migrations/
└── Tests/
```
Modules: Auth, Users, Creators, Companies, Campaigns, Applications, Invitations, Projects, Deliverables, VideoReview, Portfolios, Messaging, Notifications, Reviews, Reputation, Matching, Reporting, PeerTube, Admin, Settings.
### Cross-Module Communication
- NEVER import directly from another module's internal classes
- Use Events for async side effects
- Use Service interfaces (bound in DI container) for sync calls
- Use `App\Shared\DTOs` for data passing between modules
### Layer Responsibilities
```
Controller → validate (FormRequest) + authorize (Policy) + delegate to Service + return Resource
Service → business logic + DB transactions + dispatch Events
Model → relationships + scopes + accessors/mutators
Event → payload only (no logic)
Listener → single side effect per listener
Job → async work (video processing, emails, etc.)
Policy → authorization rules (return bool)
Resource → API response shaping (never expose internal IDs)
Request → validation rules + authorize() method
```
### Controllers MUST:
- Inject Service via constructor
- Use FormRequest for validation (not `$request->validate()`)
- Use `$this->authorize()` for permissions (not inline role checks)
- Return API Resource (not raw model)
- Be thin (max ~10 lines per method)
### Services MUST:
- Wrap multi-step operations in `DB::transaction()`
- Dispatch events after successful operations
- Throw typed exceptions (`InvalidStatusTransitionException`, not generic `Exception`)
- Never access `auth()` or `request()` — receive user/data as parameters
---
## DATABASE RULES
- PostgreSQL 16 with extensions: uuid-ossp, pg_trgm, pgcrypto
- Tables: snake_case, plural
- Columns: snake_case
- Foreign keys: `{singular}_id`
- Every user-facing entity has soft deletes (`deleted_at`)
- Public identifiers: UUID (never expose auto-increment IDs in APIs/URLs)
- Internal references: bigint auto-increment (for joins/performance)
- All timestamps UTC
- JSONB for arrays (niches, skills, languages, social_links, equipment)
- Indexes on: every FK, every status column, every field used in WHERE/ORDER
- GIN indexes on JSONB columns used for filtering
- Full-text search indexes for searchable text fields
### Migration Rules:
- One table per migration file
- Always include indexes in the migration (not separate)
- Always add `created_at`, `updated_at`
- Use CHECK constraints for enums (not MySQL-style ENUM type)
- Foreign keys with appropriate ON DELETE (CASCADE for owned, SET NULL for referenced)
---
## STATUS TRANSITIONS
Every entity with a status field has a defined transition map. Status changes MUST be validated:
```php
if (!EntityStatus::canTransition($current, $new)) {
throw new InvalidStatusTransitionException("Cannot transition from {$current} to {$new}");
}
```
Never allow arbitrary status changes. See docs 05 (campaigns), 06 (applications, invitations), 07 (projects), 08 (deliverables) for exact transition maps.
---
## VALIDATION
### Every field validated:
- Type check (string, integer, array, file, etc.)
- Length/size limits (min/max)
- Format validation (email, URL, phone, etc.)
- Business rules (unique, exists in DB, valid status, etc.)
- Enum values validated against allowed list
### Custom validation for:
- File uploads: validate MIME via magic bytes (not just extension)
- URLs: validate format per platform (TikTok, Instagram, YouTube patterns)
- Username: no reserved words, alphanumeric + underscore only
- Phone: valid format with country code
- Date ranges: start < end, future dates where required
- Budget: required_if based on budget_type
### Validation messages:
- All translatable via `lang/{locale}/validation.php`
- Custom attribute names via `attributes` array
- Custom messages via `messages()` method in FormRequest
---
## API RESPONSE FORMAT
### Success:
```json
{"success": true, "data": {...}, "message": "...", "meta": {"current_page": 1, "per_page": 25, "total": 100}}
```
### Error:
```json
{"success": false, "message": "...", "error_code": "CAMPAIGN_ALREADY_CLOSED", "errors": {"field": ["message"]}}
```
### Rules:
- Never expose internal IDs (use UUIDs)
- Always use API Resources (never return raw models)
- Pagination on all list endpoints (default 25, max 100)
- Include relationships only when loaded (`$this->whenLoaded()`)
- ISO 8601 for all datetime fields
- Consistent error codes (see `docs/19-error-handling-and-edge-cases.md`)
---
## EVENTS & SIDE EFFECTS
When an action happens, dispatch ONE event. Multiple listeners handle side effects independently:
```
ApplicationAccepted →
├── Listener: CreateProjectFromApplication
├── Listener: NotifyCreatorOfAcceptance
├── Listener: UpdateCampaignApplicationCount
└── Listener: LogActivity
```
Side effects that MUST use events (never inline):
- Sending notifications
- Sending emails
- Creating related entities (project from application)
- Updating counters/stats
- Logging activity
- Triggering background jobs
---
## AUTHORIZATION
### Policy for every model:
```php
class CampaignPolicy
{
public function update(User $user, Campaign $campaign): bool
{
return $user->role === 'company'
&& $user->companyProfile->id === $campaign->company_id;
}
}
```
### Middleware stack:
1. `auth` — authenticated
2. `verified` — email verified
3. `role:{role}` — correct role
4. `company.approved` — company is approved (for company routes)
5. `profile.complete:{min}` — profile completion gate
### Never:
- Inline role checks in controllers (`if ($user->role === ...`)
- Trust client-side data for authorization
- Allow users to access other users' private resources
---
## FILE UPLOADS
### Image uploads:
- Validate MIME (magic bytes)
- Strip EXIF data (privacy)
- Resize to multiple sizes (thumbnail: 200px, medium: 600px, large: 1200px)
- Convert to WebP
- Store via FileUploadService
### Video uploads:
- Validate MIME + file size (max 2GB)
- Upload to PeerTube via PeerTubeService
- Use resumable upload for files > 100MB
- Process asynchronously (ProcessVideoUpload job)
- Track processing status (uploading → processing → ready → failed)
- Privacy: Private (deliverables) or Unlisted (portfolio)
### Document uploads:
- Validate MIME + size (max 50MB)
- Store in private storage
- Serve via signed URLs (time-limited)
---
## FRONTEND RULES
### Tech: Blade + Tailwind CSS + Alpine.js (+ Livewire for interactive parts)
### Components:
- Every UI element is a Blade component (`<x-ui.button>`, `<x-ui.card>`, etc.)
- Components use CSS custom properties for theming (not hardcoded colors)
- All components support both LTR and RTL
- All interactive components have animations (enter, exit, hover, focus)
### Icons:
- Library: Lucide Icons (SVG sprite or inline SVG)
- Component: `<x-icon name="camera" size="md" />`
- Sizes: xs (12px), sm (16px), md (20px), lg (24px), xl (32px)
- Never use emoji, font-awesome, or icon text characters
### Visual Style: Modern Gradient (Framer/Raycast aesthetic)
- Subtle gradients on buttons, active states, decorative elements
- Glass/frosted panels (backdrop-blur) on modals, dropdowns, overlays
- Dark gradient sidebar + light content area
- Cards with soft shadows that elevate on hover
- Rounded corners: 12-16px (not sharp, not pill — refined)
- Generous whitespace, breathing room
- Font: Plus Jakarta Sans (headings EN), Inter (body EN), Tajawal (AR)
- See `docs/26-design-direction.md` for full CSS specs
### Theming:
- All colors via CSS custom properties (`var(--color-primary)`)
- Gradients via CSS custom properties (`var(--gradient-primary)`)
- Properties loaded from `platform_settings` table (cached in Redis)
- SuperAdmin changes colors → cache busted → site updates instantly
- Dark mode: separate set of CSS variables toggled via `data-theme="dark"`
### Responsive:
- Mobile-first approach
- Breakpoints: sm (640), md (768), lg (1024), xl (1280)
- Sidebar: fixed on desktop, overlay on tablet, bottom tabs on mobile
- Tables become cards on mobile
### Accessibility:
- All interactive elements keyboard-accessible
- Visible focus rings
- aria-labels on icon-only buttons
- Color contrast 4.5:1 minimum
- Skip-to-content link
- Form labels properly associated
---
## LOCALIZATION
### Structure:
```
lang/en/{module}.php
lang/ar/{module}.php
```
### Rules:
- Every string through `__()` or `@lang()`
- Fallback locale: English
- Arabic plural rules: use full ICU plural syntax (0, 1, 2, few, many, other)
- Numbers: Western numerals (0-9) in both languages
- Dates: locale-formatted via Carbon
- Currency: code + amount format (e.g., "EGP 5,000")
- Validation messages + attribute names translated
---
## PEERTUBE INTEGRATION
### PeerTubeService must handle:
- OAuth token management (cache 24h, auto-refresh)
- Video upload (standard + resumable)
- Channel management (one per creator for portfolio, one per company for project deliverables)
- Video status polling
- Error handling with retry (3 attempts, exponential backoff)
- Graceful degradation (platform works if PeerTube is down — just video features disabled)
### Video privacy mapping:
- Portfolio videos → Unlisted (accessible via embed only)
- Deliverable submissions → Private (only parties can view)
- Never Public (we control access through our platform)
---
## NOTIFICATIONS
- Every significant action triggers a notification
- In-app notifications: always (stored in DB, delivered via WebSocket/polling)
- Email notifications: respect user preferences (per-category toggles)
- Batch same-type notifications (5+ in 10min → single summary)
- Max 10 emails/hour/user
- All notification text is translatable (not hardcoded)
- All notification titles use icon references, NEVER emoji
---
## SECURITY
- CSRF on all forms
- Rate limiting on all endpoints (see docs/16 for limits)
- Parameterized queries only (Eloquent handles this)
- No raw SQL with user input
- File upload validation (magic bytes, not extension)
- CSP headers
- CORS restricted to known origins
- Audit log for all admin actions (immutable)
- Passwords: bcrypt, min 8 chars, mixed case + number
- Sessions in Redis (easy invalidation)
- Signed URLs for private file access
---
## TESTING REQUIREMENTS
Before any feature is "done":
1. Service unit tests: every method, every status transition, every edge case
2. Feature tests: every API endpoint (happy + error paths)
3. Policy tests: every permission boundary
4. Validation tests: every rule fires correctly
### Test naming:
```php
public function test_creator_can_apply_to_published_campaign(): void
public function test_creator_cannot_apply_after_deadline(): void
public function test_company_cannot_apply_to_own_campaign(): void
```
---
## DEPLOYMENT (CAPROVER + GITLAB)
### How It Deploys:
1. Code lives on GitLab (`gitlab.caprover.al-arcade.com`)
2. CapRover app linked to GitLab repo (branch: `main`)
3. Push to `main` → CapRover pulls → builds Docker image → deploys
4. No manual steps. No SSH. No `.env` file to upload. Just push.
### NO .env IN PRODUCTION
All configuration is HARDCODED in config files with production values as defaults. The app reads from environment variables (set in CapRover app settings), but if none are set, the config files contain working production defaults for our infrastructure.
### Config Strategy:
```php
// config/database.php — NO env() calls. Direct values.
'pgsql' => [
'host' => 'srv-captain--ugc-heaven-db',
'port' => '5432',
'database' => 'ugc_heaven',
'username' => 'ugcadmin',
'password' => 'UgcH3aven2024!',
],
```
**NO `env()` IN PRODUCTION CONFIG.** Values are hardcoded directly. No .env file, no CapRover env vars, no environment variable reading. The config files ARE the configuration. Push code → it works.
For local development: override via `.env` file (gitignored) which only works because `env()` is used as a wrapper — but the default/fallback is always the production value hardcoded inline.
### Production Values (Hardcoded Directly In Config Files):
```
APP_NAME = UGC Heaven
APP_URL = https://ugcheaven.caprover.al-arcade.com
APP_ENV = production
APP_DEBUG = false
APP_KEY = (generated at project init, committed to repo)
DB_HOST = srv-captain--ugc-heaven-db
DB_PORT = 5432
DB_DATABASE = ugc_heaven
DB_USERNAME = ugcadmin
DB_PASSWORD = UgcH3aven2024!
REDIS_HOST = srv-captain--ugc-heaven-redis
REDIS_PORT = 6379
PEERTUBE_URL = https://ugcvideoserver.caprover.al-arcade.com
PEERTUBE_CLIENT_ID = b148jgk5ffimzszm8cqa4fut2o5u2jf7
PEERTUBE_CLIENT_SECRET = sRXMboZI4941vn1cs3GSJIhtyCAVli32
PEERTUBE_ADMIN_USERNAME = root
PEERTUBE_ADMIN_PASSWORD = Alarcade123#
SESSION_DRIVER = redis
CACHE_STORE = redis
QUEUE_CONNECTION = redis
FILESYSTEM_DISK = local
MAIL_MAILER = smtp
MAIL_HOST = srv-captain--poste-io
MAIL_PORT = 587
MAIL_ENCRYPTION = tls
MAIL_USERNAME = noreply@al-arcade.com
MAIL_PASSWORD = Alarcade123#
MAIL_FROM_ADDRESS = noreply@al-arcade.com
MAIL_FROM_NAME = UGC Heaven
```
### SuperAdmin Seed Credentials:
```
Email: admin@al-arcade.com
Password: Alarcade123#
```
These are hardcoded in the AdminSeeder. No env vars. No config files to upload. Push → deploy → works.
### Files Required At Repo Root:
- `captain-definition` — tells CapRover how to build
- `Dockerfile` — multi-stage PHP + Nginx build
- `docker/nginx.conf` — Nginx site config
- `docker/supervisord.conf` — process manager (PHP-FPM + queue worker + cron)
- `docker/entrypoint.sh` — runs migrations + cache on container start
- `.dockerignore` — exclude dev files from image
### Build Process (entrypoint.sh):
```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
php artisan storage:link
supervisord -c /etc/supervisord.conf
```
### CapRover App Settings:
- App name: `ugcheaven`
- Domain: `ugcheaven.caprover.al-arcade.com`
- HTTPS: enabled (wildcard cert covers it)
- Persistent storage: `/var/www/html/storage/app` → volume mount
### Container Services (via supervisord):
- PHP-FPM (main app)
- Nginx (reverse proxy + static files)
- Queue worker (`php artisan queue:work redis --tries=3`)
- Cron (`php artisan schedule:run` every minute)
---
## GIT PRACTICES
- Commit per logical unit (not per file)
- Message format: `feat(module): description` / `fix(module): description`
- `.env` is gitignored — NEVER committed
- Config files contain production defaults — this IS committed (credentials in config are acceptable for this private GitLab)
- Run tests before commit
- Push to `main` triggers auto-deploy
---
## CHECKLIST BEFORE MARKING ANY FEATURE COMPLETE
- [ ] All fields validated per docs spec
- [ ] All status transitions enforced
- [ ] Policy authorization on every action
- [ ] Events dispatched for all side effects
- [ ] Notifications triggered (in-app + email where specified)
- [ ] API Resource used (no raw model in response)
- [ ] UUID exposed publicly (never internal ID)
- [ ] Translations added (en + ar)
- [ ] RTL layout tested
- [ ] Animations on enter/exit/hover/state-change
- [ ] Icons used (zero emojis)
- [ ] Empty states designed
- [ ] Loading states designed
- [ ] Error states handled gracefully
- [ ] Edge cases from docs addressed
- [ ] Mobile responsive
- [ ] Service tests written
- [ ] Feature tests written
- [ ] Activity logged (for admin audit)
# ============================================
# UGC Heaven — Production Dockerfile
# CapRover GitLab deploy: push → build → live
# ============================================
# Stage 1: Composer dependencies
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev
# Stage 2: Frontend assets
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --production=false
COPY . .
RUN npm run build
# Stage 3: Production image
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
curl \
zip \
unzip \
libpq-dev \
libzip-dev \
icu-dev \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
libwebp-dev \
oniguruma-dev \
&& rm -rf /var/cache/apk/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install \
pdo \
pdo_pgsql \
pgsql \
zip \
intl \
gd \
mbstring \
opcache \
pcntl \
bcmath
# Install Redis extension
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
# Configure PHP for production
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php.ini "$PHP_INI_DIR/conf.d/99-ugcheaven.ini"
# Configure OPcache
RUN echo "opcache.enable=1" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.memory_consumption=256" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.max_accelerated_files=20000" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.validate_timestamps=0" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini"
# Setup working directory
WORKDIR /var/www/html
# Copy application
COPY --from=composer /app/vendor ./vendor
COPY --from=frontend /app/public/build ./public/build
COPY . .
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Copy config files
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Setup cron
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1" > /etc/crontabs/www-data
# Expose port 80 (CapRover routes to this)
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost/api/health || exit 1
ENTRYPOINT ["/entrypoint.sh"]
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
<?php
namespace App\Models;
use App\Shared\Traits\HasUuid;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmail
{
use HasFactory, Notifiable, SoftDeletes, HasUuid;
protected $fillable = [
'uuid',
'name',
'email',
'password',
'role',
'status',
'email_verified_at',
'phone',
'avatar',
'locale',
'last_login_at',
'last_login_ip',
];
protected $hidden = [
'id',
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'password' => 'hashed',
];
}
public function getRouteKeyName(): string
{
return 'uuid';
}
public function creatorProfile()
{
return $this->hasOne(\App\Modules\Creators\Models\CreatorProfile::class);
}
public function companyProfile()
{
return $this->hasOne(\App\Modules\Companies\Models\CompanyProfile::class);
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isCreator(): bool
{
return $this->role === 'creator';
}
public function isCompany(): bool
{
return $this->role === 'company';
}
public function isActive(): bool
{
return $this->status === 'active';
}
}
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
Model::preventLazyLoading(!app()->isProduction());
Model::unguard(false);
if (app()->isProduction()) {
URL::forceScheme('https');
}
}
}
<?php
namespace App\Providers;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class ModuleServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->registerModuleProviders();
}
public function boot(): void
{
$this->loadModuleRoutes();
$this->loadModuleMigrations();
}
private function registerModuleProviders(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$moduleName = basename($modulePath);
$providerClass = "App\\Modules\\{$moduleName}\\Providers\\{$moduleName}ServiceProvider";
if (class_exists($providerClass)) {
$this->app->register($providerClass);
}
}
}
private function loadModuleRoutes(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$routesPath = $modulePath . '/Routes';
if (File::isDirectory($routesPath)) {
$webRoute = $routesPath . '/web.php';
$apiRoute = $routesPath . '/api.php';
if (File::exists($webRoute)) {
Route::middleware('web')->group($webRoute);
}
if (File::exists($apiRoute)) {
Route::middleware('api')->prefix('api')->group($apiRoute);
}
}
}
}
private function loadModuleMigrations(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$migrationsPath = $modulePath . '/Database/Migrations';
if (File::isDirectory($migrationsPath)) {
$this->loadMigrationsFrom($migrationsPath);
}
}
}
}
<?php
namespace App\Shared\Exceptions;
use Exception;
class InvalidStatusTransitionException extends Exception
{
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureCompanyApproved
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user || $user->role !== 'company') {
abort(403);
}
if (!$user->companyProfile || $user->companyProfile->status !== 'approved') {
abort(403, __('company.not_approved'));
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureProfileComplete
{
public function handle(Request $request, Closure $next, int $minPercent = 50): Response
{
$user = $request->user();
if (!$user) {
abort(401);
}
$profile = match ($user->role) {
'creator' => $user->creatorProfile,
'company' => $user->companyProfile,
default => null,
};
if ($profile && $profile->completion_percentage < $minPercent) {
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'message' => __('profile.incomplete'),
'error_code' => 'PROFILE_INCOMPLETE',
], 403);
}
return redirect()->route('profile.edit');
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!$request->user() || !in_array($request->user()->role, $roles, true)) {
abort(403, __('auth.unauthorized_role'));
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale', config('app.locale', 'en'));
if (in_array($locale, config('app.supported_locales', ['en', 'ar']))) {
app()->setLocale($locale);
}
return $next($request);
}
}
<?php
namespace App\Shared\Traits;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
trait ApiResponse
{
protected function success(
mixed $data = null,
string $message = '',
int $code = 200,
array $meta = []
): JsonResponse {
$response = [
'success' => true,
'data' => $data,
'message' => $message,
];
if (!empty($meta)) {
$response['meta'] = $meta;
}
return response()->json($response, $code);
}
protected function successResource(
JsonResource $resource,
string $message = '',
int $code = 200
): JsonResponse {
return response()->json([
'success' => true,
'data' => $resource,
'message' => $message,
], $code);
}
protected function successCollection(
ResourceCollection $collection,
string $message = ''
): JsonResponse {
$paginated = $collection->response()->getData(true);
return response()->json([
'success' => true,
'data' => $paginated['data'],
'message' => $message,
'meta' => $paginated['meta'] ?? [],
]);
}
protected function error(
string $message,
int $code = 400,
?string $errorCode = null,
array $errors = []
): JsonResponse {
$response = [
'success' => false,
'message' => $message,
];
if ($errorCode) {
$response['error_code'] = $errorCode;
}
if (!empty($errors)) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
}
<?php
namespace App\Shared\Traits;
use App\Shared\Exceptions\InvalidStatusTransitionException;
trait HasStatusTransitions
{
public function transitionTo(string $newStatus): void
{
if (!static::canTransition($this->status, $newStatus)) {
throw new InvalidStatusTransitionException(
"Cannot transition from '{$this->status}' to '{$newStatus}'"
);
}
$this->status = $newStatus;
$this->save();
}
public static function canTransition(string $from, string $to): bool
{
$allowed = static::allowedTransitions();
return isset($allowed[$from]) && in_array($to, $allowed[$from], true);
}
abstract protected static function allowedTransitions(): array;
}
<?php
namespace App\Shared\Traits;
use Illuminate\Support\Str;
trait HasUuid
{
protected static function bootHasUuid(): void
{
static::creating(function ($model) {
if (empty($model->uuid)) {
$model->uuid = (string) Str::uuid();
}
});
}
}
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
$status = (require_once __DIR__.'/bootstrap/app.php')
->handleCommand(new ArgvInput);
exit($status);
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Shared\Middleware\SetLocale::class,
]);
$middleware->alias([
'role' => \App\Shared\Middleware\EnsureRole::class,
'company.approved' => \App\Shared\Middleware\EnsureCompanyApproved::class,
'profile.complete' => \App\Shared\Middleware\EnsureProfileComplete::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\ModuleServiceProvider::class,
];
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
{
"$schema": "https://getcomposer.org/schema.json",
"name": "ugc-heaven/platform",
"type": "project",
"description": "UGC Heaven - The Ultimate UGC Marketplace Platform",
"keywords": ["ugc", "marketplace", "creators", "brands"],
"license": "proprietary",
"require": {
"php": "^8.2",
"blade-ui-kit/blade-icons": "^1.6",
"laravel/framework": "^11.31",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.1",
"laravel/pint": "^1.13",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.0.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"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
},
"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
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<?php
return [
'name' => env('APP_NAME', 'UGC Heaven'),
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),
'url' => env('APP_URL', 'https://ugcheaven.caprover.al-arcade.com'),
'timezone' => 'UTC',
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => 'en',
'faker_locale' => 'en_US',
'supported_locales' => ['en', 'ar'],
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY', 'base64:eaoG4Gp567JOfBeAlQ0qCOknVsQ531ZJATEMFN9gj6w='),
'previous_keys' => [],
'maintenance' => [
'driver' => 'file',
'store' => 'database',
],
];
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => 10800,
];
<?php
return [
'default' => env('CACHE_STORE', 'redis'),
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
],
'prefix' => 'ugc_heaven_cache_',
];
<?php
return [
'default' => env('DB_CONNECTION', 'pgsql'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => true,
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'srv-captain--ugc-heaven-db'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'ugc_heaven'),
'username' => env('DB_USERNAME', 'ugcadmin'),
'password' => env('DB_PASSWORD', 'UgcH3aven2024!'),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
],
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
'redis' => [
'client' => 'phpredis',
'options' => [
'cluster' => 'redis',
'prefix' => 'ugc_heaven_',
],
'default' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '0',
],
'cache' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '1',
],
'session' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '2',
],
],
];
<?php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => '/storage',
'visibility' => 'public',
'throw' => false,
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
'default' => env('LOG_CHANNEL', 'daily'),
'deprecations' => [
'channel' => 'null',
'trace' => false,
],
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'days' => 14,
'replace_placeholders' => true,
],
'stderr' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
<?php
return [
'default' => env('MAIL_MAILER', 'smtp'),
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'srv-captain--poste-io'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME', 'noreply@al-arcade.com'),
'password' => env('MAIL_PASSWORD', 'Alarcade123#'),
'timeout' => null,
'local_domain' => 'ugcheaven.caprover.al-arcade.com',
],
'log' => [
'transport' => 'log',
'channel' => null,
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'noreply@al-arcade.com'),
'name' => env('MAIL_FROM_NAME', 'UGC Heaven'),
],
];
<?php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'sync' => [
'driver' => 'sync',
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => null,
'after_commit' => false,
],
],
'batching' => [
'database' => 'pgsql',
'table' => 'job_batches',
],
'failed' => [
'driver' => 'database-uuids',
'database' => 'pgsql',
'table' => 'failed_jobs',
],
];
<?php
return [
'peertube' => [
'url' => env('PEERTUBE_URL', 'https://ugcvideoserver.caprover.al-arcade.com'),
'client_id' => env('PEERTUBE_CLIENT_ID', 'b148jgk5ffimzszm8cqa4fut2o5u2jf7'),
'client_secret' => env('PEERTUBE_CLIENT_SECRET', 'sRXMboZI4941vn1cs3GSJIhtyCAVli32'),
'admin_username' => env('PEERTUBE_ADMIN_USERNAME', 'root'),
'admin_password' => env('PEERTUBE_ADMIN_PASSWORD', 'Alarcade123#'),
],
];
<?php
return [
'driver' => env('SESSION_DRIVER', 'redis'),
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => false,
'files' => storage_path('framework/sessions'),
'connection' => 'session',
'table' => 'sessions',
'store' => null,
'lottery' => [2, 100],
'cookie' => 'ugc_heaven_session',
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => true,
'http_only' => true,
'same_site' => 'lax',
'partitioned' => false,
];
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::connection()->getDriverName() === 'pgsql') {
DB::statement('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
DB::statement('CREATE EXTENSION IF NOT EXISTS "pg_trgm"');
DB::statement('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
}
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role', 20)->default('creator');
$table->string('status', 20)->default('active');
$table->string('phone', 30)->nullable();
$table->string('avatar')->nullable();
$table->string('locale', 5)->default('en');
$table->timestamp('last_login_at')->nullable();
$table->string('last_login_ip', 45)->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index('role');
$table->index('status');
$table->index('created_at');
});
if (DB::connection()->getDriverName() === 'pgsql') {
DB::statement("ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'creator', 'company'))");
DB::statement("ALTER TABLE users ADD CONSTRAINT users_status_check CHECK (status IN ('active', 'suspended', 'banned', 'deactivated'))");
}
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('users');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('platform_settings', function (Blueprint $table) {
$table->id();
$table->string('group', 50)->index();
$table->string('key', 100);
$table->text('value')->nullable();
$table->string('type', 20)->default('string');
$table->timestamps();
$table->unique(['group', 'key']);
});
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action', 100);
$table->string('subject_type')->nullable();
$table->unsignedBigInteger('subject_id')->nullable();
$table->jsonb('properties')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['subject_type', 'subject_id']);
$table->index('action');
$table->index('created_at');
});
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
Schema::dropIfExists('activity_logs');
Schema::dropIfExists('platform_settings');
}
};
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class AdminSeeder extends Seeder
{
public function run(): void
{
User::firstOrCreate(
['email' => 'admin@al-arcade.com'],
[
'uuid' => Str::uuid()->toString(),
'name' => 'Super Admin',
'password' => Hash::make('Alarcade123#'),
'role' => 'admin',
'status' => 'active',
'email_verified_at' => now(),
'locale' => 'en',
]
);
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PlatformSettingsSeeder::class,
AdminSeeder::class,
]);
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class PlatformSettingsSeeder extends Seeder
{
public function run(): void
{
$settings = [
['group' => 'branding', 'key' => 'platform_name', 'value' => 'UGC Heaven', 'type' => 'string'],
['group' => 'branding', 'key' => 'tagline', 'value' => 'Where Brands Meet Creators', 'type' => 'string'],
['group' => 'branding', 'key' => 'logo_url', 'value' => '/images/logo.svg', 'type' => 'string'],
['group' => 'branding', 'key' => 'favicon_url', 'value' => '/images/favicon.ico', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_primary', 'value' => '#6366F1', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_primary_hover', 'value' => '#4F46E5', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_secondary', 'value' => '#06B6D4', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_accent', 'value' => '#F59E0B', 'type' => 'string'],
['group' => 'theme', 'key' => 'gradient_primary', 'value' => 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)', 'type' => 'string'],
['group' => 'theme', 'key' => 'gradient_secondary', 'value' => 'linear-gradient(135deg, #06B6D4 0%, #0EA5E9 100%)', 'type' => 'string'],
['group' => 'theme', 'key' => 'dark_mode_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'typography', 'key' => 'font_heading_en', 'value' => 'Plus Jakarta Sans', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_body_en', 'value' => 'Inter', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_heading_ar', 'value' => 'Tajawal', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_body_ar', 'value' => 'Tajawal', 'type' => 'string'],
['group' => 'features', 'key' => 'registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'creator_registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'company_registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'portfolio_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'messaging_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'reviews_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'limits', 'key' => 'max_upload_size_mb', 'value' => '2048', 'type' => 'integer'],
['group' => 'limits', 'key' => 'max_portfolio_videos', 'value' => '50', 'type' => 'integer'],
['group' => 'limits', 'key' => 'max_campaign_applications', 'value' => '500', 'type' => 'integer'],
['group' => 'seo', 'key' => 'meta_title', 'value' => 'UGC Heaven - Where Brands Meet Creators', 'type' => 'string'],
['group' => 'seo', 'key' => 'meta_description', 'value' => 'The ultimate UGC marketplace connecting companies with talented content creators.', 'type' => 'string'],
];
foreach ($settings as $setting) {
DB::table('platform_settings')->updateOrInsert(
['group' => $setting['group'], 'key' => $setting['key']],
array_merge($setting, ['created_at' => now(), 'updated_at' => now()])
);
}
}
}
#!/bin/sh
set -e
echo "=== UGC Heaven: Starting deployment ==="
cd /var/www/html
# Ensure storage directories exist
mkdir -p storage/framework/{cache,sessions,views}
mkdir -p storage/app/public
mkdir -p storage/logs
chown -R www-data:www-data storage bootstrap/cache
# Create storage symlink
php artisan storage:link --force 2>/dev/null || true
# Run migrations (non-interactive)
echo "=== Running migrations ==="
php artisan migrate --force
# Cache configuration for performance
echo "=== Caching config ==="
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Seed default data (only if needed — uses --no-interaction)
echo "=== Checking seed data ==="
php artisan db:seed --class=PlatformSettingsSeeder --force 2>/dev/null || true
php artisan db:seed --class=AdminSeeder --force 2>/dev/null || true
echo "=== UGC Heaven: Ready ==="
# Start supervisor (manages PHP-FPM, Nginx, Queue, Cron)
exec supervisord -c /etc/supervisord.conf
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
charset utf-8;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Max upload size (for video uploads)
client_max_body_size 2G;
client_body_timeout 600s;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 1000;
gzip_vary on;
# Static file caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Main routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 600;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Deny access to sensitive files
location ~ /(composer\.|artisan|package\.json|webpack\.mix\.js) {
deny all;
}
# Health check (no logging)
location = /api/health {
access_log off;
try_files $uri /index.php?$query_string;
}
}
; UGC Heaven PHP Configuration
; Upload limits (2GB for video)
upload_max_filesize = 2G
post_max_size = 2G
max_file_uploads = 20
; Memory and execution
memory_limit = 512M
max_execution_time = 600
max_input_time = 600
; Session
session.save_handler = redis
session.save_path = "tcp://srv-captain--ugc-heaven-redis:6379"
; Timezone
date.timezone = UTC
; Error handling (production)
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/www/html/storage/logs/php-errors.log
; Security
expose_php = Off
allow_url_fopen = On
allow_url_include = Off
[supervisord]
nodaemon=true
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=10
[program:nginx]
command=nginx -g "daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=20
[program:queue-worker]
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --memory=256
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=30
numprocs=2
process_name=%(program_name)s_%(process_num)02d
[program:cron]
command=crond -f -l 8
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=40
# 01 — System Overview
## Platform Identity
**UGC Heaven** is a professional UGC (User-Generated Content) marketplace connecting brands/companies with content creators. MENA-first, Arabic + English from day one. Trust and professionalism are the core brand values.
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Backend | PHP 8.2+ (Laravel 11) |
| Database | PostgreSQL 16 |
| Video Hosting | PeerTube 8.2.1 (self-hosted) |
| Cache/Queue | Redis |
| Search | PostgreSQL Full-Text + pg_trgm |
| File Storage | S3-compatible (CapRover volume or MinIO) |
| Deployment | CapRover (Docker Swarm) |
| Real-time | Laravel Reverb (WebSockets) |
| Email | Laravel Mail (SMTP) |
| Auth | Laravel Sanctum (API tokens) + session-based (web) |
---
## Three User Types (Single Codebase)
### 1. SuperAdmin
- Full platform control
- Sees everything, manages everything
- Cannot be created via registration (seeded only)
- Has dedicated `/admin` panel
### 2. Company
- Registers as a company/brand
- Posts campaigns, discovers creators, manages projects
- Can have multiple team members (future)
- Has dedicated `/company` dashboard
### 3. Creator
- Registers as a content creator
- Builds portfolio, applies to campaigns, delivers content
- Has dedicated `/creator` dashboard
---
## Core Architecture Principles
### 1. Modular by Domain
Each business domain is a self-contained Laravel module:
```
app/
├── Modules/
│ ├── Auth/
│ ├── Users/
│ ├── Companies/
│ ├── Creators/
│ ├── Portfolios/
│ ├── Campaigns/
│ ├── Applications/
│ ├── Invitations/
│ ├── Projects/
│ ├── Deliverables/
│ ├── VideoReview/
│ ├── Messaging/
│ ├── Notifications/
│ ├── Reviews/
│ ├── Reputation/
│ ├── Reporting/
│ ├── Matching/
│ ├── Admin/
│ └── PeerTube/
```
Each module contains:
```
Modules/Campaigns/
├── Models/
├── Controllers/
├── Services/
├── Requests/ (Form Requests / Validation)
├── Resources/ (API Resources / Transformers)
├── Policies/ (Authorization)
├── Events/
├── Listeners/
├── Jobs/
├── Routes/
├── Database/
│ ├── Migrations/
│ └── Seeders/
└── Tests/
```
### 2. Service Layer Pattern
Controllers are thin. Business logic lives in Services:
```php
// Controller: validate + delegate
// Service: orchestrate business logic
// Repository (optional): complex queries
// Model: data + relationships + scopes
```
### 3. Event-Driven Side Effects
Never chain side effects directly. Use events:
```
ApplicationAccepted →
├── CreateProject
├── NotifyCreator
├── UpdateCampaignStats
└── LogActivity
```
### 4. Policy-Based Authorization
Every action has a policy. No inline `if (user->role === 'admin')` checks:
```php
$this->authorize('approve', $application);
```
### 5. API-First Design
Every feature works via API first, then web views consume the same API/services.
---
## URL Structure
```
/ → Landing page
/login → Login (all types)
/register → Choose: company or creator
/register/company → Company registration
/register/creator → Creator registration
/admin/... → SuperAdmin panel
/company/... → Company dashboard
/creator/... → Creator dashboard
/creators/{username} → Public creator profile
/campaigns/{slug} → Public campaign page
/companies/{slug} → Public company page
/api/v1/... → REST API
```
---
## Database Naming Conventions
- Tables: `snake_case`, plural (`campaigns`, `creator_profiles`)
- Columns: `snake_case` (`created_at`, `company_id`)
- Foreign keys: `{singular_table}_id` (`campaign_id`, `user_id`)
- Pivot tables: alphabetical (`campaign_creator`, `project_user`)
- Soft deletes on everything user-facing
- UUIDs as public identifiers, auto-increment IDs internally
- All timestamps in UTC, display in user timezone
---
## Localization Strategy
- All strings through `__()` / `@lang()`
- Language files: `lang/en/`, `lang/ar/`
- Database content: separate `_translations` table or JSON column
- RTL support baked into CSS from day one
- Currency: support multiple (EGP, SAR, AED, USD)
- Date format: respect locale
---
## Security Baseline
- CSRF on all forms
- Rate limiting on auth + API endpoints
- Input sanitization (XSS prevention)
- SQL injection prevention (Eloquent parameterized queries)
- File upload validation (type, size, mime sniffing)
- Content Security Policy headers
- CORS configured for known origins only
- Audit log for all admin actions
- Two-factor authentication (optional, future)
# 02 — Authentication & User System
## Registration Flows
### Creator Registration
**Fields:**
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| first_name | string | yes | min:2, max:50, alpha_spaces |
| last_name | string | yes | min:2, max:50, alpha_spaces |
| email | email | yes | unique:users, valid MX record |
| password | string | yes | min:8, confirmed, mixed case + number |
| username | string | yes | min:3, max:30, unique, alphanumeric + underscores only, no reserved words |
| phone | string | no | valid phone format with country code |
| country | string | yes | valid ISO country code |
| agreed_to_terms | boolean | yes | must be true |
**Flow:**
1. User fills form → validate → create user (role: creator) + creator_profile
2. Send verification email
3. Redirect to `/creator/onboarding`
4. User CANNOT access platform features until email verified
**Edge Cases:**
- Email already exists but unverified → resend verification, don't create duplicate
- Email already exists and verified → show "already registered" with login link
- Username taken → suggest 3 alternatives (append numbers/underscores)
- Reserved usernames: `admin`, `support`, `ugcheaven`, `company`, `creator`, `api`, `www`, `mail`, `help`, `null`, `undefined`
- Disposable email domains → reject with message
- Password in breach list (HaveIBeenPwned API, optional) → warn but allow
---
### Company Registration
**Fields:**
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| company_name | string | yes | min:2, max:100 |
| contact_person_name | string | yes | min:2, max:100 |
| email | email | yes | unique:users, valid MX record, prefer business email |
| password | string | yes | min:8, confirmed, mixed case + number |
| phone | string | yes | valid phone format |
| country | string | yes | valid ISO country code |
| industry | string | yes | from predefined list |
| website | url | no | valid URL format |
| company_size | enum | yes | solo, 2-10, 11-50, 51-200, 201-500, 500+ |
| agreed_to_terms | boolean | yes | must be true |
**Flow:**
1. User fills form → validate → create user (role: company) + company_profile
2. Send verification email
3. Company status: `pending_review` (NOT immediately active)
4. Admin reviews and approves/rejects
5. On approval → email sent, company can use platform
6. On rejection → email sent with reason, can re-apply
**Edge Cases:**
- Free email (gmail, yahoo, etc.) → warning but not blocking (small businesses use these)
- Duplicate company name → allow (different companies can share names), unique slug generated
- Company rejected and wants to re-apply → allow, previous rejection visible to admin
- Company approved then found to be fraudulent → suspend flow (see below)
---
## Login
**Fields:**
| Field | Type | Required |
|-------|------|----------|
| email | email | yes |
| password | string | yes |
| remember_me | boolean | no |
**Flow:**
1. Validate credentials
2. Check user status (active, suspended, banned, pending)
3. If active → create session, redirect to role-appropriate dashboard
4. If suspended → show suspension message + reason + appeal link
5. If banned → show banned message (no appeal)
6. If email unverified → show "verify email" message + resend link
7. If company pending_review → show "under review" message
**Edge Cases:**
- 5 failed attempts in 15 minutes → lock account for 30 minutes + email notification
- Login from new device/location → email notification (informational, not blocking)
- Login from blocked country (if geo-restriction enabled) → reject
- Remember me → 30-day session, without → 2-hour session
- Concurrent sessions → allow up to 5, oldest killed when 6th created
- User deleted (soft) → "Account not found" (same message as wrong email for security)
---
## Password Reset
**Flow:**
1. User enters email → always show "if account exists, email sent" (prevent enumeration)
2. Token expires in 60 minutes
3. Token is single-use
4. On reset: kill all existing sessions
**Edge Cases:**
- Multiple reset requests → invalidate previous tokens
- Token used after expiry → "expired" message + new reset link
- User tries to reset while account is banned → silently ignore (no email sent)
- Rate limit: max 3 reset emails per hour per email address
---
## Email Verification
**Flow:**
1. Signed URL sent to email (expires in 24 hours)
2. Click verifies email, redirects to dashboard
3. Resend available (max 5 per hour)
**Edge Cases:**
- User changes email → re-verify new email, old email stays active until new confirmed
- Verification link expired → show "expired" + auto-resend button
- User verified but manually unverified by admin → must re-verify
---
## User Statuses
| Status | Can Login | Can Use Platform | Visible to Others |
|--------|-----------|-----------------|-------------------|
| active | yes | yes | yes |
| unverified | yes (limited) | no (verify email wall) | no |
| pending_review | yes (limited) | no (waiting wall) | no |
| suspended | no | no | hidden |
| banned | no | no | hidden |
| deactivated | no | no | hidden |
| deleted (soft) | no | no | no |
---
## User Status Transitions
```
registered → unverified → active
→ pending_review (company) → active
→ rejected → can re-register
active → suspended (by admin, with reason + duration)
→ banned (by admin, permanent)
→ deactivated (by user, self-service)
suspended → active (after duration expires OR admin lifts)
deactivated → active (user re-activates within 30 days)
deactivated (30+ days) → deleted (soft, automated)
```
---
## Roles & Permissions
### Role Hierarchy
```
superadmin > company > creator
```
### Permission Model
Using Spatie Laravel Permission or custom:
**SuperAdmin Permissions (all of the below plus):**
- manage_users
- manage_companies
- manage_creators
- manage_campaigns
- manage_projects
- moderate_content
- view_analytics
- manage_settings
- manage_reports
- impersonate_users
**Company Permissions:**
- manage_own_profile
- create_campaigns
- manage_own_campaigns
- view_applications
- manage_applications
- invite_creators
- manage_own_projects
- review_deliverables
- send_messages
- leave_reviews
- report_users
**Creator Permissions:**
- manage_own_profile
- manage_portfolio
- browse_campaigns
- apply_to_campaigns
- respond_to_invitations
- manage_own_deliverables
- upload_submissions
- send_messages
- leave_reviews
- report_users
---
## Session Management
- Sessions stored in Redis (for easy invalidation)
- Session data: user_id, role, ip, user_agent, last_active_at
- Admin can view all active sessions for a user
- User can view and kill their own sessions
- Force logout: admin can kill all sessions for a user
---
## Audit Log
Every auth event logged:
| Event | Data Captured |
|-------|--------------|
| login_success | ip, user_agent, location |
| login_failed | ip, user_agent, email_attempted |
| logout | ip |
| password_reset_requested | ip |
| password_reset_completed | ip |
| email_verified | ip |
| account_suspended | admin_id, reason, duration |
| account_banned | admin_id, reason |
| account_deactivated | ip |
| account_reactivated | ip |
| role_changed | admin_id, old_role, new_role |
| impersonation_started | admin_id, target_user_id |
| impersonation_ended | admin_id |
# 03 — Creator Profiles
## Profile Completeness System
Creators have a **profile completion score** (0-100%). This gates visibility:
- < 50% → not visible in search, cannot apply to campaigns
- 50-79% → visible but marked "incomplete profile"
- 80%+ → fully visible, can apply, eligible for recommendations
### Completion Score Breakdown:
| Section | Weight | Required for 50% |
|---------|--------|-------------------|
| Basic info (name, bio, photo) | 20% | yes |
| Country + Language | 10% | yes |
| At least 1 niche selected | 10% | yes |
| At least 1 skill selected | 10% | yes |
| At least 1 portfolio video | 25% | no |
| Social links (at least 1) | 10% | no |
| Equipment listed | 10% | no |
| Availability set | 5% | no |
---
## Profile Fields
### Basic Information
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| display_name | string | yes | min:2, max:50 | Shown everywhere |
| username | string | yes | unique, slug-safe | Set at registration, changeable once per 30 days |
| bio | text | yes | min:20, max:500 | Plain text, no HTML |
| profile_picture | image | yes | jpg/png/webp, max 5MB, min 200x200px | Auto-cropped to square, stored in 3 sizes |
| cover_image | image | no | jpg/png/webp, max 10MB, min 1200x400px | Wide banner format |
| date_of_birth | date | no | must be 18+ | Used for age range filtering, never shown publicly |
| gender | enum | no | male, female, prefer_not_to_say | Used for filtering when companies need specific gender |
### Location & Language
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| country | string | yes | valid ISO 3166-1 |
| city | string | no | max:100 |
| timezone | string | no | valid IANA timezone |
| languages | array | yes (min 1) | from predefined list, max 10 |
| language_proficiency | enum per lang | yes | native, fluent, conversational, basic |
### Professional Information
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| experience_level | enum | yes | beginner (0-1yr), intermediate (1-3yr), advanced (3-5yr), expert (5+yr) |
| content_niches | array | yes (min 1) | from predefined list, max 10 |
| skills | array | yes (min 1) | from predefined list, max 15 |
| video_styles | array | no | from predefined list, max 10 |
| hourly_rate | decimal | no | min:0, max:10000, nullable means "negotiable" |
| project_min_budget | decimal | no | min:0 |
### Content Niches (Predefined List)
```
gaming, technology, apps_software, fitness_health, food_cooking,
beauty, fashion, travel, education, finance, business, automotive,
pets_animals, lifestyle, parenting, home_decor, sports, music,
entertainment, science, diy_crafts, books, photography,
real_estate, saas, ecommerce, crypto_web3
```
### Skills (Predefined List)
```
video_creation, script_writing, voiceover, acting, video_editing,
motion_graphics, product_photography, livestreaming, storytelling,
comedy, unboxing, tutorials, reviews, testimonials, vlogs,
short_form_content, long_form_content, animation, sound_design,
color_grading, thumbnail_design, social_media_management
```
### Video Styles (Predefined List)
```
talking_head, cinematic, vlog_style, screen_recording, tutorial,
unboxing, testimonial, product_demo, lifestyle, comedy_skit,
before_after, day_in_life, asmr, reaction, interview,
stop_motion, timelapse, drone_footage
```
### Equipment
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| camera_type | enum | no | smartphone, dslr, mirrorless, cinema, webcam |
| camera_models | array | no | free text, max 5 entries, max 50 chars each |
| microphone | enum | no | phone_builtin, lav, shotgun, condenser, dynamic, none |
| lighting | enum | no | natural, ring_light, softbox, professional, none |
| has_green_screen | boolean | no | default false |
| has_studio | boolean | no | default false |
| editing_software | array | no | from list: premiere, final_cut, davinci, capcut, filmora, after_effects, other |
### Social Links
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| tiktok | url | no | must match tiktok.com/@username pattern |
| instagram | url | no | must match instagram.com/username pattern |
| youtube | url | no | must match youtube.com/@username or /channel/ pattern |
| facebook | url | no | must match facebook.com/ pattern |
| x_twitter | url | no | must match x.com/username or twitter.com/username |
| snapchat | string | no | username only, max:30 |
| linkedin | url | no | must match linkedin.com/in/ pattern |
| website | url | no | any valid URL |
**Edge Cases for Social Links:**
- URL format varies (with/without www, http/https) → normalize on save
- Platform changes URL structure → validate loosely, store normalized
- User puts full URL in username field → extract username automatically
- Duplicate detection: same social linked to multiple creators → warning only (agencies exist)
### Availability
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| availability_status | enum | no | available, busy, vacation, not_accepting |
| available_from | date | no | must be today or future |
| weekly_capacity_hours | integer | no | 1-80 |
| preferred_project_length | enum | no | one_off, short (1-2 weeks), medium (2-8 weeks), long (2+ months) |
| response_time | enum | no | within_hours, within_day, within_2_days, within_week |
---
## Profile Visibility Rules
| Condition | Visible in Search | Visible via Direct Link |
|-----------|-------------------|------------------------|
| Profile < 50% complete | no | yes (with "incomplete" banner) |
| Profile 50%+ complete | yes | yes |
| User suspended | no | no (404) |
| User deactivated | no | no (404) |
| User banned | no | no (404) |
| Availability = "not_accepting" | yes (marked unavailable) | yes |
---
## Profile Update Rules
- Username: changeable once per 30 days. Old username released after 90 days. Redirect from old username for 90 days.
- Profile picture: immediately live (no moderation queue for now, flag for future)
- Bio: immediate, but flagged for moderation if contains URLs or phone numbers
- All changes logged in activity_log (for admin audit)
- Rate limit: max 50 profile updates per hour (prevent automated scraping/spam)
---
## Profile Verification (Trust Badge)
**Levels:**
1. **Unverified** — just registered
2. **Email Verified** — confirmed email (automatic after verification)
3. **Identity Verified** — submitted government ID (future feature)
4. **Pro Verified** — manual review by admin (proven track record)
**Verification Badge Display:**
- Shown on profile, in search results, in applications
- Companies can filter by verification level
---
## Profile Analytics (Creator's Own Dashboard)
| Metric | Description |
|--------|-------------|
| profile_views | How many times profile was viewed (unique per day) |
| search_appearances | How many times appeared in search results |
| campaign_matches | How many campaigns matched their profile |
| application_success_rate | accepted / total applications |
| invitation_count | How many invitations received |
| response_rate | % of messages responded to within 24h |
| avg_response_time | Average time to respond to messages |
---
## Edge Cases & Special Scenarios
1. **Creator changes country** → affects campaign matching, recalculate recommendations
2. **Creator removes all niches** → profile drops below 50%, hidden from search until fixed
3. **Creator uploads NSFW profile picture** → moderation flag (future: AI detection)
4. **Creator bio contains competitor URLs** → no blocking, but flag for review
5. **Creator adds 50 social links via API spam** → max 1 per platform enforced
6. **Two creators claim same social profile** → no blocking (can't verify ownership without OAuth)
7. **Creator profile in Arabic, company searching in English** → search against both original and translated bio
8. **Creator sets hourly rate to $0** → treat as "negotiable" / "will discuss"
9. **Creator under 18 (date_of_birth check)** → reject registration, show message
10. **Creator wants to switch to company account** → not allowed. Must create new account. (Keep data separate)
# 04 — Company Profiles
## Company Registration → Approval Flow
```
Register → Email Verification → Pending Review → Admin Reviews
├── Approved → Active
└── Rejected → Can Re-apply
```
**Admin Review Checklist:**
- Company name is real (not gibberish)
- Website exists (if provided)
- Contact info seems legitimate
- Not a duplicate of existing company
- Not on any blacklist
---
## Profile Fields
### Basic Information
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| company_name | string | yes | min:2, max:100 | Display name |
| slug | string | auto | auto-generated from name, unique | URL-friendly identifier |
| logo | image | yes | jpg/png/webp/svg, max 5MB, min 200x200px | Square format, stored in 3 sizes |
| cover_image | image | no | jpg/png/webp, max 10MB, min 1200x400px | Banner |
| description | text | yes | min:50, max:2000 | Company description |
| short_description | string | no | max:160 | Used in search results / cards |
| website | url | no | valid URL, must resolve (HEAD check) |
| industry | enum | yes | from predefined list |
| country | string | yes | valid ISO 3166-1 |
| city | string | no | max:100 |
| founded_year | integer | no | 1900-current_year |
| company_size | enum | yes | solo, 2-10, 11-50, 51-200, 201-500, 500+ |
| company_type | enum | no | startup, agency, enterprise, non_profit, government |
### Industries (Predefined List)
```
gaming, technology, saas, ecommerce, fintech, healthtech,
food_beverage, beauty_cosmetics, fashion, automotive,
real_estate, education, media_entertainment, sports,
travel_hospitality, retail, telecommunications,
advertising_marketing, consulting, manufacturing, other
```
### Contact Information
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| contact_email | email | yes | valid email (may differ from login email) |
| contact_phone | string | no | valid phone with country code |
| contact_person_name | string | yes | min:2, max:100 |
| contact_person_role | string | no | max:50 (e.g., "Marketing Manager") |
### Branding Assets
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| brand_colors | array | no | max 5, valid hex codes |
| brand_guidelines_file | file | no | PDF only, max 50MB |
| brand_assets | files | no | zip/pdf/png/jpg, max 100MB total |
| tone_of_voice | enum | no | professional, casual, playful, bold, minimal, luxury |
### Social Presence
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| linkedin | url | no | linkedin.com/company/ pattern |
| instagram | url | no | instagram.com/ pattern |
| tiktok | url | no | tiktok.com/@ pattern |
| youtube | url | no | youtube.com/ pattern |
| x_twitter | url | no | x.com/ pattern |
| facebook | url | no | facebook.com/ pattern |
---
## Company Statuses
| Status | Can Login | Can Post Campaigns | Visible | Notes |
|--------|-----------|-------------------|---------|-------|
| pending_review | yes (limited view) | no | no | Waiting for admin |
| active | yes | yes | yes | Normal operation |
| suspended | no | no | hidden | Temporary, with reason |
| banned | no | no | hidden | Permanent |
| deactivated | no | no | hidden | Self-service |
---
## Company Verification Tiers
| Tier | Badge | Requirements | Benefits |
|------|-------|-------------|----------|
| Basic | none | Registered + approved | Can post campaigns |
| Verified | blue badge-check icon | Admin manual verification | Higher in search, trust badge |
| Premium | gold star icon | Paid plan OR high activity | Priority support, featured placement |
**Verification criteria (admin manual):**
- Real website with matching branding
- LinkedIn company page exists
- Has posted at least 3 campaigns
- At least 1 completed project with positive review
- OR: admin manually verifies via video call / documents
---
## Company Dashboard Sections
### Overview
- Active campaigns count
- Active projects count
- Pending applications count
- Pending deliverables to review
- Total spend (all time)
- Average creator rating given
### My Campaigns
- Draft / Active / Paused / Completed / Cancelled tabs
- Quick stats per campaign (applications, views, deadline)
### My Projects
- In Progress / Waiting Review / Completed tabs
- Creator info, progress, deadline
### Discover Creators
- Search + filters
- Saved creators (favorites)
- Recent views
### Messages
- Conversations with creators
- Unread count
### Reviews
- Reviews I've given
- Reviews I've received
### Settings
- Company profile
- Team members (future)
- Billing (future)
- Notifications preferences
---
## Company-Specific Edge Cases
1. **Company changes name** → slug stays the same (SEO continuity). Admin can approve slug change separately.
2. **Company uploads brand guidelines as .exe** → reject, only PDF allowed
3. **Company description in only Arabic** → fine, but suggest adding English version
4. **Company has 0 campaigns after 6 months** → send re-engagement email, no automatic action
5. **Company creates campaign while suspended** → impossible (policy check blocks)
6. **Company account used by multiple people** → sessions table shows multiple locations, no blocking (team use is valid)
7. **Company rejected, creates new account with same name** → allow, admin will see previous rejection in review
8. **Company wants to merge two accounts** → admin-only action, involves migrating campaigns/projects
9. **Company profile has no logo** → cannot post campaigns (logo required for public visibility)
10. **Company's website returns 404** → flag for admin, don't block (sites go down temporarily)
---
## Company Activity Feed (Admin View)
All company actions logged:
- Campaign created/edited/published/paused/cancelled
- Application accepted/rejected
- Invitation sent
- Deliverable approved/revision requested
- Message sent
- Review posted
- Profile updated
- Login/logout events
---
## Company Public Profile
What creators see when viewing a company:
- Logo, name, description
- Industry, location, size
- Verification badge
- Active campaigns (public ones)
- Stats: total projects completed, avg creator rating, member since
- Reviews from creators
- Response time badge (based on message response speed)
# 05 — Campaigns
## What is a Campaign?
A campaign is a company's request for UGC content. It defines what they need, who they need, and when they need it. Think of it as a "job posting" for content creation.
---
## Campaign Lifecycle
```
Draft → Published → Active (accepting applications)
├── Deadline Passed → Closed (no new applications)
├── Manually Closed → Closed
├── Paused → can resume to Active
└── Cancelled → terminal state
Closed → Creators Selected → Projects Created → Campaign Completed
```
### Status Definitions
| Status | Description | Public Visibility |
|--------|-------------|-------------------|
| draft | Being written, not visible | no |
| published | Live, accepting applications | yes |
| paused | Temporarily hidden, no new applications | no (existing applicants notified) |
| closed | Deadline passed or manually closed, selecting creators | no (but visible to applicants) |
| in_progress | Creators selected, projects underway | no |
| completed | All projects delivered and approved | no (archived) |
| cancelled | Company cancelled before completion | no (applicants notified) |
---
## Campaign Fields
### Core Information
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| title | string | yes | min:10, max:100 | Clear, descriptive |
| slug | string | auto | unique, from title | URL identifier |
| description | text | yes | min:100, max:5000 | Rich text (limited: bold, italic, lists, links) |
| objective | enum | yes | see list below | What the company wants to achieve |
| product_service_name | string | no | max:100 | What product/service the UGC is for |
| product_url | url | no | valid URL | Link to product being promoted |
### Campaign Objectives
```
brand_awareness, product_launch, app_install, testimonial,
tutorial_howto, unboxing_review, social_proof, event_promotion,
seasonal_campaign, ongoing_content, user_engagement, other
```
### Requirements
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| creator_count | integer | yes | 1-100 | How many creators needed |
| gender_preference | enum | no | any, male, female | null = any |
| age_range_min | integer | no | 18-65 | null = no minimum |
| age_range_max | integer | no | 18-65 | null = no maximum |
| countries | array | no | valid ISO codes | Empty = any country |
| languages | array | yes (min 1) | from predefined list | Content must be in these languages |
| niches | array | yes (min 1) | from predefined list | Relevant niches |
| experience_level_min | enum | no | beginner, intermediate, advanced, expert | null = any |
| equipment_requirements | text | no | max:500 | Free text description |
| specific_skills | array | no | from predefined list | Required skills |
### Deliverables
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| deliverable_type | enum | yes | video, image, script, raw_footage, mixed | Primary deliverable |
| video_count | integer | conditional | 1-50 | Required if video type |
| video_duration_min | integer | no | seconds, 5-3600 |
| video_duration_max | integer | no | seconds, 5-3600 |
| aspect_ratio | enum | no | 9:16, 16:9, 1:1, 4:5, any | |
| format_notes | text | no | max:1000 | Additional format requirements |
| includes_raw_footage | boolean | no | default false | Must also deliver raw files |
| includes_script_approval | boolean | no | default false | Script must be approved before filming |
| max_revisions | integer | no | 0-10, default 2 | How many revision rounds included |
### Budget & Compensation
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| budget_type | enum | yes | fixed, per_creator, negotiable, product_only | |
| budget_amount | decimal | conditional | min:0 | Required if fixed or per_creator |
| budget_currency | string | conditional | valid currency code | Required if amount set |
| product_provided | boolean | no | default false | Is physical product sent to creator? |
| product_value | decimal | no | min:0 | Value of product being sent |
| payment_terms | text | no | max:500 | When/how payment happens |
### Timeline
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| application_deadline | datetime | yes | must be future, max 90 days out | When applications close |
| project_start_date | date | no | must be after application_deadline | When work begins |
| project_deadline | date | yes | must be after start_date | Final delivery date |
| estimated_turnaround | enum | no | 1_week, 2_weeks, 1_month, 2_months, flexible | |
### Attachments
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| brand_guidelines | file | no | PDF, max 50MB |
| reference_videos | array of urls | no | max 10 URLs (YouTube, TikTok, etc.) |
| product_images | files | no | jpg/png/webp, max 10 files, max 10MB each |
| scripts | files | no | PDF/DOC/TXT, max 5 files, max 10MB each |
| mood_board | files | no | jpg/png/pdf, max 10 files, max 20MB each |
| additional_docs | files | no | any common format, max 5 files, max 20MB each |
### Visibility & Discovery
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| visibility | enum | yes | public, unlisted, invite_only | |
| featured | boolean | admin only | | Admin can feature campaigns |
| allow_applications | boolean | yes | default true | Can creators apply? |
| auto_close_on_count | boolean | no | default false | Close when creator_count applications received? |
---
## Campaign Discovery (Creator Side)
### Search
- Full-text search on title + description
- Supports Arabic and English simultaneously
### Filters
| Filter | Type | Options |
|--------|------|---------|
| Niche/Category | multi-select | from predefined list |
| Country | multi-select | ISO countries |
| Language | multi-select | from list |
| Budget type | multi-select | fixed, per_creator, negotiable, product_only |
| Budget range | range slider | min-max in USD equivalent |
| Deliverable type | multi-select | video, image, script, etc. |
| Experience level | multi-select | beginner-expert |
| Deadline | range | this week, this month, next month, any |
| Posted date | sort/filter | last 24h, last week, last month, any |
### Sorting
| Sort | Description |
|------|-------------|
| newest | Most recently published |
| deadline_soon | Closest deadline first |
| highest_budget | Highest budget first |
| most_applicants | Most popular campaigns |
| fewest_applicants | Less competition |
| best_match | Algorithmic match score (based on creator profile) |
### Campaign Cards (Search Results)
Display:
- Company logo + name + verification badge
- Campaign title
- Budget (or "Negotiable" / "Product Only")
- Deadline (with urgency indicator if < 3 days)
- Required deliverable type icon
- Countries accepted (or "Worldwide")
- Languages required
- Application count
- Match score (if logged in as creator)
---
## Campaign Detail Page (Public)
### Visible to Everyone:
- Full campaign description
- Company info (logo, name, badge, member since)
- Requirements breakdown
- Deliverable specs
- Timeline
- Budget info
- Reference materials (images only, not downloadable files)
- Application count
- "Apply" button (or "Login to Apply")
### Visible Only After Applying:
- Downloadable attachments (brand guidelines, scripts, etc.)
- Company contact info
### NOT Visible to Creators:
- Other applicants' identities
- Application status of others
- Company's internal notes on applications
---
## Campaign Creation Flow (Company Side)
### Step-by-Step Wizard:
1. **Basics** — Title, description, objective, product info
2. **Requirements** — Creator requirements (gender, age, country, skills, etc.)
3. **Deliverables** — What to deliver (video specs, format, revisions)
4. **Budget** — Compensation details
5. **Timeline** — Deadlines
6. **Attachments** — Upload brand materials
7. **Review** — Preview everything, publish or save as draft
**Save as Draft** available at every step. Can return and edit any step.
### Campaign Templates
Companies can:
- Save a campaign as template
- Create new campaign from template
- Admin provides global templates (by industry)
---
## Campaign Edge Cases
1. **Company edits campaign after creators applied** → allowed for minor edits (typos, adding attachments). Major changes (budget decrease, deadline brought forward, deliverable changes) → notify all applicants with "campaign updated" alert + option to withdraw
2. **Campaign deadline passes with 0 applications** → auto-status to "closed", send company email suggesting they improve their campaign
3. **Campaign reaches creator_count applications** → if auto_close_on_count is true, stop accepting. Otherwise keep accepting (company picks best)
4. **Company cancels campaign with accepted creators** → all accepted creators notified, any active projects marked "cancelled by company"
5. **Campaign has 500 applications** → UI must paginate, company needs filtering/sorting tools for applications
6. **Duplicate campaign detection** → if same company posts nearly identical campaign (>80% text similarity) within 7 days, warn "you already have a similar campaign"
7. **Campaign in Arabic only** → perfectly valid (MENA-first), but suggest adding English title for broader reach
8. **Campaign budget is $0 with no product** → allow but label as "exposure only" (controversial but exists in market)
9. **Campaign references illegal content** → moderation flag, admin review before publishing (future: automated)
10. **Campaign published on Friday, deadline Monday** → warn company "very short timeline, may get fewer applications"
11. **Company not active (suspended) but has active campaigns** → all campaigns auto-paused, applicants notified
12. **Invite-only campaign but company sends 0 invites** → reminder email after 48 hours
---
## Campaign Analytics (Company View)
| Metric | Description |
|--------|-------------|
| views | Total campaign page views |
| unique_views | Unique creator views |
| application_count | Total applications |
| application_rate | applications / views |
| avg_applicant_rating | Average reputation score of applicants |
| time_to_first_application | How fast first app came in |
| source_breakdown | How creators found this (search, recommendation, direct link) |
# 06 — Applications & Invitations
## Two Paths to a Project
```
Path A: Creator → applies to Campaign → Company accepts → Project
Path B: Company → invites Creator → Creator accepts → Project
```
Both converge into the same Project entity.
---
## APPLICATIONS (Creator → Campaign)
### Application Fields
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| campaign_id | FK | yes | must exist, must be published | |
| creator_id | FK | auto | current user | |
| cover_message | text | yes | min:50, max:1000 | Why they're a good fit |
| portfolio_items | array of FK | no | max 5, must belong to creator | Reference specific portfolio pieces |
| proposed_budget | decimal | no | min:0 | Creator's price (if campaign is negotiable) |
| proposed_timeline | text | no | max:200 | Creator's delivery estimate |
| availability_confirmed | boolean | yes | must be true | "I can deliver by the deadline" |
| questions_for_company | text | no | max:500 | Any questions before starting |
### Application Status Flow
```
submitted → viewed → shortlisted → accepted → project_created
→ rejected
→ withdrawn (by creator)
```
| Status | Description | Who Changes | Creator Sees |
|--------|-------------|-------------|-------------|
| submitted | Just applied | auto | "Applied" |
| viewed | Company opened application | auto | "Viewed" (check icon) |
| shortlisted | Company marked as potential | company | "Shortlisted" (star icon) |
| accepted | Company wants to work together | company | "Accepted" (check-circle icon) |
| rejected | Company passed | company | "Not selected" |
| withdrawn | Creator cancelled | creator | "Withdrawn" |
### Application Rules
1. **One application per creator per campaign** — cannot apply twice
2. **Cannot apply to own company's campaign** — (if somehow both roles exist)
3. **Cannot apply if profile < 50% complete** — must complete profile first
4. **Cannot apply if currently suspended** — blocked
5. **Cannot apply after deadline** — form disabled
6. **Cannot apply to invite-only campaigns** — button not shown
7. **Cannot apply if campaign is paused/closed/cancelled** — blocked
### Application Actions
**Creator Can:**
- Submit application
- Edit application (only while status = submitted, before company views)
- Withdraw application (any time before accepted)
- View own application status
**Company Can:**
- View all applications for their campaign
- Filter/sort applications
- Mark as shortlisted
- Accept application (creates project)
- Reject application (with optional reason)
- Bulk reject (for mass cleanup)
- Download applicant list (CSV export)
### Application Notifications
| Event | Notify Who | Channel |
|-------|-----------|---------|
| New application | Company | in-app + email |
| Application viewed | Creator | in-app only |
| Shortlisted | Creator | in-app + email |
| Accepted | Creator | in-app + email + push |
| Rejected | Creator | in-app + email |
| Company message on application | Creator | in-app + email |
### Application Edge Cases
1. **Creator applies, then gets suspended** → application hidden from company view, status set to "withdrawn (system)"
2. **Company accepts more creators than campaign.creator_count** → allowed (soft limit, just a target)
3. **Creator withdraws after being shortlisted** → company notified, status = withdrawn
4. **Company rejects then wants to accept** → allowed (status can go from rejected → accepted, rare but valid)
5. **Campaign cancelled after applications received** → all applications set to "cancelled" status, creators notified
6. **Creator applies to 50 campaigns simultaneously** → allowed, no limit (but rate limit form submission to prevent spam)
7. **Application cover message is just "hi"** → min:50 validation prevents this
8. **Creator has no portfolio but applies** → allowed if profile is 50%+ (portfolio not strictly required)
9. **Company never responds to applications** → after 30 days with no action, creator sees "no response" indicator
10. **Duplicate application attempt via API** → 422 error "You have already applied to this campaign"
---
## INVITATIONS (Company → Creator)
### Invitation Fields
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| company_id | FK | auto | current company | |
| creator_id | FK | yes | must exist, must be active | |
| campaign_id | FK | no | if linked to specific campaign | Can invite without campaign |
| message | text | yes | min:20, max:1000 | Personal message to creator |
| proposed_budget | decimal | no | min:0 | What company is offering |
| proposed_scope | text | no | max:500 | Brief description of work |
| deadline | date | no | must be future | Expected delivery |
| expires_at | datetime | yes | default: 7 days from now | Auto-expire if no response |
### Invitation Status Flow
```
sent → viewed → accepted → project_created
→ declined
→ expired (no response by expires_at)
→ cancelled (by company)
```
| Status | Description | Creator Sees |
|--------|-------------|-------------|
| sent | Invitation created | "New Invitation" |
| viewed | Creator opened it | (no change visible) |
| accepted | Creator wants to work | "Accepted" |
| declined | Creator said no | "Declined" |
| expired | No response in time | "Expired" |
| cancelled | Company withdrew | "Cancelled" |
### Invitation Rules
1. **One active invitation per company-creator pair per campaign** — no spam
2. **Cannot invite suspended/banned creators** — blocked
3. **Cannot invite if company is not active** — blocked
4. **Max 50 invitations per company per day** — rate limit
5. **Cannot invite creator who already applied to same campaign** — show "already applied" instead
6. **Expired invitations count toward daily limit** — prevents spam-then-expire abuse
### Invitation Actions
**Company Can:**
- Send invitation
- Cancel invitation (before accepted)
- Resend invitation (resets expiry, max 2 resends)
- Bulk invite from search results (up to 10 at once)
- View invitation history
**Creator Can:**
- View invitation details
- Accept invitation (triggers project creation or negotiation)
- Decline invitation (with optional reason)
- Ask questions (via messaging, before accepting)
### Invitation Notifications
| Event | Notify Who | Channel |
|-------|-----------|---------|
| New invitation | Creator | in-app + email + push |
| Invitation accepted | Company | in-app + email |
| Invitation declined | Company | in-app + email |
| Invitation expiring (24h left) | Creator | in-app + email |
| Invitation expired | Company | in-app |
| Invitation cancelled | Creator | in-app |
### Invitation Edge Cases
1. **Company invites creator whose availability = "not_accepting"** → allowed but show warning to company "this creator may not be available"
2. **Creator declines, company sends another invitation** → allowed, but after 3 declines show "this creator has declined multiple times"
3. **Invitation expires, company resends** → counts as new invitation (new expiry), max 2 resends total
4. **Creator accepts but company got suspended between send and accept** → auto-cancel project, notify creator
5. **Creator blocks company** → future invitations silently hidden (company still sees "sent" but creator never sees it)
6. **Company sends 100 invitations with same generic message** → spam detection: if same message used >10 times in 24h, flag for review
7. **Invitation to a campaign that's now closed** → still valid (invitation bypasses campaign deadline)
8. **Creator accepts invitation to campaign they also applied to** → application auto-withdrawn, invitation acceptance takes priority
---
## From Acceptance to Project
When either path results in acceptance:
**Application Accepted:**
1. Application status → "accepted"
2. Project created with:
- campaign_id
- company_id (from campaign)
- creator_id (from application)
- Deliverables copied from campaign requirements
- Timeline from campaign
- Budget from campaign (or negotiated)
3. Other applicants NOT auto-rejected (company may accept multiple)
**Invitation Accepted:**
1. Invitation status → "accepted"
2. Project created with:
- campaign_id (if invitation was campaign-linked)
- company_id (from invitation)
- creator_id (from invitation)
- Deliverables from invitation scope (or campaign if linked)
- Timeline from invitation
- Budget from invitation
In both cases → Project enters "not_started" status, both parties notified.
# 07 — Projects
## What is a Project?
A Project is the working relationship between ONE company and ONE creator for a specific piece of work. It's created when an application is accepted or an invitation is accepted.
Every project has clear deliverables, a timeline, and a structured review process.
---
## Project Lifecycle
```
not_started → in_progress → waiting_review → approved → completed
↗ ↘
revision_requested ←←←←←←←←←
in_progress (creator fixes)
Any state → cancelled (by either party or admin)
Any state → disputed (escalated to admin)
```
### Status Definitions
| Status | Description | Who Triggers |
|--------|-------------|-------------|
| not_started | Project created, hasn't begun | auto (on creation) |
| in_progress | Creator is actively working | creator (marks "started") |
| waiting_review | Creator submitted deliverable(s) | auto (on submission) |
| revision_requested | Company wants changes | company |
| approved | All deliverables accepted | company (or auto after all approved) |
| completed | Project fully done, both parties confirm | auto (after approval + confirmation period) |
| cancelled | Terminated early | either party OR admin |
| disputed | Escalated to admin for resolution | either party |
| on_hold | Temporarily paused by mutual agreement | either party (other must confirm) |
---
## Project Fields
| Field | Type | Source | Notes |
|-------|------|--------|-------|
| uuid | uuid | auto | Public identifier |
| campaign_id | FK | nullable | Not all projects come from campaigns |
| company_id | FK | required | |
| creator_id | FK | required | |
| application_id | FK | nullable | If originated from application |
| invitation_id | FK | nullable | If originated from invitation |
| title | string | from campaign or invitation | Editable by company |
| description | text | from campaign or invitation | Editable by company |
| status | enum | see above | |
| budget_amount | decimal | from acceptance | |
| budget_currency | string | from acceptance | |
| started_at | datetime | when creator marks started | |
| deadline | date | from campaign/invitation | |
| completed_at | datetime | when marked completed | |
| cancelled_at | datetime | if cancelled | |
| cancelled_by | FK (user) | who cancelled | |
| cancellation_reason | text | required on cancel | |
| revision_count | integer | starts at 0 | Tracks total revisions used |
| max_revisions | integer | from campaign | After max, company can still request (flagged) |
---
## Project Dashboard (Both Sides See)
### Header
- Project title
- Status badge (colored)
- Company logo + name | Creator avatar + name
- Deadline with countdown (red if < 3 days)
- Budget amount
### Tabs
1. **Overview** — description, requirements, timeline
2. **Deliverables** — list of required deliverables + their statuses
3. **Activity** — timeline of all events (submissions, reviews, messages)
4. **Files** — all shared files (brand assets, submissions, raw footage)
5. **Messages** — project-specific conversation thread
---
## Project Actions
### Creator Can:
- Mark project as "started" (not_started → in_progress)
- Submit deliverables
- Upload files
- Send messages in project thread
- Request deadline extension (company must approve)
- Request to cancel (requires reason, company must confirm OR admin intervenes)
- Escalate to dispute
### Company Can:
- View all deliverables and submissions
- Approve deliverable
- Request revision (with feedback)
- Approve entire project (all deliverables done)
- Cancel project (with reason, before completion)
- Extend deadline
- Escalate to dispute
### Admin Can:
- View any project
- Force status change
- Resolve disputes
- Cancel project on behalf of either party
- Add admin notes (not visible to either party)
---
## Deadline Management
### Deadline Extension Flow:
```
Creator requests extension → Company receives notification
→ Company approves → deadline updated, both notified
→ Company rejects → creator must deliver by original date
→ No response in 48h → auto-reminder to company
```
### Overdue Projects:
- Deadline passes while status = in_progress or not_started:
1. Day 0: both parties notified "project is overdue"
2. Day 3: admin flagged for review
3. Day 7: admin may intervene
4. Day 14: auto-escalate to dispute if no resolution
---
## Project Cancellation Rules
### Creator Cancels:
- Before starting (not_started) → minimal impact, no penalty
- After starting (in_progress) → noted on profile, company can leave review
- After submitting (waiting_review) → unusual, flagged for admin
### Company Cancels:
- Before creator starts → no impact
- After creator starts → creator can leave review, admin notified
- After deliverable submitted → requires admin approval (creator did the work)
### Cancellation Cooldown:
- If a user cancels 3+ projects in 30 days → admin notified for pattern review
- Cancellation rate shown in reputation (see doc 11)
---
## Project Edge Cases
1. **Creator submits, company never reviews** → after 7 days of no response, creator can escalate. After 14 days, admin can auto-approve.
2. **Company requests revisions beyond max_revisions** → allowed but: (a) creator sees "extra revision" warning, (b) creator can decline additional revisions, (c) metric tracked for admin
3. **Both parties want to cancel** → instant mutual cancellation, no penalties
4. **Creator's account gets suspended mid-project** → project put on hold, admin reviews. Company notified.
5. **Company's account gets suspended mid-project** → project put on hold, admin reviews. Creator's work protected.
6. **Project created but deadline already passed** → shouldn't happen (validation). If somehow occurs, auto-flag for admin.
7. **Creator submits wrong file type** → deliverable validation catches this (see doc 08)
8. **Project has 0 deliverables defined** → can't happen (at least 1 required on creation)
9. **Creator marks "started" but never submits** → overdue flow kicks in at deadline
10. **Company approves then wants to un-approve** → not allowed. Must create new revision request explaining why.
11. **Project dispute while deliverable is in review** → project frozen, neither party can submit/review until admin resolves
12. **Creator delivers 3 of 5 videos, then goes silent** → partial completion tracked per deliverable, overdue flow for remaining
---
## Project Activity Log
Every action creates an activity entry:
```
[2024-03-15 14:30] Creator started working on project
[2024-03-18 09:15] Creator submitted "Product Review Video" (v1)
[2024-03-19 11:00] Company viewed submission
[2024-03-19 11:30] Company requested revision: "Please re-record intro..."
[2024-03-20 16:45] Creator submitted "Product Review Video" (v2)
[2024-03-21 10:00] Company approved "Product Review Video"
[2024-03-21 10:01] All deliverables approved — project completed
```
This is immutable (no editing/deleting activity entries).
# 08 — Deliverables & Video Review System
## Deliverable Structure
Each project has 1+ deliverables. Each deliverable is a discrete item the creator must deliver.
---
## Deliverable Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | |
| title | string | yes | min:5, max:100 |
| description | text | no | max:1000 |
| type | enum | yes | video, image, script, raw_footage, audio, document |
| sort_order | integer | yes | display position |
| status | enum | auto | see below |
| specifications | JSON | no | type-specific specs |
| max_revisions | integer | no | override project-level default |
| revision_count | integer | auto | starts at 0 |
| due_date | date | no | must be ≤ project deadline |
| approved_at | datetime | auto | when company approves |
| approved_by | FK | auto | which company user approved |
### Type-Specific Specifications (JSON)
**Video:**
```json
{
"min_duration_seconds": 30,
"max_duration_seconds": 60,
"aspect_ratio": "9:16",
"resolution_min": "1080p",
"format": ["mp4", "mov"],
"includes_captions": true,
"includes_music": false,
"includes_raw": false
}
```
**Image:**
```json
{
"min_width": 1080,
"min_height": 1080,
"aspect_ratio": "1:1",
"format": ["jpg", "png"],
"count": 3
}
```
**Script:**
```json
{
"max_words": 500,
"language": "ar",
"format": ["pdf", "docx", "txt"]
}
```
---
## Deliverable Status Flow
```
pending → submitted → under_review → approved
→ revision_requested → submitted (loop)
pending → overdue (deadline passed, nothing submitted)
```
| Status | Description | Visual |
|--------|-------------|--------|
| pending | Awaiting creator submission | Gray |
| submitted | Creator uploaded, waiting for review | Blue |
| under_review | Company is actively reviewing | Yellow |
| revision_requested | Company wants changes | Orange |
| approved | Company accepted this deliverable | Green |
| overdue | Past due_date with no submission | Red |
---
## Submission System
### Creating a Submission
Each time a creator submits (or resubmits after revision), a new **Submission** record is created. This preserves version history.
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| deliverable_id | FK | yes | |
| version | integer | auto | 1, 2, 3... (auto-increment per deliverable) |
| creator_id | FK | auto | |
| type | enum | yes | matches deliverable type |
| notes | text | no | max:500, creator's notes about this version |
| submitted_at | datetime | auto | |
### Submission Files
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| submission_id | FK | yes | |
| file_type | enum | yes | video, image, document, audio, archive |
| file_path | string | yes | S3/storage path |
| file_name | string | yes | original filename |
| file_size | integer | yes | bytes |
| mime_type | string | yes | validated mime |
| peertube_uuid | string | nullable | if video, the PeerTube video UUID |
| peertube_embed_url | string | nullable | for video player embedding |
| thumbnail_path | string | nullable | auto-generated or from PeerTube |
| duration_seconds | integer | nullable | for video/audio |
| width | integer | nullable | for images/video |
| height | integer | nullable | for images/video |
| metadata | JSON | nullable | additional file metadata |
---
## Video Upload Flow (PeerTube Integration)
### Upload Process:
```
Creator clicks "Upload Video"
→ File uploaded to application server first (temp storage)
→ Server validates (size, format, duration)
→ Server uploads to PeerTube via API (resumable for large files)
→ PeerTube processes (transcoding)
→ Server receives PeerTube webhook/polls for status
→ Submission record created with peertube_uuid
→ Creator sees "Processing..." then "Ready"
→ Company can view via embedded PeerTube player
```
### PeerTube Upload Configuration:
- Privacy: `3` (Private) — only accessible via direct embed URL
- Channel: one channel per company (auto-created)
- Tags: project_id, deliverable_id (for organization)
- Name: `{project_title} - {deliverable_title} v{version}`
### Video Validation Rules:
| Rule | Value | Error Message |
|------|-------|---------------|
| Max file size | 2GB | "Video must be under 2GB" |
| Min duration | 5 seconds | "Video too short" |
| Max duration | based on deliverable spec | "Video exceeds maximum duration" |
| Allowed formats | mp4, mov, webm, mkv, avi | "Unsupported video format" |
| Min resolution | 720p | "Video resolution too low" |
### Video Processing States:
```
uploading → processing (PeerTube transcoding) → ready → available_for_review
→ failed (PeerTube error) → retry (max 3 attempts)
```
---
## Review System (Company Side)
### Viewing a Submission
- Video plays in embedded PeerTube player
- Side panel shows submission notes
- Below: review actions
### Review Actions:
1. **Approve** — deliverable marked approved, creator notified
2. **Request Revision** — must provide feedback
### Revision Request Fields:
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| deliverable_id | FK | yes | |
| submission_id | FK | yes | which version this feedback is for |
| feedback | text | yes | min:20, max:2000 |
| timestamp_comments | array | no | see below |
| priority | enum | no | minor, major, critical |
| revision_deadline | date | no | when the revision is expected |
---
## Timestamp Comments (Video-Specific)
Companies can leave feedback at specific points in a video:
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| submission_id | FK | yes | |
| timestamp_seconds | decimal | yes | 0 to video_duration |
| comment | text | yes | min:5, max:500 |
| type | enum | no | suggestion, issue, praise |
| resolved | boolean | auto | starts false |
### Timestamp Comment UX:
- Company watches video
- Clicks at a specific moment → video pauses
- Types comment → comment anchored to that timestamp
- Creator sees comments as markers on video timeline
- Creator can mark comments as "resolved" in next version
### Edge Cases for Timestamp Comments:
1. **Video re-uploaded with different duration** → old timestamp comments preserved but may not align. Show with "[alert-triangle icon] from previous version" label.
2. **Comment at 0:00** → valid (intro feedback)
3. **Comment at last second** → valid (outro feedback)
4. **50+ comments on one video** → paginate in sidebar, all markers visible on timeline
5. **Comment references a frame that doesn't exist in new version** → keep comment, mark as "may no longer apply"
---
## Revision Flow
```
Company requests revision:
1. Deliverable status → revision_requested
2. revision_count incremented
3. Creator notified with feedback
4. Creator uploads new version (new submission, version+1)
5. Deliverable status → submitted
6. Company reviews again
7. Repeat or approve
```
### Revision Limits:
- Each deliverable has max_revisions (default: 2 from campaign, overridable)
- When revision_count reaches max:
- Company CAN still request more
- Creator sees: "This is an extra revision beyond the agreed limit"
- Creator can: accept and revise, OR decline the extra revision
- If declined → company can: approve current version, escalate to dispute, or cancel
### Revision Edge Cases:
1. **Company approves then realizes mistake** → cannot un-approve. Must contact admin.
2. **Creator submits revision without addressing feedback** → no system enforcement (subjective), company can request again
3. **Revision requested but creator goes silent** → overdue flow after deadline
4. **Company gives contradictory feedback across versions** → creator can note this in submission notes, escalate if needed
5. **Version history has 10+ versions** → UI must handle gracefully (scrollable list, comparison view)
---
## Version Comparison
For each deliverable, the review page shows:
- Current version (video player or file preview)
- Version history dropdown (v1, v2, v3...)
- Side-by-side comparison (for images)
- For videos: can switch between versions in player
---
## Deliverable Approval → Project Completion
```
When ALL deliverables in a project are approved:
→ Project status auto-changes to "approved"
→ 48-hour confirmation period starts
→ If no disputes raised in 48h → project "completed"
→ Both parties prompted to leave reviews
```
---
## File Storage Architecture
```
/projects/{project_uuid}/
├── brand_assets/ (company uploads)
│ ├── guidelines.pdf
│ └── product_photos/
├── deliverables/
│ ├── {deliverable_uuid}/
│ │ ├── v1/
│ │ │ ├── video.mp4 (or PeerTube reference)
│ │ │ └── raw/ (if raw footage required)
│ │ ├── v2/
│ │ └── v3/
│ └── {deliverable_uuid_2}/
└── messages/ (message attachments)
└── {message_id}/
```
---
## Bulk Operations
### Company Managing Multiple Deliverables:
- "Approve All" button (only if all are in submitted/under_review state)
- "Download All" — zip all latest approved versions
- "Request Revisions" — can apply same note to multiple deliverables
### Creator Submitting Multiple Files:
- Drag-and-drop multiple files → auto-assign to correct deliverables (by naming convention or manual mapping)
- Batch upload progress indicator
# 09 — Creator Portfolios
## What is a Portfolio?
A creator's portfolio is their showreel — the videos and content that demonstrate their abilities. It's the primary tool companies use to evaluate creators before hiring.
Videos are stored on PeerTube (same instance as deliverables, different channel).
---
## Portfolio Structure
```
Creator
└── Portfolio
├── Collection: "Gaming Content"
│ ├── Video 1
│ ├── Video 2
│ └── Video 3
├── Collection: "Product Reviews"
│ ├── Video 4
│ └── Video 5
└── Uncategorized
└── Video 6
```
---
## Portfolio Item Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| creator_id | FK | auto | |
| collection_id | FK | no | nullable = uncategorized |
| title | string | yes | min:3, max:100 |
| description | text | no | max:500 |
| type | enum | yes | video, image, link |
| peertube_uuid | string | conditional | required for video type |
| peertube_embed_url | string | conditional | auto-generated |
| thumbnail_url | string | auto | from PeerTube or uploaded |
| external_url | url | conditional | for link type (YouTube, TikTok, etc.) |
| external_platform | enum | conditional | youtube, tiktok, instagram, other |
| file_path | string | conditional | for image type |
| duration_seconds | integer | nullable | for videos |
| tags | array | no | max 10, from predefined + custom |
| industry | enum | no | from predefined list |
| brand_worked_with | string | no | max:100, free text |
| is_featured | boolean | no | max 3 featured items |
| visibility | enum | yes | public, private, unlisted |
| sort_order | integer | auto | within collection |
| views_count | integer | auto | unique views |
| created_at | datetime | auto | |
---
## Portfolio Collections
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| creator_id | FK | auto | |
| name | string | yes | min:2, max:50 |
| description | text | no | max:200 |
| sort_order | integer | auto | |
| visibility | enum | yes | public, private |
**Rules:**
- Max 20 collections per creator
- Max 50 items per collection
- Max 100 total portfolio items per creator
- Uncategorized is implicit (items without collection_id)
---
## Portfolio Video Upload Flow
```
Creator clicks "Add to Portfolio"
→ Choose: Upload new video OR Link external video
Upload:
→ File uploaded to temp storage
→ Validated (format, size, duration)
→ Uploaded to PeerTube (creator's portfolio channel)
→ PeerTube privacy: Unlisted (accessible via embed but not in PeerTube search)
→ Processing → Ready
→ Portfolio item created
External Link:
→ Creator pastes YouTube/TikTok/Instagram URL
→ System extracts metadata (title, thumbnail, duration via oEmbed)
→ Portfolio item created with external_url
→ Thumbnail stored locally (in case external source changes)
```
### PeerTube Portfolio Channel Strategy:
- Each creator gets ONE PeerTube channel: `portfolio_{creator_username}`
- Auto-created on first portfolio upload
- All portfolio videos go here (privacy: unlisted)
- Channel is not publicly listed on PeerTube instance
---
## Portfolio Visibility
| Visibility | Who Can See | Where It Appears |
|------------|-------------|-----------------|
| public | Everyone | Profile, search, applications |
| unlisted | Anyone with direct link | Applications (if creator shares) |
| private | Creator only | Nowhere (draft/archive) |
---
## Featured Items
- Creator can mark up to 3 items as "featured"
- Featured items appear at top of their public profile
- Featured items are prioritized in search results card preview
---
## Portfolio in Applications
When applying to a campaign, creators can:
- Reference specific portfolio items (attach up to 5)
- These are shown inline in the company's application review
- Company can view full portfolio from the application page
---
## Portfolio Analytics (Creator View)
| Metric | Description |
|--------|-------------|
| total_views | Sum of all portfolio item views |
| most_viewed | Which items get the most attention |
| views_from_companies | Views where viewer was a company user |
| views_from_campaigns | Views triggered from application review |
| views_this_week | Trend indicator |
---
## Portfolio Edge Cases
1. **Creator uploads 4K video (huge file)** → max 2GB enforced, suggest compression tips in error message
2. **External link (YouTube) video gets deleted** → periodic check (weekly cron), mark as "unavailable", notify creator
3. **Creator uploads copyrighted content** → no automated check (future: fingerprinting). Reportable by others.
4. **Creator's PeerTube channel gets full** → PeerTube quotas configured per-user (generous: 50GB per creator for portfolio)
5. **Portfolio item embedded on external site** → allowed if public/unlisted (PeerTube handles embed permissions)
6. **Creator wants to reorder items** → drag-and-drop UI, sort_order updated in batch
7. **Creator deletes portfolio item that's referenced in an active application** → reference preserved as "item no longer available", company notified
8. **Company downloads/screen-records portfolio video** → can't prevent, but watermark option (future)
9. **Portfolio video has no sound** → valid (some UGC is silent/music-only)
10. **Creator uploads 100 items then switches to private** → allowed, all 100 hidden from search
11. **External URL changes format** → normalize on save, re-validate periodically
12. **Thumbnail generation fails** → show placeholder, retry in background
# 10 — Messaging System
## Overview
Messaging connects companies and creators within the platform. All communication is tracked, searchable, and moderatable.
---
## Conversation Model
```
Conversation (thread)
├── Participants: [Company User, Creator]
├── Context: campaign_id / project_id / invitation_id (nullable)
├── Messages[]
│ ├── Message 1 (text)
│ ├── Message 2 (text + attachment)
│ └── Message 3 (system message)
└── Metadata (unread counts, last_message_at, etc.)
```
---
## Conversation Types
| Type | Context | Auto-Created |
|------|---------|-------------|
| campaign_inquiry | campaign_id | no (creator initiates) |
| application_discussion | application_id | no (either party initiates) |
| invitation_discussion | invitation_id | no (either party initiates) |
| project_thread | project_id | yes (auto-created with project) |
| direct_message | none | no (company initiates) |
---
## Conversation Fields
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| uuid | uuid | auto | public ID |
| type | enum | yes | see above |
| campaign_id | FK | nullable | |
| project_id | FK | nullable | |
| application_id | FK | nullable | |
| invitation_id | FK | nullable | |
| company_id | FK | yes | |
| creator_id | FK | yes | |
| status | enum | yes | active, archived, blocked |
| last_message_at | datetime | auto | for sorting |
| created_at | datetime | auto | |
---
## Message Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| conversation_id | FK | yes | |
| sender_id | FK | yes | current user |
| sender_type | enum | auto | company, creator, system |
| body | text | yes (unless attachment only) | min:1, max:5000 |
| type | enum | yes | text, system, file_share |
| is_read | boolean | auto | starts false |
| read_at | datetime | nullable | when recipient reads |
| edited_at | datetime | nullable | if message edited |
| deleted_at | datetime | nullable | soft delete |
| metadata | JSON | nullable | system message data |
---
## Message Attachments
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| message_id | FK | yes | |
| file_name | string | yes | original name |
| file_path | string | yes | storage path |
| file_size | integer | yes | max 25MB per file |
| mime_type | string | yes | validated |
| file_type | enum | auto | image, video, document, audio, other |
| thumbnail_path | string | nullable | for images/videos |
**Allowed attachments:**
- Images: jpg, png, webp, gif (max 10MB)
- Videos: mp4, mov, webm (max 25MB — for quick clips, not deliverables)
- Documents: pdf, doc, docx, txt, xls, xlsx (max 25MB)
- Audio: mp3, wav, aac (max 25MB)
- Max 5 attachments per message
---
## System Messages
Auto-generated messages in project threads:
```
[System] Project started — Creator began working
[System] Deliverable submitted — "Product Review Video" v1
[System] Revision requested — Company left feedback
[System] Deliverable approved — "Product Review Video"
[System] Project completed
[System] Deadline extended to March 30, 2024
```
These use type: "system" and cannot be replied to directly.
---
## Read Receipts
- When recipient opens conversation → all unread messages marked as read
- Sender sees "Read" indicator (double-check icon) with timestamp
- Bulk: opening conversation marks ALL unread in that conversation as read
---
## Real-Time Features (WebSocket)
Using Laravel Reverb:
- New message appears instantly (no page refresh)
- Typing indicator ("Company is typing...")
- Online/offline status indicator
- Unread badge updates in real-time
### WebSocket Channels:
```
private-conversation.{conversation_uuid} → messages, typing, read receipts
private-user.{user_id} → new conversation notifications, unread counts
```
---
## Messaging Rules
1. **Creator cannot message company without context** → must have: active application, active invitation, active project, OR company messaged first
2. **Company can message any creator** → direct_message type (but rate limited)
3. **Messages in project thread** → always available to both parties even after project completion (for records)
4. **Blocked user** → cannot send messages, existing messages stay visible
5. **Suspended user** → messages frozen, other party sees "user suspended" in thread
6. **Deleted account** → messages preserved but sender shown as "Deleted User"
---
## Message Actions
| Action | Who | Conditions |
|--------|-----|-----------|
| Send message | both | conversation.status = active |
| Edit message | sender | within 15 minutes of sending, not system message |
| Delete message | sender | any time, soft delete, shows "message deleted" |
| Report message | recipient | opens report flow |
| Share file | both | see attachment rules |
| React (emoji) | both | single reaction per user per message |
---
## Conversation List (Inbox)
### Sorting:
- Default: last_message_at DESC (newest activity first)
- Unread first (toggle)
### Filtering:
- All / Unread / Campaigns / Projects / Direct
- Search by participant name or message content
### Conversation Card Shows:
- Other party's avatar + name
- Context badge (Campaign: X / Project: Y)
- Last message preview (truncated 80 chars)
- Timestamp
- Unread count badge
---
## Anti-Spam & Safety
| Rule | Limit |
|------|-------|
| Messages per minute (per user) | 10 |
| Messages per hour (per user) | 100 |
| New conversations per day (company → creators) | 50 |
| Max message length | 5000 chars |
| Max attachments per message | 5 |
| Max attachment size | 25MB |
### Content Filtering:
- Phone numbers in first 3 messages → flag for review (prevents taking deals off-platform)
- External URLs in first 3 messages → flag for review
- Same message sent to 10+ different creators → flag as spam
- Messages containing known scam patterns → auto-flag
---
## Messaging Edge Cases
1. **Company sends message, creator hasn't accepted invitation yet** → allowed for invitation_discussion type
2. **Creator replies to expired invitation thread** → allowed (conversation stays open)
3. **Message with attachment but empty body** → allowed (attachment is the message)
4. **User sends message then immediately blocks other** → message delivered, then conversation blocked
5. **Project completed but parties want to discuss future work** → project thread stays open, or new direct_message conversation
6. **Message edit after other party read it** → show "(edited)" indicator, original NOT shown
7. **100+ messages in a thread** → paginate (load 50, scroll for more)
8. **User offline for days** → messages queue, show all on next login
9. **Attachment is malware** → server-side virus scan before storage (ClamAV or similar)
10. **Message in Arabic, company speaks English** → no auto-translate (future feature), both parties responsible for communication
11. **Company team member changes** → all project threads accessible to any company team member (future: when teams implemented)
# 11 — Reviews & Reputation System
## Review System
Reviews are mutual — both parties review each other after project completion.
---
## When Reviews Are Triggered
```
Project status → "completed"
→ 48-hour grace period
→ Both parties receive "Leave a review" prompt
→ Review window: 30 days after project completion
→ After 30 days: window closes, no review possible
```
---
## Review Fields (Company Reviews Creator)
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | must be completed |
| reviewer_id | FK | auto | company user |
| reviewee_id | FK | auto | creator |
| overall_rating | integer | yes | 1-5 stars |
| communication_rating | integer | yes | 1-5 |
| professionalism_rating | integer | yes | 1-5 |
| quality_rating | integer | yes | 1-5 |
| reliability_rating | integer | yes | 1-5 (met deadlines?) |
| comment | text | no | min:20, max:1000 (if provided) |
| would_work_again | boolean | yes | |
| is_public | boolean | yes | default true |
---
## Review Fields (Creator Reviews Company)
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | must be completed |
| reviewer_id | FK | auto | creator |
| reviewee_id | FK | auto | company user |
| overall_rating | integer | yes | 1-5 stars |
| communication_rating | integer | yes | 1-5 |
| clarity_rating | integer | yes | 1-5 (clear requirements?) |
| professionalism_rating | integer | yes | 1-5 |
| fairness_rating | integer | yes | 1-5 (fair feedback/revisions?) |
| payment_rating | integer | yes | 1-5 (paid on time?) — future |
| comment | text | no | min:20, max:1000 (if provided) |
| would_work_again | boolean | yes | |
| is_public | boolean | yes | default true |
---
## Review Rules
1. **One review per party per project** — cannot review same project twice
2. **Cannot review yourself** — enforced at DB level
3. **Review only after completion** — not during active project
4. **Cannot edit review after 24 hours** — prevents retaliation edits
5. **Private reviews** — still count toward reputation scores but comment not shown publicly
6. **Review text moderation** — flagged if contains profanity, threats, or personal info
7. **Minimum project value for review weight** — reviews from larger projects weighted slightly higher in reputation
8. **Both reviews visible simultaneously** — to prevent retaliation. Reviews hidden until BOTH parties review, OR 14 days pass (whichever first)
---
## Review Display
### On Creator Profile:
- Average overall rating (stars)
- Rating breakdown (communication, professionalism, quality, reliability)
- Total review count
- Recent reviews (company name, rating, comment, date)
- "Would work again" percentage
### On Company Profile:
- Average overall rating (stars)
- Rating breakdown (communication, clarity, professionalism, fairness)
- Total review count
- Recent reviews (creator name or anonymous, rating, comment, date)
- "Would work again" percentage
---
## Review Edge Cases
1. **Creator got 1-star but company was clearly at fault** → creator can flag review for admin review
2. **Company leaves review, creator doesn't** → company review visible after 14 days regardless
3. **Project cancelled — can they review?** → only if project was "in_progress" or later when cancelled. Not for "not_started" cancellations.
4. **Revenge review (1-star with no substance)** → admin can hide reviews that violate guidelines
5. **Company fires creator mid-project, then leaves bad review** → allowed (cancellation reviews have different weight)
6. **Creator has 100 reviews, new bad one barely affects average** → weighted average favoring recent reviews
7. **Review contains competitor mentions** → no blocking but flag for review
8. **Fake reviews (company creates fake projects to boost creator)** → pattern detection: same IP, rapid project cycles, admin flagged
---
## Reputation System
Reputation is a calculated composite score based on multiple signals. It's NOT just reviews.
---
## Creator Reputation Score (0-100)
| Factor | Weight | Calculation |
|--------|--------|-------------|
| Review average | 30% | (avg_rating / 5) × 100 |
| Completion rate | 25% | completed_projects / total_projects × 100 |
| On-time delivery rate | 15% | on_time_projects / completed_projects × 100 |
| Response rate | 10% | messages_responded_within_24h / total_received × 100 |
| Response time | 5% | inverse of avg response time (faster = higher) |
| Profile completeness | 5% | profile_completion_percentage |
| Account age | 5% | logarithmic (diminishing returns after 1 year) |
| Revision rate | 5% | lower is better (fewer revisions needed = higher quality) |
### Reputation Tiers:
| Score | Tier | Badge |
|-------|------|-------|
| 0-39 | New | — (no badge) |
| 40-59 | Rising | Bronze |
| 60-79 | Established | Silver |
| 80-89 | Top Rated | Gold |
| 90-100 | Elite | Diamond |
---
## Company Reputation Score (0-100)
| Factor | Weight | Calculation |
|--------|--------|-------------|
| Review average | 35% | from creator reviews |
| Project completion rate | 20% | completed / total (not cancelled) |
| Response time to applications | 15% | avg time to first action on applications |
| Payment reliability | 15% | future: % paid on time |
| Account age + volume | 10% | projects completed × time factor |
| Dispute rate | 5% | lower = better |
### Company Badges:
| Condition | Badge |
|-----------|-------|
| Verified + 80+ reputation | "Trusted Employer" |
| 10+ completed projects | "Active Employer" |
| Avg response < 24h | "Quick Responder" |
| 0 cancelled projects | "Reliable" |
---
## Reputation Decay
- If creator inactive for 90+ days → score slowly decays (1 point/month)
- If company hasn't posted in 6+ months → remove "Active" badges
- Decay stops at the score floor (never below their actual performance data)
---
## Reputation Edge Cases
1. **New creator has 0 reviews** → show "New" badge, score starts at 50 (neutral)
2. **Creator has 1 review (5 stars)** → show rating but note "1 review" (don't display as "perfect")
3. **Creator's only project was cancelled by company** → don't penalize creator's completion rate for company-initiated cancellations
4. **Gaming the system: rapid small projects for reviews** → weight by project value/complexity
5. **Creator inactive but has great history** → preserve score (decay is slow), show "last active: X ago"
6. **Company with 50 projects, 2 bad reviews** → weighted correctly, 2 bad out of 50 = minor impact
7. **Score calculation timing** → recalculate on every relevant event (review, completion, etc.), cache result
8. **Admin overrides** → admin can manually adjust reputation with reason (logged in audit)
# 12 — Notifications System
## Notification Channels
| Channel | Description | Delivery Speed |
|---------|-------------|---------------|
| In-App | Bell icon badge, dropdown list | Instant (WebSocket) |
| Email | Full email with action buttons | Near-instant (queued) |
| Push (future) | Browser/mobile push notifications | Instant |
---
## Notification Model
| Field | Type | Notes |
|-------|------|-------|
| id | bigint | auto |
| user_id | FK | recipient |
| type | string | class name / event type |
| title | string | short title |
| body | text | message content |
| action_url | string | where to go when clicked |
| action_label | string | "View Application", "Review Deliverable", etc. |
| icon | string | icon identifier |
| category | enum | campaigns, projects, messages, applications, invitations, reviews, system |
| is_read | boolean | default false |
| read_at | datetime | nullable |
| emailed | boolean | whether email was sent |
| data | JSON | additional context data |
| created_at | datetime | |
---
## Notification Events (Complete List)
### Campaign Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| campaign_published | matching creators | "New campaign matches your profile" | yes |
| campaign_updated | applicants | "Campaign '{title}' was updated" | yes (if major) |
| campaign_paused | applicants | "Campaign '{title}' is paused" | yes |
| campaign_closed | applicants not yet accepted | "Campaign '{title}' is no longer accepting applications" | yes |
| campaign_cancelled | all related | "Campaign '{title}' was cancelled" | yes |
| campaign_deadline_24h | applicants | "Campaign '{title}' closes in 24 hours" | yes |
### Application Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| application_received | company | "New application from {creator_name}" | yes |
| application_viewed | creator | "Your application was viewed by {company_name}" | no (in-app only) |
| application_shortlisted | creator | "You've been shortlisted for '{campaign_title}'" | yes |
| application_accepted | creator | "You've been accepted for '{campaign_title}'" | yes |
| application_rejected | creator | "Update on your application to '{campaign_title}'" | yes |
| application_withdrawn | company | "{creator_name} withdrew their application" | no |
### Invitation Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| invitation_received | creator | "New invitation from {company_name}" | yes |
| invitation_accepted | company | "{creator_name} accepted your invitation" | yes |
| invitation_declined | company | "{creator_name} declined your invitation" | yes |
| invitation_expiring_24h | creator | "Invitation from {company_name} expires tomorrow" | yes |
| invitation_expired | company | "Your invitation to {creator_name} expired" | no |
### Project Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| project_created | both | "New project: '{title}' started" | yes |
| project_started | company | "{creator_name} started working" | no |
| project_deadline_3d | both | "Project '{title}' due in 3 days" | yes |
| project_deadline_24h | both | "Project '{title}' due tomorrow" | yes |
| project_overdue | both | "Project '{title}' is overdue" | yes |
| project_completed | both | "Project '{title}' completed!" | yes |
| project_cancelled | other party | "Project '{title}' was cancelled" | yes |
| project_disputed | both + admin | "Dispute opened on '{title}'" | yes |
| deadline_extension_requested | company | "{creator_name} requested deadline extension" | yes |
| deadline_extension_approved | creator | "Deadline extended to {new_date}" | yes |
| deadline_extension_rejected | creator | "Deadline extension was declined" | yes |
### Deliverable Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| deliverable_submitted | company | "{creator_name} submitted '{deliverable_title}' (v{n})" | yes |
| deliverable_approved | creator | "'{deliverable_title}' was approved!" | yes |
| revision_requested | creator | "Revision requested for '{deliverable_title}'" | yes |
| all_deliverables_approved | both | "All deliverables approved for '{project_title}'" | yes |
### Message Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| new_message | other party | "New message from {sender_name}" | yes (if offline 5min+) |
| message_attachment | other party | "{sender_name} shared a file" | no |
### Review Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| review_prompt | both | "Leave a review for '{project_title}'" | yes |
| review_received | reviewee | "You received a new review" | yes |
| review_reminder_7d | non-reviewer | "Reminder: review pending for '{project_title}'" | yes |
### System Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| account_verified | user | "Your email has been verified" | no |
| company_approved | company | "Your company has been approved!" | yes |
| company_rejected | company | "Your company application needs attention" | yes |
| account_suspended | user | "Your account has been suspended" | yes |
| account_reactivated | user | "Your account is active again" | yes |
| profile_incomplete_reminder | creator | "Complete your profile to start applying" | yes (after 7 days) |
| security_login_new_device | user | "New login from {device} in {location}" | yes |
---
## Notification Preferences
Users can control which notifications they receive and via which channel:
| Category | In-App | Email | Default |
|----------|--------|-------|---------|
| Applications | always | toggleable | both on |
| Invitations | always | toggleable | both on |
| Projects | always | toggleable | both on |
| Deliverables | always | toggleable | both on |
| Messages | always | toggleable | email on |
| Campaigns (recommendations) | toggleable | toggleable | in-app only |
| Reviews | always | toggleable | both on |
| Security | always | always | both (non-negotiable) |
| Marketing | toggleable | toggleable | off |
**Rules:**
- Security notifications CANNOT be disabled
- In-app notifications always fire (but user can mute categories from appearing in dropdown)
- Email digest option: instead of individual emails, receive daily/weekly summary
---
## Email Templates
All emails follow consistent template:
```
Header: UGC Heaven logo
Subject: [UGC Heaven] {notification_title}
Body:
- Greeting: "Hi {first_name},"
- Context: what happened
- Action button: primary CTA
- Secondary text: additional details
Footer:
- Unsubscribe link (per category)
- "Manage notification settings"
- Company info
```
### Email Languages:
- Detect user's language preference
- Send in their preferred language (ar or en)
- RTL layout for Arabic emails
---
## Notification Batching
To prevent email fatigue:
- If 5+ notifications of same type fire within 10 minutes → batch into one email
- Example: "You received 7 new applications" instead of 7 separate emails
- Messages: only email if user hasn't been online for 5+ minutes (they might see it in-app)
- Never more than 10 emails per hour per user (hard cap)
- Digest mode: aggregate into daily email at user's preferred time
---
## Notification Edge Cases
1. **User has 500 unread notifications** → show count as "99+" in badge, paginate list
2. **Notification for deleted entity** → action_url leads to 404 → show "This item no longer exists" gracefully
3. **Notification sent, user changes email** → already-sent emails go to old address (correct behavior)
4. **User marks all as read** → single action, bulk update
5. **Same notification fires twice (race condition)** → deduplicate by type + user + entity_id within 1 minute window
6. **Email bounce** → track bounces, after 3 bounces disable email notifications and warn user
7. **User in different timezone** → all timestamps shown in their timezone
8. **Notification for project between two users, both get notified** → separate notification records, same event
9. **Admin notification (internal)** → separate notification channel for admin alerts (disputes, reports, etc.)
10. **Push notification permission denied** → graceful degradation to in-app + email only
# 13 — SuperAdmin Panel
## Access
- URL: `/admin/*`
- Only users with role `superadmin` can access
- Middleware: `auth` + `role:superadmin`
- No public registration for admin — seeded or created by existing admin only
- Admin sessions: 2-hour timeout, re-authenticate for sensitive actions
---
## Dashboard (Overview)
### Key Metrics Cards:
- Total Users (creators + companies)
- New Users (this week)
- Active Campaigns
- Active Projects
- Pending Company Approvals
- Open Disputes
- Open Reports
- Revenue (future: when payments integrated)
### Charts:
- User growth (line chart, last 30 days / 12 months)
- Campaign creation rate
- Project completion rate
- Creator-to-company ratio
- Top categories/niches
### Quick Actions:
- Pending approvals (N)
- Open reports (N)
- Open disputes (N)
- Flagged content (N)
---
## User Management
### User List View:
- Table with: avatar, name, email, role, status, joined date, last active
- Filters: role, status, country, joined date range, verified/unverified
- Search: by name, email, username
- Bulk actions: suspend, ban, export CSV
### User Detail View:
- Full profile information
- Account status + history of status changes
- Activity log (last 100 actions)
- Active sessions (IP, device, location)
- Projects (as creator or company)
- Reviews given/received
- Reports (filed by them, filed against them)
- Messages (searchable, for moderation)
- Reputation score breakdown
- Financial history (future)
### User Actions:
| Action | Requires | Confirmation | Reversible |
|--------|----------|-------------|------------|
| Verify email manually | — | yes | no |
| Change role | reason | yes | yes |
| Suspend | reason + duration | yes | yes (auto or manual) |
| Ban | reason | double confirm | yes (by admin only) |
| Delete (soft) | reason | double confirm | yes (within 30 days) |
| Delete (hard) | reason | triple confirm + password | NO |
| Impersonate | — | yes + logged | yes (end impersonation) |
| Force logout | — | yes | N/A |
| Reset password | — | yes (sends email) | N/A |
| Merge accounts | select accounts | yes | NO |
### Impersonation:
- Admin sees the platform exactly as that user does
- Yellow banner at top: "Viewing as {username} — End Impersonation"
- All actions logged as "performed by admin via impersonation"
- Cannot impersonate another admin
- Cannot perform destructive actions while impersonating (safety)
---
## Company Management
### Company Queue:
- **Pending Review** tab: new registrations awaiting approval
- Show: company name, industry, website, country, registered date
- Actions: approve, reject (with reason), request more info
- **Active** tab: all approved companies
- **Suspended** tab: temporarily suspended
- **Rejected** tab: rejected applications (can be re-reviewed)
### Company Detail View:
- All profile fields
- Campaigns posted (with stats)
- Projects (with outcomes)
- Creators worked with
- Reviews received
- Reports against them
- Activity timeline
### Company Actions:
| Action | Effect |
|--------|--------|
| Approve | Company activated, can use platform |
| Reject | Company notified with reason, can re-apply |
| Suspend | All campaigns paused, active projects flagged |
| Ban | Everything terminated |
| Add verification badge | Trust tier upgraded |
| Remove verification | Trust tier downgraded |
| Feature company | Shown in "featured companies" section |
---
## Creator Management
### Creator List:
- Same as user management but filtered to creators
- Additional filters: niches, skills, reputation score range, portfolio count
- Additional columns: reputation score, completed projects, review avg
### Creator Actions:
| Action | Effect |
|--------|--------|
| Verify (Pro badge) | Manual verification, trust badge added |
| Remove verification | Badge removed |
| Feature creator | Shown in "featured creators" section |
| Hide from search | Invisible to companies (but profile still accessible via link) |
| Flag portfolio | Portfolio items hidden pending review |
---
## Campaign Management
### Campaign List:
- All campaigns, all statuses
- Filters: status, company, category, date range, budget range
- Flags: reported campaigns, campaigns with 0 applications
### Campaign Actions:
| Action | Effect |
|--------|--------|
| Feature | Shown at top of discover page |
| Unfeature | Removed from featured |
| Pause | Hidden from search, applicants notified |
| Unpause | Restored to active |
| Delete | Soft delete, all applicants notified |
| Edit | Admin can edit any campaign field |
---
## Project Management
### Project List:
- All projects, all statuses
- Filters: status, company, creator, date range, overdue, disputed
- Priority view: overdue + disputed projects at top
### Project Actions:
| Action | Effect |
|--------|--------|
| Force complete | Override status to completed |
| Force cancel | Cancel with admin reason |
| Extend deadline | Override deadline |
| Reassign (future) | Change creator (extreme edge case) |
| Add admin note | Internal note not visible to parties |
| Resolve dispute | Close dispute with resolution |
---
## Dispute Resolution
### Dispute Queue:
- Open disputes, sorted by age (oldest first)
- Show: project title, parties, reason, opened date
### Dispute Detail:
- Full project history
- All messages between parties
- All deliverable submissions + reviews
- Dispute reason (from opening party)
- Response from other party
- Admin notes
### Resolution Options:
| Resolution | Effect |
|-----------|--------|
| Side with creator | Project completed, creator not penalized |
| Side with company | Project cancelled, company not penalized |
| Mutual fault | Both parties penalized equally |
| No fault | Cancelled, no penalties |
| Custom resolution | Admin writes custom resolution, both notified |
---
## Moderation
### Content Queue:
Items flagged for review (auto-flagged or user-reported):
- Portfolio videos
- Profile images
- Campaign descriptions
- Messages (if reported)
- Reviews (if reported)
### Moderation Actions:
| Action | Effect |
|--------|--------|
| Approve | Remove flag, content visible |
| Remove | Content hidden, creator/company notified |
| Remove + warn | Content hidden, warning sent to user |
| Remove + suspend | Content hidden, user suspended |
| Escalate | Move to senior admin / legal |
### Auto-Moderation Rules (Future):
- Profile pictures: nudity detection
- Text content: profanity filter, phone number detection, email detection
- Videos: too short / corrupted detection
- Spam detection: same content posted repeatedly
---
## Reports Management
### Report Queue:
- All reports, unresolved first
- Show: reporter, reported entity, type, reason, date
### Report Types:
| Report Target | Reasons |
|--------------|---------|
| User | scam, harassment, spam, fake_profile, underage, impersonation |
| Campaign | scam, misleading, inappropriate, discriminatory |
| Message | harassment, spam, threats, inappropriate |
| Review | fake, harassment, irrelevant, personal_info |
| Portfolio | copyright, inappropriate, misleading |
### Report Resolution:
| Action | Effect |
|--------|--------|
| Valid — action taken | Content removed/user suspended, reporter notified |
| Valid — already resolved | No new action needed |
| Invalid | Report dismissed, reporter notified |
| Needs investigation | Assigned to admin for deeper look |
| Duplicate | Merged with existing report |
---
## Analytics Dashboard
### Platform Health:
- DAU/WAU/MAU
- Session duration
- Retention (7d, 30d)
- Funnel: register → complete profile → first application/campaign → first project → first completion
### Marketplace Health:
- Supply/demand ratio (creators vs. campaigns)
- Average time from campaign publish to first application
- Average time from application to project start
- Average project duration
- Completion rate
- Dispute rate
- Average review scores (trending)
### Geographic:
- Users by country (heatmap)
- Campaigns by country
- Cross-border projects
### Export:
- All analytics exportable as CSV
- Date range selection
- Scheduled reports (weekly email to admin)
---
## System Settings
### General:
- Platform name, logo, favicon
- Default language
- Supported languages
- Maintenance mode toggle
- Registration open/closed toggle
### Limits:
- Max file upload sizes
- Max portfolio items per creator
- Max campaigns per company (simultaneous)
- Rate limiting configuration
- Session duration
### Email:
- SMTP configuration
- Email templates preview/edit
- Test email sender
### Integrations:
- PeerTube connection status
- Redis connection status
- S3/storage status
- Webhook URLs (future)
---
## Audit Log (Admin Actions)
Every admin action logged:
| Field | Type |
|-------|------|
| admin_id | FK |
| action | string |
| target_type | string (user, campaign, project, etc.) |
| target_id | FK |
| details | JSON (what changed) |
| ip_address | string |
| user_agent | string |
| created_at | datetime |
**Retention:** audit logs kept for 2 years minimum, never auto-deleted.
# 14 — Matching & Discovery System
## Two Discovery Directions
1. **Creators discovering campaigns** → "Find Work"
2. **Companies discovering creators** → "Find Talent"
Both use the same matching engine underneath.
---
## Match Score Algorithm
A match score (0-100) represents how well a creator fits a campaign (or vice versa).
### Scoring Factors:
| Factor | Weight | Logic |
|--------|--------|-------|
| Niche overlap | 25% | creator_niches ∩ campaign_niches / campaign_niches |
| Language match | 20% | creator_languages ∩ campaign_languages |
| Country match | 15% | creator_country in campaign_countries (or 100% if campaign = any) |
| Skills match | 15% | creator_skills ∩ campaign_required_skills / campaign_skills |
| Experience level | 10% | creator_level ≥ campaign_min_level |
| Equipment match | 5% | meets equipment requirements |
| Availability | 5% | creator is available + can meet deadline |
| Reputation bonus | 5% | higher reputation = slight boost |
### Score Calculation:
```
score = Σ (factor_score × weight)
factor_score:
- exact match: 100
- partial match: (matches / required) × 100
- no match: 0
- N/A (not required): 100 (don't penalize)
```
---
## Creator Discovery (Company Searching for Creators)
### Search Interface:
**Free Text Search:**
- Searches: display_name, username, bio, skills (text), niches (text)
- Uses PostgreSQL pg_trgm for fuzzy matching
- Supports both Arabic and English
**Filters:**
| Filter | Type | Values |
|--------|------|--------|
| Country | multi-select | ISO countries, with MENA shortcut |
| Language | multi-select | from predefined |
| Gender | select | any, male, female |
| Age range | range | 18-65 |
| Niche | multi-select | from predefined |
| Skills | multi-select | from predefined |
| Experience level | multi-select | beginner-expert |
| Equipment quality | select | any, basic, professional |
| Video style | multi-select | from predefined |
| Availability | select | available_now, any |
| Has portfolio | boolean | |
| Min reputation | range | 0-100 |
| Min reviews | range | 0+ |
| Verification | select | any, verified, pro_verified |
| Hourly rate range | range | $0-$X |
**Sorting:**
| Sort | Logic |
|------|-------|
| Best Match | match_score DESC (if campaign context provided) |
| Newest | created_at DESC |
| Most Experienced | experience_level DESC + completed_projects DESC |
| Highest Rated | reputation_score DESC |
| Most Projects | completed_projects_count DESC |
| Most Active | last_active_at DESC |
### Creator Card (Search Result):
```
┌─────────────────────────────────────────┐
│ [Avatar] Display Name [badge-check icon] │
│ @username · Egypt · Arabic, English │
│ │
│ [Gaming] [Tech] [Apps] -- niche tags │
│ │
│ [star icon] 4.8 (23 reviews) · 15 projects done │
│ "Available" · Responds within hours │
│ │
│ [View Profile] [Invite] │
│ │
│ Featured portfolio thumbnail (if any) │
└─────────────────────────────────────────┘
```
### Saved Creators:
- Company can "favorite" / save creators to a list
- Multiple lists possible: "Gaming Creators", "Arabic Speakers", etc.
- Saved creators can be bulk-invited to campaigns
---
## Campaign Discovery (Creator Searching for Work)
### Campaign Feed:
- Default view: "Recommended for You" (based on match score)
- Alternative: "All Campaigns" (chronological)
### Search + Filters:
(Detailed in doc 05, summarized here)
- Text search on title + description
- Filter by: niche, country, language, budget, deliverable type, deadline, experience level
- Sort by: best match, newest, deadline soon, highest budget
### Campaign Card:
```
┌─────────────────────────────────────────┐
│ [Company Logo] Company Name [badge-check icon] │
│ │
│ Campaign Title │
│ Brief description truncated to 2 lines │
│ │
│ [banknote] $500/creator · [video] 3 videos · [ratio] 9:16 │
│ [calendar] Deadline: March 30 · [globe] Egypt, UAE │
│ │
│ 12 applicants · Match: 87% ████████░░ │
│ │
│ [View Details] [Quick Apply] │
└─────────────────────────────────────────┘
```
---
## Recommendation Engine
### For Creators — "Recommended Campaigns":
Triggered when:
- Creator logs in (dashboard widget)
- Creator visits "Find Work" page
- Weekly email digest
Logic:
1. Get all published campaigns not yet applied to
2. Calculate match_score for each
3. Filter out: expired, not matching hard requirements (wrong country, wrong language)
4. Sort by match_score DESC
5. Return top 10
### For Companies — "Recommended Creators":
Triggered when:
- Company creates a campaign (suggested creators)
- Company visits "Find Talent" page
- When campaign gets few applications (proactive suggestions)
Logic:
1. Get all active creators matching campaign requirements
2. Calculate match_score
3. Factor in: availability, reputation, response rate
4. Exclude: creators already applied, creators already invited
5. Sort by composite score DESC
6. Return top 20
### Smart Suggestions:
- "Creators similar to ones you've worked with" (collaborative filtering)
- "Creators who worked on similar campaigns" (content-based)
- "Rising creators in your niche" (new + highly rated)
---
## Search Indexing Strategy
### PostgreSQL Full-Text Search:
```sql
-- Creator search index
CREATE INDEX idx_creator_search ON creator_profiles
USING gin(to_tsvector('english', bio || ' ' || display_name));
-- Arabic support
CREATE INDEX idx_creator_search_ar ON creator_profiles
USING gin(to_tsvector('arabic', bio || ' ' || display_name));
-- Trigram for fuzzy matching
CREATE INDEX idx_creator_name_trgm ON creator_profiles
USING gin(display_name gin_trgm_ops);
```
### Materialized View for Fast Discovery:
```sql
CREATE MATERIALIZED VIEW creator_discovery AS
SELECT
creator_id,
display_name,
username,
country,
languages,
niches,
skills,
experience_level,
reputation_score,
completed_projects_count,
review_avg,
availability_status,
last_active_at,
has_portfolio,
is_verified
FROM creator_profiles
JOIN users ON users.id = creator_profiles.user_id
WHERE users.status = 'active'
AND creator_profiles.profile_completion >= 50;
-- Refresh every 15 minutes
```
---
## Discovery Edge Cases
1. **No results match filters** → show "No creators found" + suggest relaxing filters (remove most restrictive one)
2. **Creator matches but is on vacation** → show in results but marked "Currently Unavailable"
3. **Campaign requires specific country but no creators there** → show nearby countries as "partial match"
4. **Creator has 100% match but terrible reputation** → still show (company can judge reputation separately)
5. **New creator (0 projects) vs experienced** → new creators get "New" badge, not hidden
6. **Company searches for own employee by accident** → no self-match prevention needed (won't cause issues)
7. **Arabic search query for English-only profiles** → no results (correct behavior)
8. **Rate limiting on search** → max 60 searches per minute per user (prevent scraping)
9. **Creator appears in "recommended" but already declined previous invitation from same company** → still show (different campaign might interest them)
10. **Recommendation score is 0 for everyone** → fall back to "popular campaigns" / "top creators" instead of empty state
# 15 — Database Schema
## Core Tables
---
### users
```sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
email_verified_at TIMESTAMP NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'company', 'creator')),
status VARCHAR(20) NOT NULL DEFAULT 'unverified'
CHECK (status IN ('active', 'unverified', 'pending_review', 'suspended', 'banned', 'deactivated')),
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
phone VARCHAR(30) NULL,
language_preference VARCHAR(5) DEFAULT 'en',
timezone VARCHAR(50) DEFAULT 'UTC',
last_login_at TIMESTAMP NULL,
last_active_at TIMESTAMP NULL,
suspended_at TIMESTAMP NULL,
suspended_until TIMESTAMP NULL,
suspension_reason TEXT NULL,
banned_at TIMESTAMP NULL,
ban_reason TEXT NULL,
deactivated_at TIMESTAMP NULL,
remember_token VARCHAR(100) NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_uuid ON users(uuid);
```
---
### creator_profiles
```sql
CREATE TABLE creator_profiles (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
username VARCHAR(30) UNIQUE NOT NULL,
display_name VARCHAR(50) NOT NULL,
bio TEXT NULL,
profile_picture_path VARCHAR(500) NULL,
cover_image_path VARCHAR(500) NULL,
date_of_birth DATE NULL,
gender VARCHAR(20) NULL CHECK (gender IN ('male', 'female', 'prefer_not_to_say')),
country VARCHAR(3) NOT NULL,
city VARCHAR(100) NULL,
timezone VARCHAR(50) NULL,
experience_level VARCHAR(20) NOT NULL DEFAULT 'beginner'
CHECK (experience_level IN ('beginner', 'intermediate', 'advanced', 'expert')),
languages JSONB NOT NULL DEFAULT '[]',
content_niches JSONB NOT NULL DEFAULT '[]',
skills JSONB NOT NULL DEFAULT '[]',
video_styles JSONB NOT NULL DEFAULT '[]',
equipment JSONB NOT NULL DEFAULT '{}',
social_links JSONB NOT NULL DEFAULT '{}',
hourly_rate DECIMAL(10,2) NULL,
project_min_budget DECIMAL(10,2) NULL,
availability_status VARCHAR(20) DEFAULT 'available'
CHECK (availability_status IN ('available', 'busy', 'vacation', 'not_accepting')),
available_from DATE NULL,
weekly_capacity_hours INTEGER NULL,
preferred_project_length VARCHAR(20) NULL,
response_time VARCHAR(20) NULL,
profile_completion INTEGER NOT NULL DEFAULT 0,
reputation_score INTEGER NOT NULL DEFAULT 50,
completed_projects_count INTEGER NOT NULL DEFAULT 0,
review_avg DECIMAL(3,2) NOT NULL DEFAULT 0.00,
review_count INTEGER NOT NULL DEFAULT 0,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
verification_level VARCHAR(20) DEFAULT 'email'
CHECK (verification_level IN ('none', 'email', 'identity', 'pro')),
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
peertube_channel_name VARCHAR(100) NULL,
username_changed_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_creator_country ON creator_profiles(country);
CREATE INDEX idx_creator_niches ON creator_profiles USING gin(content_niches);
CREATE INDEX idx_creator_skills ON creator_profiles USING gin(skills);
CREATE INDEX idx_creator_languages ON creator_profiles USING gin(languages);
CREATE INDEX idx_creator_availability ON creator_profiles(availability_status);
CREATE INDEX idx_creator_reputation ON creator_profiles(reputation_score DESC);
CREATE INDEX idx_creator_completion ON creator_profiles(profile_completion);
CREATE INDEX idx_creator_search ON creator_profiles USING gin(
to_tsvector('english', COALESCE(display_name,'') || ' ' || COALESCE(bio,''))
);
```
---
### company_profiles
```sql
CREATE TABLE company_profiles (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
company_name VARCHAR(100) NOT NULL,
slug VARCHAR(120) UNIQUE NOT NULL,
logo_path VARCHAR(500) NULL,
cover_image_path VARCHAR(500) NULL,
description TEXT NULL,
short_description VARCHAR(160) NULL,
website VARCHAR(500) NULL,
industry VARCHAR(50) NOT NULL,
country VARCHAR(3) NOT NULL,
city VARCHAR(100) NULL,
founded_year INTEGER NULL,
company_size VARCHAR(20) NOT NULL,
company_type VARCHAR(20) NULL,
contact_email VARCHAR(255) NOT NULL,
contact_phone VARCHAR(30) NULL,
contact_person_name VARCHAR(100) NOT NULL,
contact_person_role VARCHAR(50) NULL,
brand_colors JSONB NOT NULL DEFAULT '[]',
brand_guidelines_path VARCHAR(500) NULL,
tone_of_voice VARCHAR(20) NULL,
social_links JSONB NOT NULL DEFAULT '{}',
verification_tier VARCHAR(20) DEFAULT 'basic'
CHECK (verification_tier IN ('basic', 'verified', 'premium')),
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
reputation_score INTEGER NOT NULL DEFAULT 50,
completed_projects_count INTEGER NOT NULL DEFAULT 0,
review_avg DECIMAL(3,2) NOT NULL DEFAULT 0.00,
review_count INTEGER NOT NULL DEFAULT 0,
approved_at TIMESTAMP NULL,
approved_by BIGINT NULL REFERENCES users(id),
rejected_at TIMESTAMP NULL,
rejection_reason TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_company_slug ON company_profiles(slug);
CREATE INDEX idx_company_industry ON company_profiles(industry);
CREATE INDEX idx_company_country ON company_profiles(country);
```
---
### campaigns
```sql
CREATE TABLE campaigns (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
company_id BIGINT NOT NULL REFERENCES company_profiles(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL,
slug VARCHAR(120) UNIQUE NOT NULL,
description TEXT NOT NULL,
objective VARCHAR(50) NOT NULL,
product_service_name VARCHAR(100) NULL,
product_url VARCHAR(500) NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'paused', 'closed', 'in_progress', 'completed', 'cancelled')),
visibility VARCHAR(20) NOT NULL DEFAULT 'public'
CHECK (visibility IN ('public', 'unlisted', 'invite_only')),
creator_count INTEGER NOT NULL DEFAULT 1,
gender_preference VARCHAR(20) NULL,
age_range_min INTEGER NULL,
age_range_max INTEGER NULL,
countries JSONB NOT NULL DEFAULT '[]',
languages JSONB NOT NULL DEFAULT '[]',
niches JSONB NOT NULL DEFAULT '[]',
experience_level_min VARCHAR(20) NULL,
specific_skills JSONB NOT NULL DEFAULT '[]',
equipment_requirements TEXT NULL,
deliverable_type VARCHAR(20) NOT NULL,
video_count INTEGER NULL,
video_duration_min INTEGER NULL,
video_duration_max INTEGER NULL,
aspect_ratio VARCHAR(10) NULL,
format_notes TEXT NULL,
includes_raw_footage BOOLEAN DEFAULT FALSE,
includes_script_approval BOOLEAN DEFAULT FALSE,
max_revisions INTEGER DEFAULT 2,
budget_type VARCHAR(20) NOT NULL,
budget_amount DECIMAL(12,2) NULL,
budget_currency VARCHAR(3) NULL,
product_provided BOOLEAN DEFAULT FALSE,
product_value DECIMAL(10,2) NULL,
payment_terms TEXT NULL,
application_deadline TIMESTAMP NOT NULL,
project_start_date DATE NULL,
project_deadline DATE NOT NULL,
estimated_turnaround VARCHAR(20) NULL,
allow_applications BOOLEAN DEFAULT TRUE,
auto_close_on_count BOOLEAN DEFAULT FALSE,
is_featured BOOLEAN DEFAULT FALSE,
views_count INTEGER NOT NULL DEFAULT 0,
applications_count INTEGER NOT NULL DEFAULT 0,
published_at TIMESTAMP NULL,
closed_at TIMESTAMP NULL,
completed_at TIMESTAMP NULL,
cancelled_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_campaign_company ON campaigns(company_id);
CREATE INDEX idx_campaign_status ON campaigns(status);
CREATE INDEX idx_campaign_visibility ON campaigns(visibility);
CREATE INDEX idx_campaign_deadline ON campaigns(application_deadline);
CREATE INDEX idx_campaign_niches ON campaigns USING gin(niches);
CREATE INDEX idx_campaign_languages ON campaigns USING gin(languages);
CREATE INDEX idx_campaign_countries ON campaigns USING gin(countries);
CREATE INDEX idx_campaign_search ON campaigns USING gin(
to_tsvector('english', title || ' ' || description)
);
```
---
### campaign_attachments
```sql
CREATE TABLE campaign_attachments (
id BIGSERIAL PRIMARY KEY,
campaign_id BIGINT NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
type VARCHAR(30) NOT NULL
CHECK (type IN ('brand_guidelines', 'reference_video', 'product_image', 'script', 'mood_board', 'document')),
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size INTEGER NOT NULL,
mime_type VARCHAR(100) NOT NULL,
external_url VARCHAR(500) NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_campaign_attachment_campaign ON campaign_attachments(campaign_id);
```
---
### applications
```sql
CREATE TABLE applications (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
campaign_id BIGINT NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'submitted'
CHECK (status IN ('submitted', 'viewed', 'shortlisted', 'accepted', 'rejected', 'withdrawn', 'cancelled')),
cover_message TEXT NOT NULL,
proposed_budget DECIMAL(10,2) NULL,
proposed_timeline VARCHAR(200) NULL,
questions_for_company TEXT NULL,
rejection_reason TEXT NULL,
viewed_at TIMESTAMP NULL,
shortlisted_at TIMESTAMP NULL,
accepted_at TIMESTAMP NULL,
rejected_at TIMESTAMP NULL,
withdrawn_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(campaign_id, creator_id)
);
CREATE INDEX idx_application_campaign ON applications(campaign_id);
CREATE INDEX idx_application_creator ON applications(creator_id);
CREATE INDEX idx_application_status ON applications(status);
```
---
### application_portfolio_items
```sql
CREATE TABLE application_portfolio_items (
application_id BIGINT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
portfolio_item_id BIGINT NOT NULL REFERENCES portfolio_items(id) ON DELETE CASCADE,
PRIMARY KEY (application_id, portfolio_item_id)
);
```
---
### invitations
```sql
CREATE TABLE invitations (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
company_id BIGINT NOT NULL REFERENCES company_profiles(id) ON DELETE CASCADE,
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id) ON DELETE CASCADE,
campaign_id BIGINT NULL REFERENCES campaigns(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL DEFAULT 'sent'
CHECK (status IN ('sent', 'viewed', 'accepted', 'declined', 'expired', 'cancelled')),
message TEXT NOT NULL,
proposed_budget DECIMAL(10,2) NULL,
proposed_scope TEXT NULL,
deadline DATE NULL,
expires_at TIMESTAMP NOT NULL,
decline_reason TEXT NULL,
viewed_at TIMESTAMP NULL,
accepted_at TIMESTAMP NULL,
declined_at TIMESTAMP NULL,
cancelled_at TIMESTAMP NULL,
resend_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_invitation_company ON invitations(company_id);
CREATE INDEX idx_invitation_creator ON invitations(creator_id);
CREATE INDEX idx_invitation_status ON invitations(status);
CREATE INDEX idx_invitation_expires ON invitations(expires_at);
```
---
### projects
```sql
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
campaign_id BIGINT NULL REFERENCES campaigns(id) ON DELETE SET NULL,
company_id BIGINT NOT NULL REFERENCES company_profiles(id),
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id),
application_id BIGINT NULL REFERENCES applications(id) ON DELETE SET NULL,
invitation_id BIGINT NULL REFERENCES invitations(id) ON DELETE SET NULL,
title VARCHAR(200) NOT NULL,
description TEXT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'not_started'
CHECK (status IN ('not_started', 'in_progress', 'waiting_review', 'revision_requested',
'approved', 'completed', 'cancelled', 'disputed', 'on_hold')),
budget_amount DECIMAL(12,2) NULL,
budget_currency VARCHAR(3) NULL,
deadline DATE NOT NULL,
max_revisions INTEGER NOT NULL DEFAULT 2,
revision_count INTEGER NOT NULL DEFAULT 0,
started_at TIMESTAMP NULL,
completed_at TIMESTAMP NULL,
cancelled_at TIMESTAMP NULL,
cancelled_by BIGINT NULL REFERENCES users(id),
cancellation_reason TEXT NULL,
disputed_at TIMESTAMP NULL,
dispute_reason TEXT NULL,
dispute_resolved_at TIMESTAMP NULL,
dispute_resolution TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_project_company ON projects(company_id);
CREATE INDEX idx_project_creator ON projects(creator_id);
CREATE INDEX idx_project_campaign ON projects(campaign_id);
CREATE INDEX idx_project_status ON projects(status);
CREATE INDEX idx_project_deadline ON projects(deadline);
```
---
### deliverables
```sql
CREATE TABLE deliverables (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL,
description TEXT NULL,
type VARCHAR(20) NOT NULL
CHECK (type IN ('video', 'image', 'script', 'raw_footage', 'audio', 'document')),
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'submitted', 'under_review', 'revision_requested', 'approved', 'overdue')),
specifications JSONB NOT NULL DEFAULT '{}',
sort_order INTEGER NOT NULL DEFAULT 0,
max_revisions INTEGER NULL,
revision_count INTEGER NOT NULL DEFAULT 0,
due_date DATE NULL,
approved_at TIMESTAMP NULL,
approved_by BIGINT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_deliverable_project ON deliverables(project_id);
CREATE INDEX idx_deliverable_status ON deliverables(status);
```
---
### submissions
```sql
CREATE TABLE submissions (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
deliverable_id BIGINT NOT NULL REFERENCES deliverables(id) ON DELETE CASCADE,
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id),
version INTEGER NOT NULL DEFAULT 1,
notes TEXT NULL,
submitted_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_submission_deliverable ON submissions(deliverable_id);
CREATE UNIQUE INDEX idx_submission_version ON submissions(deliverable_id, version);
```
---
### submission_files
```sql
CREATE TABLE submission_files (
id BIGSERIAL PRIMARY KEY,
submission_id BIGINT NOT NULL REFERENCES submissions(id) ON DELETE CASCADE,
file_type VARCHAR(20) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
peertube_uuid VARCHAR(100) NULL,
peertube_embed_url VARCHAR(500) NULL,
thumbnail_path VARCHAR(500) NULL,
duration_seconds INTEGER NULL,
width INTEGER NULL,
height INTEGER NULL,
metadata JSONB NULL,
processing_status VARCHAR(20) DEFAULT 'ready'
CHECK (processing_status IN ('uploading', 'processing', 'ready', 'failed')),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_submission_file_submission ON submission_files(submission_id);
```
---
### revision_requests
```sql
CREATE TABLE revision_requests (
id BIGSERIAL PRIMARY KEY,
deliverable_id BIGINT NOT NULL REFERENCES deliverables(id) ON DELETE CASCADE,
submission_id BIGINT NOT NULL REFERENCES submissions(id) ON DELETE CASCADE,
requested_by BIGINT NOT NULL REFERENCES users(id),
feedback TEXT NOT NULL,
priority VARCHAR(10) DEFAULT 'major' CHECK (priority IN ('minor', 'major', 'critical')),
revision_deadline DATE NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_revision_deliverable ON revision_requests(deliverable_id);
```
---
### timestamp_comments
```sql
CREATE TABLE timestamp_comments (
id BIGSERIAL PRIMARY KEY,
submission_id BIGINT NOT NULL REFERENCES submissions(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id),
timestamp_seconds DECIMAL(10,2) NOT NULL,
comment TEXT NOT NULL,
type VARCHAR(20) DEFAULT 'issue' CHECK (type IN ('suggestion', 'issue', 'praise')),
is_resolved BOOLEAN NOT NULL DEFAULT FALSE,
resolved_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_timestamp_comment_submission ON timestamp_comments(submission_id);
```
---
### portfolio_collections
```sql
CREATE TABLE portfolio_collections (
id BIGSERIAL PRIMARY KEY,
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
description VARCHAR(200) NULL,
visibility VARCHAR(10) DEFAULT 'public' CHECK (visibility IN ('public', 'private')),
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_portfolio_collection_creator ON portfolio_collections(creator_id);
```
---
### portfolio_items
```sql
CREATE TABLE portfolio_items (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id) ON DELETE CASCADE,
collection_id BIGINT NULL REFERENCES portfolio_collections(id) ON DELETE SET NULL,
title VARCHAR(100) NOT NULL,
description VARCHAR(500) NULL,
type VARCHAR(10) NOT NULL CHECK (type IN ('video', 'image', 'link')),
peertube_uuid VARCHAR(100) NULL,
peertube_embed_url VARCHAR(500) NULL,
thumbnail_url VARCHAR(500) NULL,
external_url VARCHAR(500) NULL,
external_platform VARCHAR(20) NULL,
file_path VARCHAR(500) NULL,
duration_seconds INTEGER NULL,
tags JSONB NOT NULL DEFAULT '[]',
industry VARCHAR(50) NULL,
brand_worked_with VARCHAR(100) NULL,
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
visibility VARCHAR(10) NOT NULL DEFAULT 'public'
CHECK (visibility IN ('public', 'private', 'unlisted')),
sort_order INTEGER NOT NULL DEFAULT 0,
views_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_portfolio_creator ON portfolio_items(creator_id);
CREATE INDEX idx_portfolio_visibility ON portfolio_items(visibility);
CREATE INDEX idx_portfolio_featured ON portfolio_items(is_featured) WHERE is_featured = TRUE;
```
---
### conversations
```sql
CREATE TABLE conversations (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
type VARCHAR(30) NOT NULL,
company_id BIGINT NOT NULL REFERENCES company_profiles(id),
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id),
campaign_id BIGINT NULL REFERENCES campaigns(id) ON DELETE SET NULL,
project_id BIGINT NULL REFERENCES projects(id) ON DELETE SET NULL,
application_id BIGINT NULL REFERENCES applications(id) ON DELETE SET NULL,
invitation_id BIGINT NULL REFERENCES invitations(id) ON DELETE SET NULL,
status VARCHAR(10) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'blocked')),
last_message_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_conversation_company ON conversations(company_id);
CREATE INDEX idx_conversation_creator ON conversations(creator_id);
CREATE INDEX idx_conversation_project ON conversations(project_id);
CREATE INDEX idx_conversation_last_msg ON conversations(last_message_at DESC);
```
---
### messages
```sql
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
sender_id BIGINT NOT NULL REFERENCES users(id),
sender_type VARCHAR(10) NOT NULL CHECK (sender_type IN ('company', 'creator', 'system')),
body TEXT NULL,
type VARCHAR(20) NOT NULL DEFAULT 'text' CHECK (type IN ('text', 'system', 'file_share')),
is_read BOOLEAN NOT NULL DEFAULT FALSE,
read_at TIMESTAMP NULL,
edited_at TIMESTAMP NULL,
metadata JSONB NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_message_conversation ON messages(conversation_id);
CREATE INDEX idx_message_sender ON messages(sender_id);
CREATE INDEX idx_message_unread ON messages(conversation_id, is_read) WHERE is_read = FALSE;
```
---
### message_attachments
```sql
CREATE TABLE message_attachments (
id BIGSERIAL PRIMARY KEY,
message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size INTEGER NOT NULL,
mime_type VARCHAR(100) NOT NULL,
file_type VARCHAR(20) NOT NULL,
thumbnail_path VARCHAR(500) NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_msg_attachment_message ON message_attachments(message_id);
```
---
### reviews
```sql
CREATE TABLE reviews (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
reviewer_id BIGINT NOT NULL REFERENCES users(id),
reviewee_id BIGINT NOT NULL REFERENCES users(id),
reviewer_role VARCHAR(10) NOT NULL CHECK (reviewer_role IN ('company', 'creator')),
overall_rating SMALLINT NOT NULL CHECK (overall_rating BETWEEN 1 AND 5),
communication_rating SMALLINT NOT NULL CHECK (communication_rating BETWEEN 1 AND 5),
dimension_1_rating SMALLINT NOT NULL CHECK (dimension_1_rating BETWEEN 1 AND 5),
dimension_2_rating SMALLINT NOT NULL CHECK (dimension_2_rating BETWEEN 1 AND 5),
dimension_3_rating SMALLINT NULL CHECK (dimension_3_rating BETWEEN 1 AND 5),
comment TEXT NULL,
would_work_again BOOLEAN NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT TRUE,
is_visible BOOLEAN NOT NULL DEFAULT FALSE,
made_visible_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, reviewer_id)
);
CREATE INDEX idx_review_reviewee ON reviews(reviewee_id);
CREATE INDEX idx_review_project ON reviews(project_id);
CREATE INDEX idx_review_visible ON reviews(is_visible) WHERE is_visible = TRUE;
```
---
### notifications
```sql
CREATE TABLE notifications (
id BIGSERIAL PRIMARY KEY,
uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL,
title VARCHAR(200) NOT NULL,
body TEXT NOT NULL,
action_url VARCHAR(500) NULL,
action_label VARCHAR(50) NULL,
icon VARCHAR(50) NULL,
category VARCHAR(30) NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
read_at TIMESTAMP NULL,
emailed BOOLEAN NOT NULL DEFAULT FALSE,
data JSONB NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_notification_user ON notifications(user_id);
CREATE INDEX idx_notification_unread ON notifications(user_id, is_read) WHERE is_read = FALSE;
CREATE INDEX idx_notification_created ON notifications(created_at DESC);
```
---
### reports
```sql
CREATE TABLE reports (
id BIGSERIAL PRIMARY KEY,
reporter_id BIGINT NOT NULL REFERENCES users(id),
reportable_type VARCHAR(50) NOT NULL,
reportable_id BIGINT NOT NULL,
reason VARCHAR(50) NOT NULL,
description TEXT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'investigating', 'resolved_valid', 'resolved_invalid', 'duplicate')),
resolution_notes TEXT NULL,
resolved_by BIGINT NULL REFERENCES users(id),
resolved_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_report_status ON reports(status);
CREATE INDEX idx_report_reportable ON reports(reportable_type, reportable_id);
```
---
### activity_logs
```sql
CREATE TABLE activity_logs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NULL REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL,
target_type VARCHAR(50) NULL,
target_id BIGINT NULL,
details JSONB NULL,
ip_address VARCHAR(45) NULL,
user_agent TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_activity_user ON activity_logs(user_id);
CREATE INDEX idx_activity_target ON activity_logs(target_type, target_id);
CREATE INDEX idx_activity_created ON activity_logs(created_at DESC);
```
---
### saved_creators
```sql
CREATE TABLE saved_creators (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT NOT NULL REFERENCES company_profiles(id) ON DELETE CASCADE,
creator_id BIGINT NOT NULL REFERENCES creator_profiles(id) ON DELETE CASCADE,
list_name VARCHAR(50) NULL,
notes TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(company_id, creator_id)
);
```
---
### user_sessions
```sql
CREATE TABLE user_sessions (
id VARCHAR(255) PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
ip_address VARCHAR(45) NULL,
user_agent TEXT NULL,
device_type VARCHAR(20) NULL,
location VARCHAR(100) NULL,
last_active_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_session_user ON user_sessions(user_id);
```
---
### notification_preferences
```sql
CREATE TABLE notification_preferences (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
preferences JSONB NOT NULL DEFAULT '{
"applications": {"in_app": true, "email": true},
"invitations": {"in_app": true, "email": true},
"projects": {"in_app": true, "email": true},
"deliverables": {"in_app": true, "email": true},
"messages": {"in_app": true, "email": true},
"campaigns": {"in_app": true, "email": false},
"reviews": {"in_app": true, "email": true},
"marketing": {"in_app": false, "email": false}
}',
email_digest VARCHAR(10) DEFAULT 'instant'
CHECK (email_digest IN ('instant', 'daily', 'weekly', 'off')),
digest_time TIME DEFAULT '09:00',
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
```
# 16 — API Routes
## Base URL
```
/api/v1
```
## Authentication
- Bearer token (Laravel Sanctum)
- Token returned on login, valid for 24 hours
- Refresh via `/api/v1/auth/refresh`
---
## Auth Routes
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /auth/register/creator | Register as creator | no |
| POST | /auth/register/company | Register as company | no |
| POST | /auth/login | Login (all roles) | no |
| POST | /auth/logout | Logout (kill token) | yes |
| POST | /auth/forgot-password | Request password reset | no |
| POST | /auth/reset-password | Reset with token | no |
| POST | /auth/verify-email/{id}/{hash} | Verify email | signed URL |
| POST | /auth/resend-verification | Resend verification email | yes |
| POST | /auth/refresh | Refresh token | yes |
| GET | /auth/me | Get current user | yes |
---
## Creator Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /creators | List/search creators | yes | company |
| GET | /creators/{username} | Get creator public profile | no | — |
| PUT | /creator/profile | Update own profile | yes | creator |
| PATCH | /creator/profile/avatar | Update profile picture | yes | creator |
| PATCH | /creator/profile/cover | Update cover image | yes | creator |
| GET | /creator/dashboard | Dashboard stats | yes | creator |
| GET | /creator/recommendations | Recommended campaigns | yes | creator |
---
## Company Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /companies/{slug} | Get company public profile | no | — |
| PUT | /company/profile | Update own profile | yes | company |
| PATCH | /company/profile/logo | Update logo | yes | company |
| GET | /company/dashboard | Dashboard stats | yes | company |
| GET | /company/recommendations | Recommended creators | yes | company |
---
## Portfolio Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /creators/{username}/portfolio | List portfolio items | no | — |
| GET | /portfolio | List own portfolio | yes | creator |
| POST | /portfolio | Add portfolio item | yes | creator |
| PUT | /portfolio/{id} | Update portfolio item | yes | creator |
| DELETE | /portfolio/{id} | Delete portfolio item | yes | creator |
| PATCH | /portfolio/{id}/featured | Toggle featured | yes | creator |
| PATCH | /portfolio/reorder | Reorder items | yes | creator |
| GET | /portfolio/collections | List collections | yes | creator |
| POST | /portfolio/collections | Create collection | yes | creator |
| PUT | /portfolio/collections/{id} | Update collection | yes | creator |
| DELETE | /portfolio/collections/{id} | Delete collection | yes | creator |
| POST | /portfolio/upload-video | Upload video to PeerTube | yes | creator |
---
## Campaign Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /campaigns | List/search public campaigns | no* | — |
| GET | /campaigns/{slug} | Get campaign detail | no | — |
| POST | /campaigns | Create campaign | yes | company |
| PUT | /campaigns/{id} | Update campaign | yes | company (owner) |
| PATCH | /campaigns/{id}/publish | Publish draft | yes | company (owner) |
| PATCH | /campaigns/{id}/pause | Pause campaign | yes | company (owner) |
| PATCH | /campaigns/{id}/resume | Resume paused | yes | company (owner) |
| PATCH | /campaigns/{id}/close | Close campaign | yes | company (owner) |
| PATCH | /campaigns/{id}/cancel | Cancel campaign | yes | company (owner) |
| DELETE | /campaigns/{id} | Delete draft | yes | company (owner) |
| GET | /company/campaigns | List own campaigns | yes | company |
| GET | /campaigns/{id}/analytics | Campaign analytics | yes | company (owner) |
| POST | /campaigns/{id}/attachments | Add attachment | yes | company (owner) |
| DELETE | /campaigns/{id}/attachments/{aid} | Remove attachment | yes | company (owner) |
*\*No auth shows public campaigns. With auth shows match scores.*
---
## Application Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| POST | /campaigns/{id}/apply | Apply to campaign | yes | creator |
| PUT | /applications/{id} | Update application | yes | creator (owner) |
| DELETE | /applications/{id} | Withdraw application | yes | creator (owner) |
| GET | /creator/applications | List my applications | yes | creator |
| GET | /campaigns/{id}/applications | List applications for campaign | yes | company (owner) |
| GET | /applications/{id} | Get application detail | yes | party |
| PATCH | /applications/{id}/shortlist | Shortlist | yes | company |
| PATCH | /applications/{id}/accept | Accept | yes | company |
| PATCH | /applications/{id}/reject | Reject | yes | company |
| POST | /applications/{id}/reject-bulk | Bulk reject | yes | company |
---
## Invitation Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| POST | /invitations | Send invitation | yes | company |
| POST | /invitations/bulk | Bulk invite (up to 10) | yes | company |
| DELETE | /invitations/{id} | Cancel invitation | yes | company (sender) |
| PATCH | /invitations/{id}/resend | Resend invitation | yes | company |
| PATCH | /invitations/{id}/accept | Accept invitation | yes | creator (recipient) |
| PATCH | /invitations/{id}/decline | Decline invitation | yes | creator (recipient) |
| GET | /creator/invitations | List my invitations | yes | creator |
| GET | /company/invitations | List sent invitations | yes | company |
---
## Project Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /projects | List my projects | yes | both |
| GET | /projects/{uuid} | Get project detail | yes | party |
| PATCH | /projects/{uuid}/start | Mark started | yes | creator |
| PATCH | /projects/{uuid}/cancel | Request cancellation | yes | both |
| PATCH | /projects/{uuid}/hold | Put on hold | yes | both |
| PATCH | /projects/{uuid}/resume | Resume from hold | yes | both |
| PATCH | /projects/{uuid}/dispute | Open dispute | yes | both |
| PATCH | /projects/{uuid}/extend-deadline | Request/approve extension | yes | both |
| GET | /projects/{uuid}/activity | Activity log | yes | party |
---
## Deliverable Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /projects/{uuid}/deliverables | List deliverables | yes | party |
| GET | /deliverables/{id} | Get deliverable detail | yes | party |
| POST | /deliverables/{id}/submit | Submit deliverable | yes | creator |
| POST | /deliverables/{id}/upload | Upload file for deliverable | yes | creator |
| PATCH | /deliverables/{id}/approve | Approve deliverable | yes | company |
| POST | /deliverables/{id}/revision | Request revision | yes | company |
| GET | /deliverables/{id}/submissions | List all versions | yes | party |
| GET | /submissions/{id} | Get submission detail | yes | party |
| POST | /submissions/{id}/comments | Add timestamp comment | yes | company |
| PATCH | /submissions/{id}/comments/{cid}/resolve | Resolve comment | yes | creator |
---
## Messaging Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /conversations | List conversations | yes | both |
| POST | /conversations | Start conversation | yes | both |
| GET | /conversations/{uuid} | Get conversation + messages | yes | participant |
| POST | /conversations/{uuid}/messages | Send message | yes | participant |
| PATCH | /conversations/{uuid}/read | Mark all read | yes | participant |
| PATCH | /messages/{id} | Edit message | yes | sender |
| DELETE | /messages/{id} | Delete message | yes | sender |
| POST | /messages/{id}/report | Report message | yes | recipient |
---
## Notification Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /notifications | List notifications | yes | both |
| GET | /notifications/unread-count | Get unread count | yes | both |
| PATCH | /notifications/{id}/read | Mark as read | yes | owner |
| PATCH | /notifications/read-all | Mark all read | yes | owner |
| GET | /notifications/preferences | Get preferences | yes | both |
| PUT | /notifications/preferences | Update preferences | yes | both |
---
## Review Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| POST | /projects/{uuid}/reviews | Leave review | yes | party |
| GET | /creators/{username}/reviews | Get creator reviews | no | — |
| GET | /companies/{slug}/reviews | Get company reviews | no | — |
| GET | /reviews/{id} | Get review detail | no | — |
| PUT | /reviews/{id} | Edit review (within 24h) | yes | author |
| POST | /reviews/{id}/report | Report review | yes | — |
---
## Report Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| POST | /reports | Submit report | yes | both |
| GET | /reports | List my reports | yes | both |
---
## Saved Creators Routes
| Method | Route | Description | Auth | Role |
|--------|-------|-------------|------|------|
| GET | /saved-creators | List saved creators | yes | company |
| POST | /saved-creators | Save a creator | yes | company |
| DELETE | /saved-creators/{creator_id} | Unsave creator | yes | company |
| PUT | /saved-creators/{creator_id} | Update notes/list | yes | company |
---
## Admin Routes (prefix: /admin)
| Method | Route | Description |
|--------|-------|-------------|
| GET | /admin/dashboard | Dashboard stats |
| GET | /admin/users | List all users |
| GET | /admin/users/{id} | User detail |
| PATCH | /admin/users/{id}/status | Change user status |
| PATCH | /admin/users/{id}/role | Change role |
| POST | /admin/users/{id}/impersonate | Start impersonation |
| DELETE | /admin/impersonate | End impersonation |
| GET | /admin/companies/pending | Pending approvals |
| PATCH | /admin/companies/{id}/approve | Approve company |
| PATCH | /admin/companies/{id}/reject | Reject company |
| GET | /admin/reports | All reports |
| PATCH | /admin/reports/{id}/resolve | Resolve report |
| GET | /admin/disputes | All disputes |
| PATCH | /admin/disputes/{id}/resolve | Resolve dispute |
| GET | /admin/campaigns | All campaigns |
| PATCH | /admin/campaigns/{id}/feature | Feature/unfeature |
| GET | /admin/projects | All projects |
| PATCH | /admin/projects/{id}/status | Force status change |
| GET | /admin/analytics | Platform analytics |
| GET | /admin/audit-log | Audit log |
| GET | /admin/settings | Platform settings |
| PUT | /admin/settings | Update settings |
---
## Response Format
All responses follow:
```json
{
"success": true,
"data": { ... },
"message": "Operation successful",
"meta": {
"current_page": 1,
"per_page": 25,
"total": 100,
"last_page": 4
}
}
```
Error responses:
```json
{
"success": false,
"message": "Validation failed",
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
}
}
```
---
## Rate Limiting
| Endpoint Group | Limit |
|---------------|-------|
| Auth (login/register) | 5 per minute |
| Password reset | 3 per hour |
| General API | 60 per minute |
| Search | 30 per minute |
| File upload | 10 per minute |
| Messaging | 30 per minute |
| Admin | 120 per minute |
# 17 — MVP Roadmap (2-4 Week Sprint)
## Philosophy
Build the shortest path from "company posts campaign" to "creator delivers approved video." Everything else is enhancement.
---
## Week 1: Foundation + Auth + Profiles
### Day 1-2: Project Setup
- [ ] Laravel 11 fresh install
- [ ] PostgreSQL connection configured
- [ ] Module structure created (folders, service providers)
- [ ] Base models, migrations for: users, creator_profiles, company_profiles
- [ ] Authentication (register, login, logout, email verification)
- [ ] Middleware: auth, role-based, verified
- [ ] Sanctum API token setup
- [ ] Basic CORS, rate limiting
- [ ] Redis for sessions + cache
- [ ] Localization setup (en + ar)
### Day 3-4: Profiles
- [ ] Creator registration + onboarding flow
- [ ] Creator profile CRUD (all fields from doc 03)
- [ ] Profile completion calculator
- [ ] Company registration + pending review
- [ ] Company profile CRUD (all fields from doc 04)
- [ ] Image upload handling (profile pics, logos, covers)
- [ ] Public profile pages (creator, company)
### Day 5: Admin Basics
- [ ] Admin seeder (first superadmin account)
- [ ] Admin dashboard (basic stats)
- [ ] Company approval/rejection flow
- [ ] User list + status management
- [ ] Activity logging foundation
---
## Week 2: Campaigns + Applications + Invitations
### Day 6-7: Campaigns
- [ ] Campaign model + migration
- [ ] Campaign CRUD (create, edit, publish, pause, close)
- [ ] Campaign creation wizard (multi-step form)
- [ ] Campaign listing + search + filters (creator side)
- [ ] Campaign detail page
- [ ] Campaign attachments upload
- [ ] Campaign slug generation
### Day 8-9: Applications
- [ ] Application model + migration
- [ ] Creator applies to campaign (form + validation)
- [ ] Company views applications list
- [ ] Application status management (shortlist, accept, reject)
- [ ] Application → Project creation (on accept)
- [ ] Notifications: new application, accepted, rejected
### Day 10: Invitations
- [ ] Invitation model + migration
- [ ] Company sends invitation to creator
- [ ] Creator views/accepts/declines invitation
- [ ] Invitation → Project creation (on accept)
- [ ] Invitation expiry (scheduled command)
- [ ] Notifications: invitation sent, accepted, declined
---
## Week 3: Projects + Deliverables + Video
### Day 11-12: Projects
- [ ] Project model + migration
- [ ] Project dashboard (both sides)
- [ ] Project status management
- [ ] Deliverable model + migration
- [ ] Deliverable CRUD (auto-created from campaign specs)
- [ ] Project activity log
### Day 13-14: Video Upload + Review
- [ ] PeerTube service class (API wrapper)
- [ ] Video upload flow (file → PeerTube → embed)
- [ ] Submission model + migration
- [ ] Creator submits deliverable (upload video)
- [ ] Company reviews submission (embedded player)
- [ ] Approve / Request Revision flow
- [ ] Revision requests with feedback
- [ ] Version history (v1, v2, v3...)
### Day 15: Completion Flow
- [ ] All deliverables approved → project approved
- [ ] Project completion confirmation
- [ ] Basic review system (post-completion)
- [ ] Notification: deliverable submitted, approved, revision requested
---
## Week 4: Messaging + Polish + Deploy
### Day 16-17: Messaging
- [ ] Conversation model + messages
- [ ] Send/receive messages
- [ ] Project thread (auto-created)
- [ ] Message attachments
- [ ] Unread counts
- [ ] Basic real-time (polling fallback, WebSocket stretch goal)
### Day 18-19: Notifications + Discovery
- [ ] Full notification system (in-app)
- [ ] Email notifications (critical events only for MVP)
- [ ] Creator search/discovery (basic filters)
- [ ] Match score (simplified: niche + language + country)
- [ ] Campaign recommendations on creator dashboard
### Day 20: Admin + Deploy
- [ ] Admin: campaign management
- [ ] Admin: project overview
- [ ] Admin: report handling (basic)
- [ ] Deploy to CapRover
- [ ] Environment configuration
- [ ] SSL + domain setup
- [ ] Smoke testing
---
## What's IN the MVP:
| Feature | Included |
|---------|----------|
| Creator registration + profile | YES |
| Company registration + approval | YES |
| Campaign creation + listing | YES |
| Campaign search + filters | YES |
| Applications (apply, accept, reject) | YES |
| Invitations (send, accept, decline) | YES |
| Projects (full lifecycle) | YES |
| Deliverables + video upload | YES |
| Video review (approve/revision) | YES |
| Basic messaging | YES |
| Notifications (in-app + key emails) | YES |
| Basic admin panel | YES |
| Creator discovery (search + filters) | YES |
| Reviews (post-completion) | YES |
| Arabic + English | YES |
| Mobile-responsive | YES |
---
## What's NOT in MVP (Phase 2+):
| Feature | Phase |
|---------|-------|
| Timestamp comments on videos | Phase 2 |
| WebSocket real-time messaging | Phase 2 |
| Reputation system (full scoring) | Phase 2 |
| Portfolio collections | Phase 2 |
| Bulk invitations | Phase 2 |
| Campaign templates | Phase 2 |
| Advanced matching algorithm | Phase 2 |
| Email digests | Phase 2 |
| Dispute resolution system | Phase 2 |
| Two-factor authentication | Phase 3 |
| Team members (company) | Phase 3 |
| Payment integration | Phase 3 |
| AI recommendations | Phase 3 |
| Mobile app | Phase 3 |
| Advanced analytics | Phase 3 |
| Agency accounts | Phase 4 |
---
## Technical Debt Accepted in MVP:
1. Polling instead of WebSocket for messages (replace in Phase 2)
2. Simplified match score (add full algorithm in Phase 2)
3. No virus scanning on uploads (add in Phase 2)
4. Basic email templates (polish in Phase 2)
5. No automated moderation (manual only for MVP)
6. No caching layer optimization (add when needed)
7. Profile completion is calculated on every request (cache in Phase 2)
8. No background job for PeerTube status polling (manual check for MVP)
---
## Definition of Done (MVP):
A company can:
1. Register → get approved → complete profile
2. Create a campaign → publish it
3. Receive applications → review them → accept a creator
4. OR search creators → invite one → creator accepts
5. See project created → track progress
6. Review video submission → approve or request revision
7. Approve final deliverable → project completes
8. Leave a review
A creator can:
1. Register → complete profile → upload portfolio video
2. Browse campaigns → apply to one
3. OR receive invitation → accept it
4. See project → start working → submit video
5. Receive feedback → resubmit → get approved
6. Project completes → leave a review
An admin can:
1. Approve/reject companies
2. View all users, campaigns, projects
3. Suspend/ban users
4. View basic platform stats
# 18 — UI Patterns & State Handling
## Design System Foundation
### Color Palette:
- **Primary**: Deep blue/indigo (trust, professionalism)
- **Secondary**: Emerald/teal (growth, creativity)
- **Accent**: Amber/gold (premium, success)
- **Error**: Red
- **Warning**: Orange
- **Success**: Green
- **Neutral**: Slate gray scale
### Typography:
- **English**: Inter (headings) + Inter (body)
- **Arabic**: IBM Plex Arabic / Noto Sans Arabic
- **Monospace**: JetBrains Mono (code/technical)
### Layout:
- Max width: 1280px (content area)
- Sidebar: 260px (collapsible on mobile)
- Responsive breakpoints: 640, 768, 1024, 1280
---
## Empty States
Every list/page has a designed empty state (never blank):
| Page | Empty State Message | CTA |
|------|-------------------|-----|
| Creator Dashboard - No projects | "No active projects yet" | "Browse Campaigns" |
| Creator Dashboard - No applications | "Apply to your first campaign" | "Discover Campaigns" |
| Company Dashboard - No campaigns | "Create your first campaign" | "New Campaign" |
| Company Dashboard - No applications | "No applications yet" | "Share your campaign" |
| Messages - No conversations | "No messages yet" | "Start a conversation" |
| Notifications - Empty | "All caught up!" | — |
| Portfolio - No items | "Showcase your best work" | "Upload Video" |
| Search - No results | "No results match your filters" | "Clear filters" |
| Projects - None | "Your projects will appear here" | (contextual) |
---
## Loading States
| Context | Pattern |
|---------|---------|
| Page load | Full-page skeleton (shimmer effect) |
| Table/list | Skeleton rows (3-5 placeholder rows) |
| Button action | Button shows spinner, disabled |
| Form submit | Submit button spinner + disable all inputs |
| File upload | Progress bar with percentage |
| Video processing | "Processing..." with animated dots |
| Search | Skeleton cards + "Searching..." text |
| Infinite scroll | Spinner at bottom |
| Image load | Blur placeholder → sharp (progressive) |
---
## Error States
### Form Validation:
- Inline errors below each field (red text)
- Field border turns red
- Error summary at top of form (scrolls into view)
- Errors cleared on input change (not on blur)
### API Errors:
| HTTP Code | User Sees |
|-----------|-----------|
| 400 | Specific validation errors |
| 401 | "Please log in" → redirect to login |
| 403 | "You don't have permission" |
| 404 | "Not found" page |
| 409 | Conflict message (e.g., "already applied") |
| 422 | Validation errors (same as 400) |
| 429 | "Too many requests. Wait a moment." |
| 500 | "Something went wrong. Try again." + error ID for support |
### Network Errors:
- Offline: banner "You're offline. Changes will sync when connected."
- Timeout: "Request timed out. Check your connection."
- Connection lost mid-upload: resume upload (if resumable) or "Upload interrupted"
---
## Success States
| Action | Feedback |
|--------|----------|
| Form submitted | Toast notification (green) + redirect |
| Message sent | Message appears in thread instantly |
| File uploaded | Progress → checkmark → thumbnail appears |
| Status changed | Badge color animation + toast |
| Item deleted | Item fades out + undo toast (5 seconds) |
| Copied to clipboard | Brief "Copied!" tooltip |
---
## Confirmation Dialogs
Required before destructive actions:
| Action | Dialog |
|--------|--------|
| Delete campaign draft | "Delete this draft? This cannot be undone." |
| Cancel project | "Cancel this project? Both parties will be notified." + reason field |
| Withdraw application | "Withdraw your application? You cannot re-apply." |
| Ban user (admin) | "Ban {name}? This will terminate all their active projects." + reason |
| Reject company | "Reject {name}?" + reason field (required) |
| Delete portfolio item | "Remove from portfolio?" + undo option |
| Bulk reject applications | "Reject {N} applications? They will be notified." |
---
## Pagination Patterns
| Context | Pattern | Default |
|---------|---------|---------|
| Search results | Load more button | 20 per page |
| Admin tables | Traditional pagination (prev/next + page numbers) | 25 per page |
| Messages | Infinite scroll (load older on scroll up) | 50 per load |
| Notifications | Infinite scroll | 20 per load |
| Activity log | Traditional pagination | 50 per page |
| Portfolio items | Grid with load more | 12 per load |
---
## Form Patterns
### Multi-Step Forms (Wizard):
- Campaign creation: 7 steps with progress indicator
- Creator onboarding: 4 steps
- Step indicator shows: completed (green), current (blue), upcoming (gray)
- Can navigate back to any completed step
- "Save as Draft" available at every step
- Auto-save on step transition
### Inline Editing:
- Profile fields: click to edit, ESC to cancel, blur/Enter to save
- Real-time save indicator (saving... saved [check icon])
### File Upload:
- Drag-and-drop zone + click to browse
- Preview thumbnails for images
- Progress bar per file
- Cancel button during upload
- File type icons for non-image files
- Max file count indicator
---
## Responsive Behavior
### Navigation:
- Desktop: fixed sidebar (260px)
- Tablet: collapsible sidebar (overlay)
- Mobile: bottom tab bar (5 items max) + hamburger for more
### Tables:
- Desktop: full table
- Mobile: card layout (each row becomes a card)
### Campaign Cards:
- Desktop: 3-column grid
- Tablet: 2-column grid
- Mobile: single column, full width
### Video Player:
- Desktop: 16:9 player with side panel for comments
- Mobile: full-width player, comments below
---
## RTL (Right-to-Left) Handling
- Layout completely mirrors for Arabic
- Text alignment flips
- Icons that imply direction (arrows, chevrons) flip
- Icons that don't imply direction (close X, play, etc.) do NOT flip
- Numbers stay LTR even in RTL context
- CSS: `[dir="rtl"]` selectors for all directional properties
- Tailwind: `rtl:` modifier for RTL-specific styles
---
## Toast Notifications
- Position: top-right (LTR), top-left (RTL)
- Duration: 4 seconds (auto-dismiss)
- Types: success (green), error (red), warning (orange), info (blue)
- Stackable (max 3 visible, queue others)
- Dismissable (X button)
- Action button optional ("Undo", "View", etc.)
---
## Modals & Overlays
| Use Case | Pattern |
|----------|---------|
| Confirmation | Small centered modal (max 400px wide) |
| Create/Edit forms | Right-side drawer (500px) on desktop, full-screen on mobile |
| Video player | Full-width modal overlay with dark background |
| Image preview | Lightbox with navigation arrows |
| Quick actions | Dropdown menu (positioned relative to trigger) |
---
## Keyboard Shortcuts (Power Users)
| Shortcut | Action |
|----------|--------|
| / | Focus search |
| N | New (campaign/message depending on context) |
| Escape | Close modal/drawer |
| Cmd+Enter | Submit form |
| J/K | Navigate list items |
| R | Reply (in message thread) |
---
## Accessibility Minimums
- All interactive elements keyboard-accessible
- Focus rings visible
- Color contrast ratio: 4.5:1 minimum
- Alt text on all images
- ARIA labels on icon-only buttons
- Screen reader announcements for dynamic content
- Skip-to-content link
- Form labels properly associated
- Error messages linked to fields via aria-describedby
# 19 — Global Error Handling & Cross-Cutting Edge Cases
## Error Handling Strategy
### Exception Hierarchy:
```php
App\Exceptions\
├── BusinessException (base for all domain errors)
├── InsufficientPermissionException
├── InvalidStatusTransitionException
├── ResourceNotFoundException
├── DuplicateActionException
├── RateLimitExceededException
└── AccountStatusException
├── IntegrationException (external services)
├── PeerTubeException
├── StorageException
└── EmailDeliveryException
└── SystemException (infra-level)
├── DatabaseConnectionException
└── CacheUnavailableException
```
### Error Response Structure:
```json
{
"success": false,
"message": "Human-readable message",
"error_code": "CAMPAIGN_ALREADY_CLOSED",
"errors": {},
"trace_id": "abc123-def456"
}
```
### Error Codes (machine-readable):
```
AUTH_INVALID_CREDENTIALS
AUTH_ACCOUNT_SUSPENDED
AUTH_ACCOUNT_BANNED
AUTH_EMAIL_UNVERIFIED
AUTH_TOKEN_EXPIRED
AUTH_RATE_LIMITED
PROFILE_INCOMPLETE
PROFILE_USERNAME_TAKEN
PROFILE_USERNAME_CHANGE_COOLDOWN
CAMPAIGN_NOT_FOUND
CAMPAIGN_ALREADY_CLOSED
CAMPAIGN_CANNOT_APPLY_OWN
CAMPAIGN_APPLICATION_DEADLINE_PASSED
CAMPAIGN_ALREADY_APPLIED
CAMPAIGN_INVITE_ONLY
CAMPAIGN_MAX_APPLICATIONS
APPLICATION_NOT_FOUND
APPLICATION_ALREADY_ACCEPTED
APPLICATION_CANNOT_WITHDRAW
APPLICATION_CANNOT_EDIT_VIEWED
INVITATION_NOT_FOUND
INVITATION_EXPIRED
INVITATION_ALREADY_RESPONDED
INVITATION_DAILY_LIMIT
PROJECT_NOT_FOUND
PROJECT_INVALID_STATUS_TRANSITION
PROJECT_CANNOT_CANCEL_APPROVED
PROJECT_OVERDUE
DELIVERABLE_NOT_FOUND
DELIVERABLE_CANNOT_SUBMIT
DELIVERABLE_ALREADY_APPROVED
UPLOAD_TOO_LARGE
UPLOAD_INVALID_TYPE
UPLOAD_PEERTUBE_FAILED
UPLOAD_PROCESSING_FAILED
MESSAGE_CONVERSATION_BLOCKED
MESSAGE_RATE_LIMITED
MESSAGE_CANNOT_EDIT_EXPIRED
REVIEW_WINDOW_CLOSED
REVIEW_ALREADY_EXISTS
REVIEW_EDIT_WINDOW_CLOSED
```
---
## Cross-Cutting Edge Cases (Master List)
### Account State Changes During Active Work
| Scenario | Behavior |
|----------|----------|
| Creator suspended while project in_progress | Project → on_hold, company notified, admin alerted |
| Company suspended while project in_progress | Project → on_hold, creator notified, admin alerted |
| Creator banned while project in_progress | Project → cancelled (admin), company gets priority support |
| Company banned with active campaigns | All campaigns → cancelled, all projects → disputed |
| Creator deactivates with pending applications | Applications auto-withdrawn |
| Company deactivates with active projects | Projects flagged for admin, NOT auto-cancelled |
| User deleted with reviews | Reviews preserved but author shown as "Deleted User" |
| User re-activates after deactivation | All data restored, projects/campaigns NOT auto-resumed |
### Concurrent Action Conflicts
| Scenario | Resolution |
|----------|-----------|
| Company accepts application while creator withdraws | First action wins (database lock). If withdraw processed first → accept fails with "application withdrawn" |
| Two admins approve same company simultaneously | Idempotent — second approval is no-op |
| Creator submits while company is requesting revision | Both succeed — new submission + revision request both created. Creator sees revision note for previous version. |
| Company approves deliverable v2 while creator uploads v3 | Approval wins. v3 upload saved but deliverable stays "approved" (creator notified: "already approved") |
| Creator applies to campaign that closes mid-submission | Application deadline checked at submit time. If closed between page load and submit → error message |
### Data Integrity
| Scenario | Resolution |
|----------|-----------|
| PeerTube upload succeeds but DB write fails | Retry DB write. If still fails, log orphan video UUID for cleanup cron |
| DB write succeeds but PeerTube upload fails | Submission created with processing_status = "failed". Creator sees retry button. |
| Campaign deleted with active applications | Soft delete. Applications still visible to creators as "campaign no longer available" |
| Creator profile incomplete drops below 50% | Active applications NOT withdrawn. But new applications blocked until fixed. |
| Message sent to blocked conversation | Reject at API level. Message not stored. |
| Notification created for deleted entity | Action URL points to 404. UI shows "This item no longer exists." |
### Timing & Scheduling
| Scenario | Resolution |
|----------|-----------|
| Campaign deadline in past (clock skew) | Check deadline against server time, not client. Grace period of 5 minutes for submission. |
| Invitation expires at exact moment of acceptance | Accept request includes timestamp. If within 1 minute of expiry, honor the acceptance. |
| Scheduled campaign close while admin is editing | Admin edit takes precedence. Scheduled close checks status before closing. |
| User in timezone where "tomorrow" is ambiguous | All deadlines stored in UTC. Display in user's timezone. Deadline = end of day (23:59:59) in campaign creator's timezone. |
| Cron job to expire invitations runs while user is accepting | Database transaction with row lock on invitation. |
### File & Upload Edge Cases
| Scenario | Resolution |
|----------|-----------|
| Upload interrupted at 95% | Resumable uploads (PeerTube supports this). Frontend shows resume option. |
| Duplicate file uploaded | Allow (different versions/contexts may have same content). No dedup. |
| File passes validation but PeerTube rejects | Show error to user: "Video could not be processed. Try a different format." |
| Storage quota exceeded (PeerTube per-user) | Admin alerted. User sees "Storage limit reached. Contact support." |
| Video transcoding takes 30+ minutes | Show "processing" state. Don't block submission — mark as "processing" in deliverable view. |
| EXIF data in uploaded images contains GPS | Strip EXIF on upload (privacy). |
| User uploads PDF disguised as .jpg | MIME type validation (check magic bytes, not just extension). |
| Very long video (2+ hours) | Reject at validation: max 60 minutes for portfolio, project deliverable max based on campaign spec. |
### Search & Discovery Edge Cases
| Scenario | Resolution |
|----------|-----------|
| Creator updates profile, search index stale | Materialized view refreshed every 15 min. Acceptable lag for MVP. |
| Search returns 10,000 results | Pagination enforced. Max 100 per page. Total count estimated (not exact for performance). |
| SQL injection in search query | Parameterized queries (Eloquent handles this). |
| Arabic + English mixed in same search | pg_trgm handles mixed scripts. Search against both language indexes. |
| Creator in search results but profile returns 404 | Race condition (deactivated after search cache). Show "Profile unavailable" gracefully. |
---
## Graceful Degradation
### If PeerTube is Down:
- Portfolio video playback: show "Video temporarily unavailable" placeholder
- Video upload: disable upload button, show "Video service maintenance"
- Existing submissions: show thumbnail + "Video processing" message
- DO NOT block the entire platform
### If Redis is Down:
- Sessions: fall back to database sessions
- Cache: bypass cache, hit DB directly (slower but functional)
- Queues: jobs processed synchronously (degraded performance)
- Real-time: WebSocket features disabled, polling fallback
### If Email Service is Down:
- Queue emails for retry (exponential backoff)
- Critical actions still succeed (don't fail user action because email fails)
- Show warning: "Email notification may be delayed"
- Admin dashboard: email service health indicator
---
## Rate Limiting Strategy
### Per-User Limits:
```
Login attempts: 5 per minute, lockout 30 min
API general: 60 per minute
Search: 30 per minute
File upload: 10 per minute
Message send: 30 per minute
Application submit: 5 per minute
Invitation send: 50 per day
Report submit: 10 per day
```
### Per-IP Limits (unauthenticated):
```
Login attempts: 20 per minute (shared IP like office)
Registration: 3 per hour
Password reset: 5 per hour
Public API: 30 per minute
```
### Response on Rate Limit:
```json
HTTP 429
{
"success": false,
"message": "Too many requests. Try again in 45 seconds.",
"error_code": "RATE_LIMITED",
"retry_after": 45
}
```
Header: `Retry-After: 45`
---
## Scheduled Jobs (Cron)
| Job | Frequency | Description |
|-----|-----------|-------------|
| expire_invitations | every 5 min | Set expired status on past-due invitations |
| close_campaigns | every 5 min | Close campaigns past application_deadline |
| overdue_projects | daily 00:00 | Flag projects past deadline |
| refresh_search_index | every 15 min | Refresh materialized views |
| cleanup_orphan_files | daily 03:00 | Remove temp uploads older than 24h |
| send_digest_emails | daily (user's time) | Daily notification digest |
| reputation_recalculate | every 6 hours | Recalculate reputation scores |
| inactive_user_reminder | weekly | Email users inactive 30+ days |
| deactivated_account_cleanup | daily | Soft-delete accounts deactivated 30+ days |
| external_link_check | weekly | Verify external portfolio URLs still valid |
| peertube_sync | every 10 min | Check video processing status |
| session_cleanup | daily | Remove expired sessions |
| campaign_deadline_reminders | every hour | Send 24h/3d deadline reminders |
---
## Health Checks
### Endpoint: `/api/health`
```json
{
"status": "healthy",
"timestamp": "2024-03-15T10:30:00Z",
"services": {
"database": "up",
"redis": "up",
"peertube": "up",
"storage": "up",
"email": "up"
},
"version": "1.0.0"
}
```
### Monitoring:
- Response time > 2s → alert
- Error rate > 1% → alert
- PeerTube unreachable → alert
- Disk space > 80% → warning
- Queue backlog > 1000 → alert
- Failed jobs > 10 in 5 min → alert
# 20 — Developer Guidelines & Coding Standards
## Architecture Rules (Non-Negotiable)
### 1. Module Independence
Each module MUST be self-contained. No module directly imports from another module's internal classes. Communication between modules happens via:
- Events (async, decoupled)
- Service interfaces (injected via DI container)
- Shared DTOs (Data Transfer Objects in `App\Shared\DTOs`)
### 2. Controller Rules
```php
// GOOD: Thin controller
public function store(CreateCampaignRequest $request): JsonResponse
{
$campaign = $this->campaignService->create($request->validated());
return response()->json(new CampaignResource($campaign), 201);
}
// BAD: Fat controller
public function store(Request $request): JsonResponse
{
$request->validate([...]); // No - use FormRequest
$campaign = Campaign::create([...]); // No - use Service
Mail::send(...); // No - use Event
Notification::send(...); // No - use Event
return response()->json($campaign); // No - use Resource
}
```
### 3. Service Layer
```php
class CampaignService
{
public function create(array $data): Campaign
{
return DB::transaction(function () use ($data) {
$campaign = Campaign::create($data);
if (isset($data['attachments'])) {
$this->attachmentService->storeMany($campaign, $data['attachments']);
}
event(new CampaignCreated($campaign));
return $campaign;
});
}
}
```
### 4. Event-Driven Side Effects
```php
// When something happens → dispatch event
event(new ApplicationAccepted($application));
// Listeners handle side effects independently
class CreateProjectOnAcceptance { ... }
class NotifyCreatorOfAcceptance { ... }
class UpdateCampaignStats { ... }
class LogApplicationAccepted { ... }
```
### 5. Authorization via Policies
```php
// GOOD
$this->authorize('approve', $application);
// BAD
if ($user->role !== 'company') { abort(403); }
if ($campaign->company_id !== $user->company->id) { abort(403); }
```
---
## Naming Conventions
### PHP:
```
Models: Campaign, CreatorProfile (singular, PascalCase)
Controllers: CampaignController (singular + Controller)
Services: CampaignService (singular + Service)
Requests: CreateCampaignRequest, UpdateCampaignRequest
Resources: CampaignResource, CampaignCollection
Events: CampaignCreated, ApplicationAccepted (past tense)
Listeners: SendApplicationNotification (verb phrase)
Jobs: ProcessVideoUpload (verb phrase)
Policies: CampaignPolicy (singular + Policy)
Middleware: EnsureProfileComplete (verb phrase)
Enums: CampaignStatus, ApplicationStatus (singular + concept)
```
### Database:
```
Tables: campaigns, creator_profiles (plural, snake_case)
Columns: created_at, company_id (snake_case)
Foreign keys: campaign_id (singular_table + _id)
Pivot tables: campaign_creator (alphabetical, singular)
Indexes: idx_campaigns_status, idx_creator_country
```
### Routes:
```
API: /api/v1/campaigns (plural, kebab-case)
Web: /creator/campaigns (role prefix + resource)
Parameters: /campaigns/{campaign} (singular)
```
### Files:
```
Migrations: 2024_03_15_000001_create_campaigns_table
Seeders: CampaignSeeder
Factories: CampaignFactory
Tests: CampaignServiceTest, CampaignControllerTest
```
---
## Request Validation Pattern
```php
class CreateCampaignRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->role === 'company'
&& $this->user()->status === 'active';
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:10', 'max:100'],
'description' => ['required', 'string', 'min:100', 'max:5000'],
'objective' => ['required', Rule::in(CampaignObjective::values())],
'languages' => ['required', 'array', 'min:1'],
'languages.*' => ['string', Rule::in(Language::values())],
'application_deadline' => ['required', 'date', 'after:now'],
'project_deadline' => ['required', 'date', 'after:application_deadline'],
'budget_amount' => ['required_if:budget_type,fixed,per_creator', 'numeric', 'min:0'],
];
}
public function messages(): array
{
return [
'title.min' => __('campaigns.title_too_short'),
'description.min' => __('campaigns.description_too_short'),
];
}
}
```
---
## API Resource Pattern
```php
class CampaignResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->uuid, // Always expose UUID, never internal ID
'title' => $this->title,
'slug' => $this->slug,
'description' => $this->description,
'company' => new CompanyResource($this->whenLoaded('company')),
'status' => $this->status,
'budget' => [
'type' => $this->budget_type,
'amount' => $this->budget_amount,
'currency' => $this->budget_currency,
],
'deadline' => $this->application_deadline->toIso8601String(),
'stats' => [
'views' => $this->views_count,
'applications' => $this->applications_count,
],
'match_score' => $this->when(
auth()->check() && auth()->user()->role === 'creator',
fn() => $this->calculateMatchScore(auth()->user())
),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
```
---
## Status Transition Pattern
```php
class ProjectStatus
{
const TRANSITIONS = [
'not_started' => ['in_progress', 'cancelled', 'on_hold'],
'in_progress' => ['waiting_review', 'cancelled', 'on_hold', 'disputed'],
'waiting_review' => ['approved', 'revision_requested', 'disputed'],
'revision_requested' => ['in_progress'],
'approved' => ['completed'],
'on_hold' => ['in_progress', 'not_started', 'cancelled'],
'completed' => [], // terminal
'cancelled' => [], // terminal
'disputed' => ['cancelled', 'completed'], // admin only
];
public static function canTransition(string $from, string $to): bool
{
return in_array($to, self::TRANSITIONS[$from] ?? []);
}
}
// In service:
public function changeStatus(Project $project, string $newStatus): void
{
if (!ProjectStatus::canTransition($project->status, $newStatus)) {
throw new InvalidStatusTransitionException(
"Cannot transition from {$project->status} to {$newStatus}"
);
}
$oldStatus = $project->status;
$project->update(['status' => $newStatus]);
event(new ProjectStatusChanged($project, $oldStatus, $newStatus));
}
```
---
## Testing Strategy
### Unit Tests (Services):
```php
class CampaignServiceTest extends TestCase
{
public function test_create_campaign_with_valid_data(): void { ... }
public function test_cannot_publish_incomplete_campaign(): void { ... }
public function test_closing_campaign_notifies_applicants(): void { ... }
}
```
### Feature Tests (API Endpoints):
```php
class CampaignApiTest extends TestCase
{
public function test_creator_can_list_published_campaigns(): void
{
$creator = User::factory()->creator()->create();
Campaign::factory()->published()->count(3)->create();
Campaign::factory()->draft()->count(2)->create();
$response = $this->actingAs($creator)->getJson('/api/v1/campaigns');
$response->assertOk()
->assertJsonCount(3, 'data'); // Only published visible
}
public function test_company_cannot_apply_to_campaign(): void
{
$company = User::factory()->company()->create();
$campaign = Campaign::factory()->published()->create();
$response = $this->actingAs($company)
->postJson("/api/v1/campaigns/{$campaign->uuid}/apply", [...]);
$response->assertForbidden();
}
}
```
### What To Test:
- Every status transition (valid + invalid)
- Every permission boundary (authorized + unauthorized)
- Every validation rule (valid + each invalid case)
- Edge cases from the docs (concurrent actions, race conditions)
- PeerTube integration (mocked)
---
## File Structure (Final)
```
app/
├── Modules/
│ ├── Auth/
│ │ ├── Controllers/
│ │ │ ├── LoginController.php
│ │ │ ├── RegisterController.php
│ │ │ ├── ForgotPasswordController.php
│ │ │ └── VerificationController.php
│ │ ├── Requests/
│ │ ├── Services/AuthService.php
│ │ └── Routes/auth.php
│ ├── Users/
│ │ ├── Models/User.php
│ │ ├── Enums/UserRole.php, UserStatus.php
│ │ └── ...
│ ├── Creators/
│ │ ├── Models/CreatorProfile.php
│ │ ├── Controllers/
│ │ │ ├── CreatorProfileController.php
│ │ │ └── CreatorDashboardController.php
│ │ ├── Services/CreatorService.php
│ │ ├── Resources/CreatorResource.php
│ │ ├── Requests/UpdateCreatorProfileRequest.php
│ │ ├── Policies/CreatorProfilePolicy.php
│ │ └── ...
│ ├── Companies/
│ │ └── (mirror structure)
│ ├── Campaigns/
│ │ ├── Models/Campaign.php, CampaignAttachment.php
│ │ ├── Enums/CampaignStatus.php, CampaignObjective.php
│ │ ├── Services/CampaignService.php
│ │ ├── Events/CampaignCreated.php, CampaignPublished.php
│ │ └── ...
│ ├── Applications/
│ │ └── ...
│ ├── Invitations/
│ │ └── ...
│ ├── Projects/
│ │ ├── Models/Project.php, Deliverable.php, Submission.php
│ │ ├── Enums/ProjectStatus.php, DeliverableStatus.php
│ │ └── ...
│ ├── VideoReview/
│ │ ├── Models/TimestampComment.php, RevisionRequest.php
│ │ └── ...
│ ├── Portfolios/
│ │ └── ...
│ ├── Messaging/
│ │ └── ...
│ ├── Notifications/
│ │ └── ...
│ ├── Reviews/
│ │ └── ...
│ ├── Reputation/
│ │ └── ...
│ ├── Matching/
│ │ └── ...
│ ├── Reporting/
│ │ └── ...
│ ├── PeerTube/
│ │ ├── Services/PeerTubeService.php
│ │ ├── Jobs/ProcessVideoUpload.php
│ │ └── ...
│ └── Admin/
│ ├── Controllers/
│ │ ├── AdminDashboardController.php
│ │ ├── AdminUserController.php
│ │ ├── AdminCompanyController.php
│ │ └── ...
│ └── ...
├── Shared/
│ ├── DTOs/
│ ├── Traits/
│ │ ├── HasUuid.php
│ │ ├── HasActivityLog.php
│ │ └── Searchable.php
│ ├── Enums/
│ └── Services/
│ ├── FileUploadService.php
│ └── SlugService.php
├── Http/
│ ├── Middleware/
│ │ ├── EnsureEmailVerified.php
│ │ ├── EnsureProfileComplete.php
│ │ ├── EnsureCompanyApproved.php
│ │ └── SetLocale.php
│ └── Kernel.php
├── Providers/
│ ├── ModuleServiceProvider.php
│ └── EventServiceProvider.php
└── Exceptions/
├── Handler.php
└── (exception classes)
```
---
## Environment Variables
```env
# App
APP_NAME="UGC Heaven"
APP_URL=https://ugcheaven.caprover.al-arcade.com
APP_LOCALE=en
APP_SUPPORTED_LOCALES=en,ar
# Database
DB_CONNECTION=pgsql
DB_HOST=srv-captain--ugc-heaven-db
DB_PORT=5432
DB_DATABASE=ugc_heaven
DB_USERNAME=ugcadmin
DB_PASSWORD=UgcH3aven2024!
# Redis
REDIS_HOST=srv-captain--ugc-heaven-redis
REDIS_PORT=6379
# PeerTube
PEERTUBE_URL=https://ugcvideoserver.caprover.al-arcade.com
PEERTUBE_CLIENT_ID=b148jgk5ffimzszm8cqa4fut2o5u2jf7
PEERTUBE_CLIENT_SECRET=sRXMboZI4941vn1cs3GSJIhtyCAVli32
PEERTUBE_ADMIN_USERNAME=root
PEERTUBE_ADMIN_PASSWORD=Alarcade123#
# Storage
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=eu-central-1
AWS_BUCKET=ugc-heaven
# Mail
MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=noreply@ugcheaven.com
MAIL_FROM_NAME="UGC Heaven"
# Queue
QUEUE_CONNECTION=redis
# Broadcasting (WebSocket)
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=ugcheaven
REVERB_APP_KEY=
REVERB_APP_SECRET=
```
---
## Git Workflow
- `main` — production
- `staging` — staging/testing
- `develop` — integration branch
- Feature branches: `feature/campaign-creation`
- Bug fixes: `fix/application-status-race`
- Hotfixes: `hotfix/login-lockout`
### Commit Messages:
```
feat(campaigns): add campaign creation wizard
fix(applications): prevent duplicate applications race condition
refactor(projects): extract status transition logic to service
chore(deps): update laravel to 11.x
```
---
## Performance Targets
| Metric | Target |
|--------|--------|
| Page load (TTFB) | < 200ms |
| API response (simple) | < 100ms |
| API response (with joins) | < 300ms |
| Search response | < 500ms |
| File upload start | < 1s |
| Video processing | < 5 min (depends on size) |
| WebSocket latency | < 100ms |
# 21 — Platform Theming & CMS (SuperAdmin-Controlled)
## Philosophy
The SuperAdmin can change the entire look and feel of the platform **without touching code**. Every visual aspect is driven by a configuration layer that the admin edits from a dashboard. The developer builds the system once; the admin skins it forever.
---
## What the SuperAdmin Can Control
### 1. Brand Identity
| Setting | Type | Default | Notes |
|---------|------|---------|-------|
| platform_name | string | "UGC Heaven" | Shown in header, emails, meta tags |
| platform_tagline | string | "Where brands meet creators" | Landing page, meta description |
| logo_light | image | — | For dark backgrounds |
| logo_dark | image | — | For light backgrounds |
| logo_icon | image | — | Favicon, small spaces |
| logo_full | image | — | With wordmark |
### 2. Color System (Design Tokens)
All colors stored as CSS custom properties. Admin picks from a color picker UI:
| Token | Purpose | Default |
|-------|---------|---------|
| --color-primary | Buttons, links, active states | #4F46E5 (indigo) |
| --color-primary-hover | Hover state | #4338CA |
| --color-primary-light | Backgrounds, badges | #EEF2FF |
| --color-secondary | Secondary actions | #0D9488 (teal) |
| --color-secondary-hover | Hover | #0F766E |
| --color-secondary-light | Backgrounds | #F0FDFA |
| --color-accent | Highlights, premium badges | #F59E0B (amber) |
| --color-success | Success states | #10B981 |
| --color-warning | Warning states | #F59E0B |
| --color-error | Error states | #EF4444 |
| --color-info | Informational | #3B82F6 |
| --color-background | Page background | #FFFFFF |
| --color-surface | Card/panel backgrounds | #F8FAFC |
| --color-surface-hover | Card hover | #F1F5F9 |
| --color-border | Borders | #E2E8F0 |
| --color-text-primary | Main text | #1E293B |
| --color-text-secondary | Muted text | #64748B |
| --color-text-on-primary | Text on primary buttons | #FFFFFF |
| --color-sidebar-bg | Sidebar background | #1E293B |
| --color-sidebar-text | Sidebar text | #CBD5E1 |
| --color-sidebar-active | Active sidebar item | #4F46E5 |
| --color-header-bg | Header background | #FFFFFF |
| --color-footer-bg | Footer background | #0F172A |
| --color-footer-text | Footer text | #94A3B8 |
**Admin UI:** visual color picker with live preview panel. Can also input hex codes directly.
**Dark Mode:**
- Separate set of tokens for dark mode
- Admin controls both light + dark palettes
- Users can toggle (or follow system preference)
### 3. Typography
| Setting | Type | Options | Default |
|---------|------|---------|---------|
| font_heading_en | select | Inter, Poppins, Plus Jakarta Sans, DM Sans, Outfit | Inter |
| font_body_en | select | Inter, Poppins, Plus Jakarta Sans, DM Sans, Outfit, System | Inter |
| font_heading_ar | select | IBM Plex Arabic, Noto Sans Arabic, Cairo, Tajawal, Almarai | IBM Plex Arabic |
| font_body_ar | select | IBM Plex Arabic, Noto Sans Arabic, Cairo, Tajawal, Almarai | Noto Sans Arabic |
| font_size_base | select | 14px, 15px, 16px | 16px |
| heading_weight | select | 500, 600, 700, 800 | 700 |
| body_line_height | select | 1.4, 1.5, 1.6, 1.75 | 1.5 |
| border_radius | select | none (0), subtle (4px), rounded (8px), pill (16px) | rounded (8px) |
### 4. Layout & Spacing
| Setting | Type | Options | Default |
|---------|------|---------|---------|
| sidebar_style | select | fixed, collapsible, floating, hidden | fixed |
| sidebar_width | select | narrow (220px), default (260px), wide (300px) | default |
| content_max_width | select | narrow (1024px), default (1280px), wide (1440px) | default |
| card_style | select | flat, shadow, bordered, elevated | shadow |
| card_padding | select | compact, default, spacious | default |
| button_style | select | rounded, pill, square | rounded |
| table_style | select | striped, bordered, clean, minimal | clean |
| avatar_style | select | circle, rounded_square, square | circle |
| density | select | compact, comfortable, spacious | comfortable |
### 5. Landing Page (CMS)
SuperAdmin can edit the public landing page without code:
| Section | Editable Fields | Notes |
|---------|----------------|-------|
| Hero | headline, subheadline, CTA text, CTA link, background image/video, overlay color | |
| Features | title, array of {icon, title, description} (max 6) | |
| How It Works | title, array of {step_number, title, description, image} (max 5) | |
| Stats | array of {number, label} (max 4) e.g., "500+ Creators" | |
| Testimonials | array of {quote, name, role, avatar} (max 6) | |
| CTA Banner | headline, description, button_text, button_link, background_color | |
| Footer | columns of links, social links, copyright text, legal links | |
**Section Ordering:** Admin can drag-and-drop sections to reorder.
**Section Visibility:** Toggle any section on/off.
### 6. Navigation
| Setting | Type | Notes |
|---------|------|-------|
| header_links | array | Public nav items [{label, url, new_tab}] |
| footer_columns | array | Footer link groups [{title, links: [{label, url}]}] |
| creator_sidebar_items | array | Can reorder/hide sidebar items |
| company_sidebar_items | array | Can reorder/hide sidebar items |
| admin_sidebar_items | array | Can reorder/hide sidebar items |
### 7. Email Templates
| Template | Editable Fields |
|----------|----------------|
| All emails | header_logo, footer_text, primary_color, font |
| Welcome | subject, body_html, CTA_text |
| Verification | subject, body_html |
| Application Accepted | subject, body_html |
| (every notification email) | subject, body_html (with variable placeholders) |
**Variable Placeholders:** `{{user_name}}`, `{{campaign_title}}`, `{{project_title}}`, etc.
**Preview:** admin sees rendered preview before saving.
### 8. Pages (Static CMS)
SuperAdmin can create/edit static pages:
| Page | URL | Editable |
|------|-----|----------|
| About Us | /about | Rich text + images |
| Terms of Service | /terms | Rich text |
| Privacy Policy | /privacy | Rich text |
| FAQ | /faq | Array of {question, answer} |
| Contact | /contact | Form fields config + email target |
| Custom pages | /pages/{slug} | Rich text + images |
---
## How It Works Technically
### Storage: `platform_settings` table
```sql
CREATE TABLE platform_settings (
id BIGSERIAL PRIMARY KEY,
group_name VARCHAR(50) NOT NULL,
key VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
updated_by BIGINT NULL REFERENCES users(id),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(group_name, key)
);
```
**Groups:**
- `branding` — logo, name, tagline
- `colors` — all color tokens
- `colors_dark` — dark mode tokens
- `typography` — fonts, sizes
- `layout` — sidebar, spacing, card styles
- `landing` — landing page sections
- `navigation` — header/footer/sidebar links
- `emails` — email template customizations
- `pages` — static page content
- `features` — feature toggles
- `limits` — platform limits
### CSS Variable Injection
On every page load, a middleware injects the current theme as CSS variables:
```php
// Middleware: InjectThemeVariables
public function handle($request, Closure $next)
{
$colors = Cache::remember('theme.colors', 3600, function () {
return PlatformSetting::where('group_name', 'colors')->pluck('value', 'key');
});
View::share('themeColors', $colors);
return $next($request);
}
```
```blade
{{-- In layout head --}}
<style>
:root {
@foreach($themeColors as $key => $value)
--color-{{ $key }}: {{ $value }};
@endforeach
}
</style>
```
All Tailwind/CSS classes reference these variables:
```css
.btn-primary {
background-color: var(--color-primary);
color: var(--color-text-on-primary);
}
.btn-primary:hover {
background-color: var(--color-primary-hover);
}
```
### Cache Strategy
- All settings cached in Redis (key: `platform_settings:{group}`)
- Cache cleared on any setting update
- TTL: 1 hour (with manual invalidation on change)
- Fallback to defaults if cache AND DB fail
---
## Theme Presets
SuperAdmin can choose from built-in presets OR create custom:
| Preset | Vibe |
|--------|------|
| Professional (default) | Indigo + slate, clean, corporate |
| Creative | Purple + pink gradients, playful |
| Minimal | Black + white, Swiss design |
| Dark | Dark backgrounds, neon accents |
| MENA | Deep green + gold, Arabic-optimized |
| Warm | Amber + brown, organic feel |
**Save as Preset:** admin can save current config as named preset.
**Restore Preset:** one click to apply a preset (with confirmation).
---
## Feature Toggles (Admin Controls)
| Feature | Toggle | Default | Effect When Off |
|---------|--------|---------|-----------------|
| Creator registration | on/off | on | Registration page shows "Registration closed" |
| Company registration | on/off | on | Same |
| Public campaign browsing | on/off | on | Requires login to see campaigns |
| Portfolio uploads | on/off | on | Upload button hidden |
| Messaging | on/off | on | Messaging section hidden |
| Reviews | on/off | on | Review prompts suppressed |
| Invitations | on/off | on | Invite button hidden |
| Dark mode | on/off | on | Toggle hidden from users |
| Maintenance mode | on/off | off | Shows maintenance page to all non-admins |
| New user onboarding | on/off | on | Skip onboarding flow |
---
## Admin Theme Editor UI
### Layout:
```
┌─────────────────────────────────────────────────────┐
│ Theme Editor [Preview] │
├───────────────┬─────────────────────────────────────┤
│ │ │
│ Sidebar: │ Live Preview Panel │
│ • Brand │ (iframe showing actual pages │
│ • Colors │ with current settings applied) │
│ • Typography │ │
│ • Layout │ │
│ • Landing │ │
│ • Navigation │ │
│ • Emails │ │
│ • Pages │ │
│ • Features │ │
│ │ │
├───────────────┴─────────────────────────────────────┤
│ [Reset to Default] [Save Draft] [Publish] │
└─────────────────────────────────────────────────────┘
```
### Key UX Details:
- **Live preview:** changes show immediately in preview panel (not saved yet)
- **Save Draft:** saves but doesn't go live (admin can review)
- **Publish:** applies changes to live site
- **History:** last 10 published states saved, can rollback
- **Compare:** side-by-side current vs. draft
---
## Edge Cases
1. **Admin sets white text on white background** → preview shows contrast warning, but doesn't block (admin knows best)
2. **Admin uploads 50MB logo** → max 5MB enforced, WebP recommended
3. **Admin breaks landing page layout** → "Reset to Default" always available per section
4. **Two admins editing theme simultaneously** → last-save-wins with warning "Settings were modified by {other_admin} at {time}"
5. **Custom font not loading** → fallback chain: custom → system fonts → sans-serif
6. **Admin disables registration while users are mid-registration** → users already on the form can complete, new visits see "closed"
7. **Theme cache stale after update** → cache busted on save via event listener
8. **Landing page image too large** → auto-resize on upload (max 1920px wide, WebP conversion)
9. **Email template broken (missing closing tag)** → preview + validation before save, reject invalid HTML
10. **Admin wants to A/B test themes** → not in MVP, but preset system enables quick switches
---
## Platform Limits (Admin-Configurable)
| Setting | Default | Admin Can Change |
|---------|---------|-----------------|
| max_portfolio_items | 100 | yes |
| max_portfolio_file_size_mb | 2048 | yes |
| max_campaigns_per_company | 50 (simultaneous active) | yes |
| max_applications_per_creator_per_day | 20 | yes |
| max_invitations_per_company_per_day | 50 | yes |
| max_file_upload_size_mb | 25 | yes |
| max_video_upload_size_mb | 2048 | yes |
| max_message_length | 5000 | yes |
| session_timeout_minutes | 120 | yes |
| account_lockout_attempts | 5 | yes |
| account_lockout_duration_minutes | 30 | yes |
| profile_completion_minimum | 50 | yes |
| review_window_days | 30 | yes |
| invitation_expiry_default_days | 7 | yes |
| deactivation_cleanup_days | 30 | yes |
# 22 — Development Stages (Complete Build Plan)
## Overview
The platform is built in **6 phases**, each delivering a usable increment. No phase depends on a future phase. Each phase builds on the previous.
```
Phase 1: Foundation & Infrastructure (Week 1)
Phase 2: Core Entities & Auth (Week 1-2)
Phase 3: Marketplace Flow (Week 2-3)
Phase 4: Project Workflow & Video (Week 3-4)
Phase 5: Communication & Discovery (Week 4-5)
Phase 6: Admin, Polish & Deploy (Week 5-6)
```
---
## PHASE 1: Foundation & Infrastructure
**Goal:** A running Laravel app with all infrastructure connected, modular architecture in place, and base tooling configured. No features yet — just the skeleton that everything builds on.
---
### Stage 1.1: Project Initialization
**Tasks:**
- [ ] `laravel new ugc-heaven` (Laravel 11, PHP 8.2+)
- [ ] Remove default Laravel boilerplate (welcome view, example tests, etc.)
- [ ] Configure `.env` for PostgreSQL connection (use ugc-heaven-db on CapRover)
- [ ] Configure Redis connection (create Redis service on CapRover if not exists)
- [ ] Run `php artisan migrate` to verify DB connection
- [ ] Configure timezone to UTC in `config/app.php`
- [ ] Set up `.env.example` with all required vars (no secrets)
**Deliverable:** `php artisan serve` shows blank Laravel app, connected to Postgres + Redis.
---
### Stage 1.2: Module Architecture Setup
**Tasks:**
- [ ] Create module directory structure:
```
app/Modules/{Auth,Users,Creators,Companies,Campaigns,Applications,
Invitations,Projects,Deliverables,VideoReview,Portfolios,Messaging,
Notifications,Reviews,Reputation,Matching,Reporting,PeerTube,Admin}/
```
- [ ] Create `ModuleServiceProvider` that auto-registers each module's:
- Routes
- Migrations
- Event listeners
- Policies
- [ ] Each module gets its own subfolder structure:
```
Models/, Controllers/, Services/, Requests/, Resources/,
Policies/, Events/, Listeners/, Jobs/, Routes/, Database/Migrations/
```
- [ ] Create `app/Shared/` directory for cross-cutting concerns:
```
Shared/Traits/ (HasUuid, HasActivityLog, Searchable, HasSlug)
Shared/Enums/
Shared/DTOs/
Shared/Services/ (FileUploadService, SlugService)
```
- [ ] Register `ModuleServiceProvider` in `bootstrap/providers.php`
**Deliverable:** Modular architecture scaffolded. Adding a new module = create folder + register.
---
### Stage 1.3: Base Tooling & Config
**Tasks:**
- [ ] Install and configure packages:
```
composer require laravel/sanctum # API auth
composer require spatie/laravel-permission # roles (optional, can use custom)
composer require spatie/laravel-activitylog # activity logging
composer require intervention/image # image processing
composer require laravel/reverb # WebSocket (can defer to Phase 5)
```
- [ ] Configure Sanctum (token auth for API)
- [ ] Configure CORS (`config/cors.php`)
- [ ] Configure rate limiting in `RouteServiceProvider` / `bootstrap/app.php`
- [ ] Set up API versioning: all routes under `/api/v1/`
- [ ] Create base controller `App\Http\Controllers\ApiController` with standard response helpers:
```php
protected function success($data, $message = 'OK', $code = 200)
protected function error($message, $code = 400, $errors = [])
protected function paginated($query, $resource)
```
- [ ] Create global exception handler for consistent JSON error responses
- [ ] Set up localization:
- `lang/en/` and `lang/ar/` directories
- `SetLocale` middleware (reads `Accept-Language` header or user preference)
- Test `__('auth.login_failed')` works in both languages
**Deliverable:** Consistent API responses, auth tokens, localization, error handling all working.
---
### Stage 1.4: Database Foundations
**Tasks:**
- [ ] Enable PostgreSQL extensions:
```sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
```
- [ ] Create shared traits:
- `HasUuid` — auto-generates UUID on creation
- `HasSlug` — auto-generates unique slug from a field
- `HasActivityLog` — logs changes to activity_logs table
- [ ] Create `activity_logs` migration (first migration, everything references it)
- [ ] Create `platform_settings` migration (for theming/CMS system)
- [ ] Seed platform_settings with default theme values
- [ ] Verify: `php artisan migrate:fresh --seed` works cleanly
**Deliverable:** Database ready with extensions, base tables, and utility traits.
---
### Stage 1.5: File Storage & PeerTube Service
**Tasks:**
- [ ] Configure filesystem disks:
- `local` — temp uploads
- `public` — profile images, logos (publicly accessible)
- `s3` or `local` — project files, attachments (private, signed URLs)
- [ ] Create `FileUploadService`:
```php
class FileUploadService {
public function uploadImage(UploadedFile $file, string $path, array $sizes = []): string
public function uploadDocument(UploadedFile $file, string $path): string
public function delete(string $path): bool
public function getSignedUrl(string $path, int $minutes = 60): string
}
```
- Image processing: resize, crop, WebP conversion, multiple sizes
- Validation: mime type check (magic bytes), size limits
- EXIF stripping for privacy
- [ ] Create `PeerTubeService`:
```php
class PeerTubeService {
public function authenticate(): string // get/refresh OAuth token
public function uploadVideo(string $filePath, array $metadata): array
public function uploadResumable(string $filePath, array $metadata): array
public function getVideo(string $uuid): array
public function deleteVideo(string $uuid): bool
public function createChannel(string $name, string $displayName): array
public function createUser(string $username, string $email, string $password): array
public function getEmbedUrl(string $uuid): string
public function getVideoStatus(string $uuid): string
}
```
- OAuth token caching (24h, auto-refresh)
- Error handling for PeerTube downtime
- Retry logic (3 attempts with exponential backoff)
- [ ] Test: upload a video via PeerTubeService, get embed URL back
- [ ] Create `ProcessVideoUpload` job (queued):
```
Upload file → PeerTube → poll status → update submission_files record
```
**Deliverable:** File uploads work (images + documents). PeerTube integration tested. Video upload job ready.
---
### Stage 1.6: Frontend Foundation
**Tasks:**
- [ ] Choose frontend approach:
- **Option A:** Blade + Livewire + Alpine.js (full Laravel stack, SSR)
- **Option B:** Blade + Tailwind + Vanilla JS (lightweight, fast)
- **Option C:** Inertia.js + Vue/React (SPA-like with Laravel backend)
- **Recommended for this project:** Blade + Tailwind CSS + Alpine.js + Livewire for interactive parts
- [ ] Install Tailwind CSS + configure
- [ ] Install Alpine.js
- [ ] Create base layout:
- `layouts/app.blade.php` — authenticated layout (sidebar + header)
- `layouts/guest.blade.php` — unauthenticated (landing, auth pages)
- `layouts/admin.blade.php` — admin panel layout
- [ ] Implement CSS variable theming system:
```blade
<style>:root { @foreach($theme as $key => $val) --color-{{$key}}: {{$val}}; @endforeach }</style>
```
- [ ] Create base UI components (Blade components):
```
components/
├── ui/button.blade.php
├── ui/input.blade.php
├── ui/textarea.blade.php
├── ui/select.blade.php
├── ui/card.blade.php
├── ui/badge.blade.php
├── ui/avatar.blade.php
├── ui/modal.blade.php
├── ui/toast.blade.php
├── ui/table.blade.php
├── ui/pagination.blade.php
├── ui/empty-state.blade.php
├── ui/loading.blade.php
├── ui/dropdown.blade.php
├── ui/tabs.blade.php
├── ui/file-upload.blade.php
└── ui/stat-card.blade.php
```
- [ ] RTL support: `<html dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">`
- [ ] Tailwind config: extend with CSS variable references
- [ ] Dark mode toggle (CSS class-based)
- [ ] Responsive sidebar component
**Deliverable:** Styled, themed, RTL-ready layout with reusable components. Looks professional even with no features.
---
## PHASE 2: Core Entities & Authentication
**Goal:** Users can register, log in, complete profiles, and the admin can manage them.
---
### Stage 2.1: User Model & Authentication
**Tasks:**
- [ ] Create `users` migration (from doc 15 schema)
- [ ] Create `User` model with:
- HasUuid trait
- Role accessors (`isAdmin()`, `isCompany()`, `isCreator()`)
- Status accessors (`isActive()`, `isSuspended()`, etc.)
- Relationships (creatorProfile, companyProfile)
- Scopes (`scopeActive()`, `scopeByRole()`)
- [ ] Create `UserStatus` and `UserRole` enums
- [ ] Auth controllers:
- `RegisterCreatorController` — creator registration
- `RegisterCompanyController` — company registration
- `LoginController` — login (all roles)
- `LogoutController` — kill session/token
- `ForgotPasswordController` — send reset link
- `ResetPasswordController` — process reset
- `VerificationController` — email verification
- [ ] Form requests with full validation (per doc 02):
- `RegisterCreatorRequest`
- `RegisterCompanyRequest`
- `LoginRequest`
- [ ] Middleware:
- `EnsureEmailVerified` — block unverified users
- `EnsureCompanyApproved` — block pending companies
- `EnsureRoleIs:{role}` — role gate
- `EnsureProfileComplete` — block <50% profiles from certain actions
- [ ] Login throttling (5 attempts / 15 min → 30 min lockout)
- [ ] Password reset flow (token expiry, single-use)
- [ ] Session management (store in Redis, concurrent session limits)
- [ ] Activity logging for all auth events
- [ ] Routes: `/register/creator`, `/register/company`, `/login`, `/logout`, `/forgot-password`, `/reset-password`
- [ ] API routes: `/api/v1/auth/*`
**Deliverable:** Full auth flow working. Users can register, verify email, log in, reset password.
---
### Stage 2.2: Creator Profile
**Tasks:**
- [ ] Create `creator_profiles` migration (from doc 15)
- [ ] Create `CreatorProfile` model:
- Relationships: user, portfolioItems, applications, invitations, projects, reviews
- Scopes: active, searchable, byCountry, byNiche, byLanguage
- Accessors: fullName, profileUrl, avatarUrl, completionPercentage
- Mutators: normalizing social links on set
- [ ] `CreatorProfileService`:
```php
public function create(User $user, array $data): CreatorProfile
public function update(CreatorProfile $profile, array $data): CreatorProfile
public function updateAvatar(CreatorProfile $profile, UploadedFile $file): string
public function updateCover(CreatorProfile $profile, UploadedFile $file): string
public function calculateCompletion(CreatorProfile $profile): int
public function recalculateReputation(CreatorProfile $profile): int
```
- [ ] Profile completion calculator (per doc 03 weights)
- [ ] `UpdateCreatorProfileRequest` — full validation per doc 03
- [ ] `CreatorProfileController`:
- `show()` — public profile view
- `edit()` — edit form
- `update()` — process update
- `updateAvatar()` — upload profile pic
- `updateCover()` — upload cover
- [ ] `CreatorResource` — API response transformer
- [ ] `CreatorProfilePolicy` — authorization
- [ ] Creator onboarding flow (multi-step):
1. Basic info (name, bio, photo)
2. Location + languages
3. Niches + skills
4. Equipment (optional)
5. Social links (optional)
- [ ] Username change logic (once per 30 days)
- [ ] Views:
- `/creator/profile/edit` — edit own profile
- `/creators/{username}` — public profile page
- `/creator/onboarding` — onboarding wizard
- `/creator/dashboard` — creator home (placeholder for now)
**Deliverable:** Creators can register, complete onboarding, edit profile, and have a public profile page.
---
### Stage 2.3: Company Profile
**Tasks:**
- [ ] Create `company_profiles` migration (from doc 15)
- [ ] Create `CompanyProfile` model:
- Relationships: user, campaigns, projects, reviews, invitations
- Scopes: active, verified, byIndustry, byCountry
- Accessors: profileUrl, logoUrl, verificationBadge
- [ ] `CompanyProfileService`:
```php
public function create(User $user, array $data): CompanyProfile
public function update(CompanyProfile $profile, array $data): CompanyProfile
public function updateLogo(CompanyProfile $profile, UploadedFile $file): string
public function approve(CompanyProfile $profile, User $admin): void
public function reject(CompanyProfile $profile, User $admin, string $reason): void
```
- [ ] `UpdateCompanyProfileRequest` — validation per doc 04
- [ ] `CompanyProfileController`:
- `show()` — public page
- `edit()` — edit form
- `update()` — process
- `updateLogo()` — upload
- [ ] `CompanyResource` — API transformer
- [ ] `CompanyProfilePolicy` — authorization
- [ ] Company registration → pending_review status
- [ ] Views:
- `/company/profile/edit` — edit own profile
- `/companies/{slug}` — public company page
- `/company/dashboard` — company home (placeholder)
- Pending approval wall (shown when status = pending_review)
**Deliverable:** Companies can register, see pending wall, edit profile (once approved), have public page.
---
### Stage 2.4: Admin Foundation
**Tasks:**
- [ ] Create admin seeder (first superadmin account)
- [ ] Admin layout (`layouts/admin.blade.php`)
- [ ] Admin sidebar navigation
- [ ] Admin middleware (`EnsureRoleIs:superadmin`)
- [ ] `AdminDashboardController`:
- Stats: total users, creators, companies, pending approvals
- Quick links to pending items
- [ ] `AdminUserController`:
- List all users (paginated, filterable, searchable)
- View user detail
- Change user status (suspend, ban, activate)
- Impersonate user
- [ ] `AdminCompanyController`:
- Pending companies queue
- Approve/reject with reason
- View company detail
- [ ] User impersonation system:
- Admin clicks "View as" → session flag set
- Banner shown: "Viewing as {user}" + "End" button
- All actions logged
- Cannot impersonate other admins
- [ ] Activity log viewer (admin sees all auth events)
- [ ] Views:
- `/admin/dashboard`
- `/admin/users` — user list
- `/admin/users/{id}` — user detail
- `/admin/companies/pending` — approval queue
- `/admin/audit-log`
**Deliverable:** Admin can approve companies, manage users, view activity. Platform is operational for basic user management.
---
## PHASE 3: Marketplace Flow
**Goal:** Companies can post campaigns, creators can discover and apply, companies can invite creators. The two paths to "starting a project" are fully working.
---
### Stage 3.1: Campaign CRUD
**Tasks:**
- [ ] Create `campaigns` migration (from doc 15)
- [ ] Create `campaign_attachments` migration
- [ ] Create `Campaign` model:
- HasUuid, HasSlug traits
- Relationships: company, applications, projects, attachments
- Scopes: published, active, byNiche, byCountry, byLanguage, upcoming, expired
- Accessors: isOpen, isExpired, budgetDisplay, deadlineFormatted
- [ ] Create `CampaignAttachment` model
- [ ] `CampaignStatus` enum with transition rules
- [ ] `CampaignService`:
```php
public function create(CompanyProfile $company, array $data): Campaign
public function update(Campaign $campaign, array $data): Campaign
public function publish(Campaign $campaign): void
public function pause(Campaign $campaign): void
public function resume(Campaign $campaign): void
public function close(Campaign $campaign): void
public function cancel(Campaign $campaign, string $reason): void
public function addAttachment(Campaign $campaign, UploadedFile $file, string $type): CampaignAttachment
public function removeAttachment(CampaignAttachment $attachment): void
```
- [ ] Status transition validation (per doc 05 lifecycle)
- [ ] `CreateCampaignRequest`, `UpdateCampaignRequest` — full validation
- [ ] `CampaignController` (company side):
- `index()` — list my campaigns (with status tabs)
- `create()` — multi-step wizard
- `store()` — save campaign
- `edit()` / `update()` — modify
- `publish()` / `pause()` / `resume()` / `close()` / `cancel()`
- [ ] `CampaignResource` — API transformer
- [ ] `CampaignPolicy` — only owning company can edit/manage
- [ ] Slug generation (unique, from title)
- [ ] Campaign creation wizard UI (7 steps per doc 05)
- [ ] Auto-save as draft on each step
- [ ] Views:
- `/company/campaigns` — list own campaigns
- `/company/campaigns/create` — wizard
- `/company/campaigns/{id}/edit` — edit
- `/campaigns/{slug}` — public campaign page
**Deliverable:** Companies can create, publish, and manage campaigns. Public campaign pages work.
---
### Stage 3.2: Campaign Discovery
**Tasks:**
- [ ] `CampaignDiscoveryController` (creator side):
- `index()` — browse/search campaigns
- `show()` — campaign detail page
- [ ] Full-text search implementation:
- PostgreSQL `to_tsvector` / `to_tsquery` on title + description
- `pg_trgm` for fuzzy matching
- Support Arabic + English in same search
- [ ] Filter implementation (per doc 05):
- Niche (JSONB `@>` operator)
- Country (JSONB contains)
- Language (JSONB contains)
- Budget type / range
- Deliverable type
- Experience level
- Deadline range
- Posted date
- [ ] Sort options: newest, deadline_soon, highest_budget, most/fewest applicants
- [ ] Campaign cards component (search results)
- [ ] Views:
- `/creator/campaigns` — discover campaigns (search + filter page)
- `/campaigns/{slug}` — public detail (shared with stage 3.1)
- [ ] View counting (unique per user per day)
- [ ] Match score display (if creator is logged in — basic version: niche + language + country overlap)
**Deliverable:** Creators can search, filter, and browse campaigns. Match scores shown.
---
### Stage 3.3: Applications
**Tasks:**
- [ ] Create `applications` migration (from doc 15)
- [ ] Create `application_portfolio_items` migration
- [ ] Create `Application` model:
- Relationships: campaign, creator, portfolioItems, project
- Scopes: byStatus, byCampaign, byCreator
- [ ] `ApplicationStatus` enum with transitions
- [ ] `ApplicationService`:
```php
public function apply(CreatorProfile $creator, Campaign $campaign, array $data): Application
public function update(Application $application, array $data): Application
public function withdraw(Application $application): void
public function markViewed(Application $application): void
public function shortlist(Application $application): void
public function accept(Application $application): Project // creates project!
public function reject(Application $application, ?string $reason): void
public function bulkReject(Collection $applications, ?string $reason): void
```
- [ ] Validation rules:
- One application per creator per campaign (unique constraint)
- Cannot apply if profile < 50%
- Cannot apply after deadline
- Cannot apply to invite-only
- Cannot apply to own company's campaign
- [ ] `ApplyToCampaignRequest` — validation
- [ ] `ApplicationController` (creator side):
- `store()` — submit application
- `update()` — edit (before viewed)
- `withdraw()` — withdraw
- `index()` — list my applications
- [ ] `CompanyApplicationController` (company side):
- `index()` — list applications for a campaign (filterable, sortable)
- `show()` — application detail (with creator profile embedded)
- `shortlist()` / `accept()` / `reject()` / `bulkReject()`
- [ ] Events:
- `ApplicationSubmitted` → notify company
- `ApplicationViewed` → notify creator (in-app only)
- `ApplicationShortlisted` → notify creator
- `ApplicationAccepted` → notify creator + create project
- `ApplicationRejected` → notify creator
- [ ] Views:
- `/creator/applications` — my applications list
- `/campaigns/{slug}/apply` — application form
- `/company/campaigns/{id}/applications` — applications list for company
- `/company/applications/{id}` — single application detail
**Deliverable:** Full application flow works. Creator applies → company reviews → accepts (project created) or rejects.
---
### Stage 3.4: Invitations
**Tasks:**
- [ ] Create `invitations` migration (from doc 15)
- [ ] Create `Invitation` model:
- Relationships: company, creator, campaign (nullable), project
- Scopes: byStatus, pending, expired
- [ ] `InvitationStatus` enum with transitions
- [ ] `InvitationService`:
```php
public function send(CompanyProfile $company, CreatorProfile $creator, array $data): Invitation
public function cancel(Invitation $invitation): void
public function resend(Invitation $invitation): void
public function accept(Invitation $invitation): Project // creates project!
public function decline(Invitation $invitation, ?string $reason): void
public function expireOverdue(): int // scheduled command
```
- [ ] Validation rules:
- One active invitation per company-creator pair per campaign
- Max 50 invitations per company per day
- Cannot invite suspended/banned creators
- Max 2 resends
- [ ] `SendInvitationRequest` — validation
- [ ] `InvitationController` (company side):
- `store()` — send invitation
- `cancel()` — cancel
- `resend()` — resend
- `index()` — sent invitations list
- [ ] `CreatorInvitationController` (creator side):
- `index()` — received invitations
- `show()` — invitation detail
- `accept()` — accept
- `decline()` — decline
- [ ] Events:
- `InvitationSent` → notify creator
- `InvitationAccepted` → notify company + create project
- `InvitationDeclined` → notify company
- `InvitationExpired` → notify company
- [ ] Scheduled command: `invitation:expire-overdue` (run every 5 min)
- [ ] Views:
- `/company/invitations` — sent invitations
- `/creator/invitations` — received invitations
- `/creator/invitations/{id}` — invitation detail
**Deliverable:** Full invitation flow works. Company invites → creator accepts (project created) or declines.
---
### Stage 3.5: Creator Discovery (Company Searching)
**Tasks:**
- [ ] `CreatorDiscoveryController`:
- `index()` — search/filter creators
- `show()` — redirects to public profile
- [ ] Full-text search on creator profiles:
- display_name, username, bio
- pg_trgm for fuzzy matching
- [ ] Filters (per doc 14):
- Country, language, gender, age range, niche, skills
- Experience level, equipment, video style
- Availability, reputation, verification
- [ ] Sort: best match, newest, most experienced, highest rated, most projects
- [ ] Creator card component (search results)
- [ ] Saved creators (`saved_creators` table):
- Save / unsave
- Notes
- List management
- [ ] Materialized view for fast queries (or optimized query with proper indexing)
- [ ] Views:
- `/company/discover` — creator search page
- `/company/saved-creators` — saved list
- [ ] "Invite" button directly from search results (opens invitation form)
**Deliverable:** Companies can search, filter, save, and invite creators from a discovery page.
---
## PHASE 4: Project Workflow & Video
**Goal:** The complete project lifecycle works — from creation through deliverable submission, video review, revisions, and completion.
---
### Stage 4.1: Project Entity
**Tasks:**
- [ ] Create `projects` migration (from doc 15)
- [ ] Create `Project` model:
- HasUuid trait
- Relationships: company, creator, campaign, application, invitation, deliverables, conversations, reviews, activityLog
- Scopes: byStatus, active, overdue, byCompany, byCreator
- Accessors: isOverdue, daysUntilDeadline, progressPercentage
- [ ] `ProjectStatus` enum with full transition map (from doc 07)
- [ ] `ProjectService`:
```php
public function createFromApplication(Application $application): Project
public function createFromInvitation(Invitation $invitation): Project
public function start(Project $project): void
public function requestDeadlineExtension(Project $project, Date $newDeadline, string $reason): void
public function approveExtension(Project $project): void
public function rejectExtension(Project $project): void
public function cancel(Project $project, User $cancelledBy, string $reason): void
public function dispute(Project $project, User $disputedBy, string $reason): void
public function complete(Project $project): void
public function checkOverdue(): int // scheduled
```
- [ ] Status transition enforcement (throws `InvalidStatusTransitionException`)
- [ ] Project activity log (immutable timeline of all events)
- [ ] `ProjectController`:
- `index()` — list my projects (both roles, different views)
- `show()` — project dashboard
- `start()` — creator marks started
- `cancel()` — cancel with reason
- `dispute()` — escalate
- `extendDeadline()` — request/approve
- [ ] `ProjectPolicy` — only parties can access their project
- [ ] Views:
- `/creator/projects` — creator's project list
- `/company/projects` — company's project list
- `/projects/{uuid}` — project dashboard (tabs: overview, deliverables, activity, files, messages)
**Deliverable:** Projects created from applications/invitations. Dashboard shows status, timeline, details.
---
### Stage 4.2: Deliverables
**Tasks:**
- [ ] Create `deliverables` migration (from doc 15)
- [ ] Create `Deliverable` model:
- Relationships: project, submissions, revisionRequests
- Scopes: byStatus, pending, approved, overdue
- Accessors: isApproved, latestSubmission, currentVersion
- [ ] `DeliverableStatus` enum with transitions
- [ ] `DeliverableService`:
```php
public function createForProject(Project $project, array $deliverableSpecs): Collection
public function approve(Deliverable $deliverable, User $approver): void
public function requestRevision(Deliverable $deliverable, User $requester, array $data): RevisionRequest
public function checkAllApproved(Project $project): bool // triggers project completion
```
- [ ] Auto-create deliverables when project is created (from campaign specs)
- [ ] `DeliverableController`:
- `index()` — list for a project
- `show()` — deliverable detail with submissions
- `approve()` — company approves
- `requestRevision()` — company requests changes
- [ ] Events:
- `DeliverableApproved` → notify creator
- `AllDeliverablesApproved` → trigger project approval flow
- `RevisionRequested` → notify creator
- [ ] Views:
- Deliverable list within project dashboard
- Deliverable detail view (status, specs, submission history)
**Deliverable:** Deliverables exist in projects. Company can approve or request revisions.
---
### Stage 4.3: Submissions & Video Upload
**Tasks:**
- [ ] Create `submissions` migration
- [ ] Create `submission_files` migration
- [ ] Create `Submission` model:
- Relationships: deliverable, creator, files
- Version auto-increment per deliverable
- [ ] Create `SubmissionFile` model:
- PeerTube fields (uuid, embed_url, processing_status)
- Metadata (duration, dimensions, thumbnail)
- [ ] `SubmissionService`:
```php
public function submit(Deliverable $deliverable, CreatorProfile $creator, array $data): Submission
public function uploadFile(Submission $submission, UploadedFile $file): SubmissionFile
public function uploadVideo(Submission $submission, UploadedFile $file): SubmissionFile // PeerTube flow
```
- [ ] Video upload flow:
1. Creator selects file → frontend validates (size, type)
2. File uploaded to server temp storage
3. Server validates (mime check, duration check if possible)
4. `ProcessVideoUpload` job dispatched (queued)
5. Job uploads to PeerTube (resumable for large files)
6. Job polls PeerTube for processing status
7. On ready: update `submission_files` with peertube_uuid + embed_url
8. Notify creator: "Video ready"
9. Update deliverable status to "submitted"
- [ ] Non-video file upload (documents, images, audio):
- Direct to storage (no PeerTube)
- Immediate availability
- [ ] File validation per deliverable type (check specs)
- [ ] Upload progress tracking (frontend polling or WebSocket)
- [ ] `SubmissionController`:
- `store()` — create submission
- `uploadFile()` — attach file
- `show()` — view submission with files
- `versions()` — list all versions for a deliverable
- [ ] Events:
- `DeliverableSubmitted` → notify company
- [ ] Views:
- Upload form within deliverable view
- Progress indicator
- Video player (PeerTube embed) for viewing submissions
- Version switcher (v1, v2, v3 dropdown)
**Deliverable:** Creator can upload videos (via PeerTube) and files. Company sees them in review interface.
---
### Stage 4.4: Video Review System
**Tasks:**
- [ ] Create `revision_requests` migration
- [ ] Create `timestamp_comments` migration
- [ ] Create `RevisionRequest` model
- [ ] Create `TimestampComment` model:
- Relationships: submission, user
- Fields: timestamp_seconds, comment, type, is_resolved
- [ ] `VideoReviewService`:
```php
public function addTimestampComment(Submission $submission, User $user, array $data): TimestampComment
public function resolveComment(TimestampComment $comment): void
public function getCommentsForSubmission(Submission $submission): Collection
```
- [ ] `TimestampCommentController`:
- `store()` — add comment at timestamp
- `resolve()` — mark resolved
- `index()` — list for a submission
- [ ] Video review UI:
- PeerTube embed player
- Timeline with comment markers (colored dots)
- Click marker → jumps to timestamp + shows comment
- Click on timeline → add new comment at that point
- Side panel: list of all comments (sorted by timestamp)
- Resolved/unresolved toggle
- [ ] Revision request UI:
- Feedback text field
- Attach timestamp comments to the revision request
- Priority selector (minor/major/critical)
- [ ] Version comparison:
- Dropdown to switch between v1, v2, v3
- Show which comments are from which version
- "From previous version" label on old comments
**Deliverable:** Full video review with timestamp comments, revision flow, version history.
---
### Stage 4.5: Project Completion & Reviews
**Tasks:**
- [ ] Project completion flow:
1. All deliverables approved → project status = "approved"
2. 48-hour confirmation period (no disputes)
3. Project status = "completed"
4. Both parties prompted to leave reviews
- [ ] Create `reviews` migration (from doc 15)
- [ ] Create `Review` model:
- Relationships: project, reviewer (user), reviewee (user)
- Rating dimensions per role (per doc 11)
- [ ] `ReviewService`:
```php
public function create(Project $project, User $reviewer, array $data): Review
public function update(Review $review, array $data): Review // within 24h
public function makeVisible(Project $project): void // after both review or 14 days
```
- [ ] Review visibility logic:
- Hidden until BOTH parties review OR 14 days pass
- Then both become visible simultaneously
- [ ] `ReviewController`:
- `store()` — leave review
- `update()` — edit within 24h
- [ ] `ReviewResource` — API transformer
- [ ] Reputation recalculation trigger (after review created)
- [ ] Views:
- Review form (post-completion prompt)
- Reviews list on public profiles
- Review detail
**Deliverable:** Projects complete cleanly. Reviews are mutual and visible. Reputation updates.
---
## PHASE 5: Communication & Discovery
**Goal:** Real-time messaging, full notification system, and smart recommendations.
---
### Stage 5.1: Messaging System
**Tasks:**
- [ ] Create `conversations` migration
- [ ] Create `messages` migration
- [ ] Create `message_attachments` migration
- [ ] Create `Conversation` model:
- Relationships: company, creator, messages, project, campaign
- Scopes: forUser, active, withUnread
- Accessors: lastMessage, unreadCount (for specific user)
- [ ] Create `Message` model:
- Relationships: conversation, sender, attachments
- Scopes: unread
- [ ] `MessagingService`:
```php
public function startConversation(User $initiator, User $recipient, string $type, array $context): Conversation
public function sendMessage(Conversation $conversation, User $sender, array $data): Message
public function markConversationRead(Conversation $conversation, User $user): void
public function editMessage(Message $message, string $newBody): void // within 15 min
public function deleteMessage(Message $message): void // soft delete
public function getUnreadCount(User $user): int
```
- [ ] Auto-create project conversation on project creation
- [ ] System messages (auto-generated on project events)
- [ ] Message validation:
- Cannot message without context (application, invitation, project, or company-initiated)
- Rate limits
- Anti-spam: detect repeated messages, phone numbers in early messages
- [ ] File attachments in messages (images, docs, small videos)
- [ ] `ConversationController`:
- `index()` — inbox (list conversations)
- `show()` — single conversation with messages
- `store()` — start new conversation
- [ ] `MessageController`:
- `store()` — send message
- `update()` — edit
- `destroy()` — delete
- `markRead()` — mark conversation as read
- [ ] Real-time:
- **Phase 5 MVP:** polling every 5 seconds for new messages
- **Phase 5 upgrade:** Laravel Reverb WebSocket
- Private channel per conversation
- Typing indicators (WebSocket only)
- Online status
- [ ] Views:
- `/messages` — inbox (conversation list)
- `/messages/{uuid}` — conversation thread
- Unread badge in sidebar navigation
**Deliverable:** Full messaging works. Real-time updates (polling or WebSocket). Project threads auto-created.
---
### Stage 5.2: Notifications System
**Tasks:**
- [ ] Create `notifications` migration
- [ ] Create `notification_preferences` migration
- [ ] Create `Notification` model (or use Laravel's built-in notification system)
- [ ] `NotificationService`:
```php
public function send(User $user, string $type, array $data): void
public function sendBulk(Collection $users, string $type, array $data): void
public function markRead(Notification $notification): void
public function markAllRead(User $user): void
public function getUnreadCount(User $user): int
public function getUserPreferences(User $user): array
public function updatePreferences(User $user, array $prefs): void
```
- [ ] Implement every notification event from doc 12:
- Campaign events (published matching, updated, closed)
- Application events (received, viewed, shortlisted, accepted, rejected)
- Invitation events (received, accepted, declined, expiring)
- Project events (created, started, deadline reminders, completed, cancelled)
- Deliverable events (submitted, approved, revision requested)
- Message events (new message when offline)
- Review events (prompt, received)
- System events (verified, approved, suspended)
- [ ] Email notifications:
- Email template base (header, footer, CTA button)
- Arabic + English templates
- Queued sending (never block the request)
- Respect user preferences (which events get email)
- Batching: 5+ same-type in 10 min → single summary email
- Hard cap: max 10 emails/hour/user
- [ ] Notification preferences UI:
- Per-category toggle (in-app / email)
- Email digest option (instant, daily, weekly)
- [ ] Real-time notification delivery (WebSocket push to bell icon)
- [ ] Views:
- Bell icon with unread count (header)
- Notification dropdown (latest 10)
- `/notifications` — full notification list (paginated)
- `/settings/notifications` — preferences
**Deliverable:** All platform events trigger appropriate notifications. Users control preferences.
---
### Stage 5.3: Matching & Recommendations
**Tasks:**
- [ ] `MatchingService`:
```php
public function scoreCreatorForCampaign(CreatorProfile $creator, Campaign $campaign): int
public function scoreCampaignForCreator(Campaign $campaign, CreatorProfile $creator): int
public function getRecommendedCampaigns(CreatorProfile $creator, int $limit = 10): Collection
public function getRecommendedCreators(Campaign $campaign, int $limit = 20): Collection
public function getSimilarCreators(CreatorProfile $creator, int $limit = 5): Collection
```
- [ ] Match score algorithm (per doc 14):
- Niche overlap (25%)
- Language match (20%)
- Country match (15%)
- Skills match (15%)
- Experience level (10%)
- Equipment (5%)
- Availability (5%)
- Reputation bonus (5%)
- [ ] Creator dashboard: "Recommended Campaigns" widget
- [ ] Company campaign view: "Suggested Creators" panel
- [ ] Campaign discovery: "Best Match" sort option
- [ ] Creator discovery: "Best Match" sort option (when campaign context provided)
- [ ] Materialized view for fast creator discovery (refreshed every 15 min via cron)
- [ ] Weekly "New matches for you" email digest
**Deliverable:** Smart recommendations appear on dashboards. Search results sortable by match score.
---
### Stage 5.4: Reputation System
**Tasks:**
- [ ] `ReputationService`:
```php
public function calculateCreatorScore(CreatorProfile $creator): int
public function calculateCompanyScore(CompanyProfile $company): int
public function getScoreBreakdown(User $user): array
public function recalculateAll(): void // scheduled
```
- [ ] Creator score calculation (per doc 11):
- Review average (30%)
- Completion rate (25%)
- On-time delivery (15%)
- Response rate (10%)
- Response time (5%)
- Profile completeness (5%)
- Account age (5%)
- Revision rate (5%)
- [ ] Company score calculation
- [ ] Reputation tiers: New → Rising → Established → Top Rated → Elite
- [ ] Badges logic (company side)
- [ ] Scheduled recalculation (every 6 hours)
- [ ] Reputation decay for inactive users
- [ ] Display on profiles, search cards, application views
**Deliverable:** Reputation scores calculated, displayed, and influence search ranking.
---
## PHASE 6: Admin, Polish & Deploy
**Goal:** Complete admin panel, platform theming, performance optimization, and production deployment.
---
### Stage 6.1: Full Admin Panel
**Tasks:**
- [ ] `AdminCampaignController`:
- List all campaigns
- Feature/unfeature
- Pause/unpause any campaign
- View campaign analytics
- [ ] `AdminProjectController`:
- List all projects (with status filters, overdue highlight)
- Force status change
- Resolve disputes
- Add admin notes
- [ ] `AdminCreatorController`:
- Full creator management
- Verify (pro badge)
- Feature/unfeature
- View full activity
- [ ] Dispute resolution system:
- Dispute queue
- View full project history + messages
- Resolution options (side with creator/company/mutual/no fault/custom)
- Resolution notification to both parties
- [ ] Report management:
- Report queue (sorted by age)
- Report detail (context + reported entity)
- Resolution actions (valid, invalid, escalate)
- Track reporter credibility
- [ ] Analytics dashboard:
- User growth charts
- Campaign/project metrics
- Supply/demand ratio
- Retention funnel
- Geographic distribution
- Export to CSV
- [ ] Audit log viewer:
- All admin actions searchable
- Filter by admin, action type, date
- Immutable (no delete/edit)
- [ ] Admin notifications:
- Pending approvals badge
- Open disputes alert
- Flagged content alert
- System health indicators
**Deliverable:** Admin has full control over the platform. Disputes resolvable. Reports manageable.
---
### Stage 6.2: Platform Theming & CMS
**Tasks:**
- [ ] `PlatformSettingsService`:
```php
public function get(string $group, string $key, $default = null): mixed
public function set(string $group, string $key, $value): void
public function getGroup(string $group): array
public function setGroup(string $group, array $values): void
public function resetGroup(string $group): void // restore defaults
public function getHistory(string $group, int $limit = 10): Collection
```
- [ ] Theme editor UI (admin):
- Color picker for all tokens
- Font selector
- Layout options
- Live preview (iframe with temp CSS injection)
- Save draft / publish flow
- Preset selector
- Reset to default
- [ ] Landing page CMS:
- Section editor (hero, features, stats, testimonials, CTA)
- Drag-and-drop section ordering
- Section visibility toggle
- Image uploads for hero/sections
- [ ] Static page editor:
- Rich text editor (TinyMCE or similar)
- Create/edit/delete pages
- SEO fields (title, meta description)
- [ ] Navigation manager:
- Header links editor
- Footer links editor
- Sidebar item reordering
- [ ] Feature toggles UI:
- On/off switches for all toggleable features
- Maintenance mode (with custom message)
- [ ] Platform limits UI:
- All configurable limits with input fields
- [ ] Email template editor:
- Template list
- Variable placeholders shown
- Preview rendering
- Test email sender
**Deliverable:** Admin controls every visual aspect of the platform without code. CMS for static pages.
---
### Stage 6.3: Portfolios (Full)
**Tasks:**
- [ ] Create `portfolio_collections` migration
- [ ] Create `portfolio_items` migration
- [ ] `PortfolioService`:
```php
public function addItem(CreatorProfile $creator, array $data): PortfolioItem
public function uploadVideo(CreatorProfile $creator, UploadedFile $file, array $metadata): PortfolioItem
public function addExternalLink(CreatorProfile $creator, string $url, array $metadata): PortfolioItem
public function updateItem(PortfolioItem $item, array $data): PortfolioItem
public function deleteItem(PortfolioItem $item): void
public function reorder(CreatorProfile $creator, array $order): void
public function toggleFeatured(PortfolioItem $item): void // max 3
public function createCollection(CreatorProfile $creator, array $data): PortfolioCollection
```
- [ ] PeerTube integration for portfolio videos:
- Auto-create channel `portfolio_{username}` on first upload
- Privacy: unlisted
- [ ] External link parsing (YouTube, TikTok, Instagram oEmbed)
- [ ] Portfolio views on public profile
- [ ] Collection management
- [ ] Featured items (max 3, shown prominently)
- [ ] Views:
- `/creator/portfolio` — manage portfolio
- `/creator/portfolio/upload` — upload new video
- `/creators/{username}` — public profile with portfolio display
**Deliverable:** Full portfolio system with video uploads, external links, collections, and featured items.
---
### Stage 6.4: Reporting System
**Tasks:**
- [ ] Create `reports` migration
- [ ] `ReportService`:
```php
public function submit(User $reporter, string $targetType, int $targetId, array $data): Report
public function resolve(Report $report, User $admin, string $status, ?string $notes): void
```
- [ ] Reportable entities: users, campaigns, messages, reviews, portfolio items
- [ ] Report reasons per entity type (per doc 13)
- [ ] Rate limit: max 10 reports per day per user
- [ ] `ReportController`:
- `store()` — submit report
- `index()` — my submitted reports (user)
- [ ] Admin report management (built in Stage 6.1)
- [ ] Views:
- Report modal (triggered from "..." menu on any reportable entity)
- Report form (reason select + description)
**Deliverable:** Users can report content/users. Admin can manage and resolve reports.
---
### Stage 6.5: Performance & Security Hardening
**Tasks:**
- [ ] Database optimization:
- Analyze slow queries (enable pg_stat_statements)
- Add missing indexes
- Optimize N+1 queries (eager loading audit)
- Implement query caching where appropriate
- [ ] Redis caching layer:
- Cache platform settings (TTL 1h)
- Cache creator discovery results (TTL 15 min)
- Cache campaign listings (TTL 5 min, bust on new campaign)
- Cache user permissions (TTL 1h)
- Cache reputation scores (TTL 6h)
- [ ] Image optimization:
- Serve WebP with fallback
- Multiple sizes (thumbnail, medium, large)
- Lazy loading on all images
- CDN headers (if using S3/CloudFront)
- [ ] Security audit:
- OWASP top 10 check
- CSP headers configured
- CORS tightened to known origins
- Rate limiting verified on all endpoints
- SQL injection prevention verified (parameterized queries)
- XSS prevention verified (Blade auto-escaping)
- CSRF protection verified
- File upload validation (magic bytes, not just extension)
- Sensitive data exposure check (no passwords/tokens in logs)
- Admin action logging completeness
- [ ] API response optimization:
- Pagination enforced everywhere
- Sparse fieldsets (select only needed columns)
- Response compression (gzip)
- [ ] Queue optimization:
- Separate queues: `high` (notifications), `default` (emails), `low` (video processing, analytics)
- Failed job handling + retry logic
- Dead letter queue for permanently failed jobs
- [ ] Scheduled commands verification:
- All cron jobs from doc 19 registered
- Health check for each job (last run, success/fail)
**Deliverable:** Platform is fast, secure, and scalable for production traffic.
---
### Stage 6.6: Deployment & DevOps
**Tasks:**
- [ ] Dockerize the application:
```dockerfile
FROM php:8.2-fpm-alpine
# + nginx, supervisor, cron
```
- [ ] Docker Compose for local development:
- PHP-FPM
- Nginx
- PostgreSQL
- Redis
- PeerTube (or use remote)
- [ ] CapRover deployment:
- Create app: `ugcheaven`
- Configure environment variables
- Enable HTTPS
- Set up persistent volume for uploads
- Configure captain-definition file
- [ ] CI/CD pipeline (GitHub Actions or GitLab CI):
```yaml
stages:
- lint (PHP CS Fixer)
- test (PHPUnit)
- build (Docker image)
- deploy (to CapRover)
```
- [ ] Production environment:
- `APP_ENV=production`
- `APP_DEBUG=false`
- Cache config, routes, views: `php artisan optimize`
- Queue worker via supervisor
- Cron via container cron
- Log rotation
- Error tracking (Sentry or similar)
- [ ] Health check endpoint (`/api/health`)
- [ ] Database backup strategy:
- Daily automated backup (pg_dump)
- Store off-server (S3 or separate volume)
- Tested restore procedure
- [ ] Monitoring:
- Application logs
- Queue backlog
- Response times
- Error rates
- Disk usage
- PeerTube health
**Deliverable:** Platform deployed and running on CapRover with HTTPS, monitoring, and backups.
---
### Stage 6.7: Testing & QA
**Tasks:**
- [ ] Unit tests (Services):
- Every service method
- Status transitions (valid + invalid)
- Validation edge cases
- Calculation accuracy (match score, reputation)
- [ ] Feature tests (API + Web):
- Every endpoint (happy path + error cases)
- Permission boundaries
- Rate limiting
- Auth flows
- [ ] Integration tests:
- PeerTube upload flow (mocked in CI, real in staging)
- Email sending (mocked)
- Full user journeys
- [ ] Browser tests (Dusk or Cypress):
- Registration flow (both types)
- Campaign creation wizard
- Application flow
- Video upload + review
- Messaging
- [ ] Factories for all models:
- `UserFactory` with states: creator, company, admin, suspended, banned
- `CampaignFactory` with states: draft, published, closed
- `ProjectFactory` with states: all statuses
- etc.
- [ ] Seeders for demo data:
- 10 creators with complete profiles + portfolios
- 5 companies with campaigns
- Active projects in various states
- Sample messages, reviews
- [ ] Manual QA checklist (pre-launch):
- [ ] Creator registration → onboarding → profile completion
- [ ] Company registration → approval → campaign creation
- [ ] Application flow end-to-end
- [ ] Invitation flow end-to-end
- [ ] Video upload → review → revision → approval
- [ ] Project completion → review
- [ ] Messaging (both directions)
- [ ] Notifications (in-app + email)
- [ ] Admin: approve company, suspend user, resolve dispute
- [ ] RTL layout (Arabic)
- [ ] Mobile responsiveness
- [ ] Dark mode
- [ ] Theme changes (admin)
**Deliverable:** Comprehensive test suite. QA checklist passed. Ready for users.
---
## Dependency Graph (What Blocks What)
```
Stage 1.1 → everything
Stage 1.2 → everything (module structure)
Stage 1.3 → everything (tooling)
Stage 1.4 → Stage 2.* (migrations need base)
Stage 1.5 → Stage 4.3 (video upload needs PeerTube service)
Stage 1.6 → all views
Stage 2.1 → Stage 2.2, 2.3, 2.4 (auth needed for profiles)
Stage 2.2 → Stage 3.2, 3.3, 3.4, 3.5 (creator profile needed for apps/invites)
Stage 2.3 → Stage 3.1, 3.4, 3.5 (company profile needed for campaigns)
Stage 2.4 → Stage 6.1 (admin foundation needed for full admin)
Stage 3.1 → Stage 3.2, 3.3 (campaigns needed for discovery + applications)
Stage 3.3 → Stage 4.1 (applications create projects)
Stage 3.4 → Stage 4.1 (invitations create projects)
Stage 4.1 → Stage 4.2 (projects needed for deliverables)
Stage 4.2 → Stage 4.3 (deliverables needed for submissions)
Stage 4.3 → Stage 4.4 (submissions needed for review)
Stage 4.4 → Stage 4.5 (review completion needed for project completion)
Stage 5.1 → independent (can start after Stage 2.1)
Stage 5.2 → after Stage 2.1 (notifications need users)
Stage 5.3 → after Stage 2.2 + 3.1 (matching needs profiles + campaigns)
Stage 5.4 → after Stage 4.5 (reputation needs reviews)
Stage 6.* → after respective Phase dependencies
```
---
## Parallel Work Streams (For the Full Team)
### Developer A: Backend Core
- Phase 1 (all)
- Stage 2.1 (auth)
- Stage 3.1 (campaign CRUD)
- Stage 3.3 (applications)
- Stage 4.1 (projects)
- Stage 4.2 (deliverables)
- Stage 4.5 (reviews)
### Developer B: Backend Features
- Stage 2.2 (creator profile)
- Stage 2.3 (company profile)
- Stage 3.4 (invitations)
- Stage 4.3 (submissions + PeerTube)
- Stage 5.1 (messaging)
- Stage 5.2 (notifications)
- Stage 5.3 (matching)
### Developer C: Frontend
- Stage 1.6 (frontend foundation)
- All views across all stages (builds UI as backend is ready)
- Components library
- RTL support
- Responsive design
- Dark mode
### Developer D: Admin + DevOps
- Stage 2.4 (admin foundation)
- Stage 6.1 (full admin)
- Stage 6.2 (theming CMS)
- Stage 6.5 (performance)
- Stage 6.6 (deployment)
---
## Definition of Done Per Phase
### Phase 1 Complete When:
- `php artisan serve` works
- PeerTube test upload succeeds
- File upload works (image + document)
- Themed layout renders (with CSS variables)
- Arabic/English switch works
- API returns consistent JSON responses
### Phase 2 Complete When:
- Creator can: register → verify email → complete onboarding → have public profile
- Company can: register → see pending wall → admin approves → have public profile
- Admin can: approve/reject companies, suspend/ban users, impersonate
### Phase 3 Complete When:
- Company can: create campaign → publish → receive applications → accept creator
- Creator can: browse campaigns → search/filter → apply → see status
- Company can: search creators → invite → creator accepts
- Both paths create a project
### Phase 4 Complete When:
- Project lifecycle: not_started → in_progress → submitted → reviewed → approved → completed
- Video upload via PeerTube works end-to-end
- Timestamp comments on videos work
- Revision flow works (request → resubmit → re-review)
- Reviews work after project completion
### Phase 5 Complete When:
- Messaging works both directions
- All events trigger notifications (in-app + email)
- Match scores display in search results
- Recommendations appear on dashboards
- Reputation scores calculated and displayed
### Phase 6 Complete When:
- Admin panel fully operational
- Theme editor works (change colors → live on site)
- Platform deployed on CapRover with HTTPS
- Performance targets met (< 200ms TTFB)
- Security audit passed
- All tests passing
- Demo data seeded
- Manual QA checklist complete
# 23 — Design Rules (ABSOLUTE LAWS)
These rules are NON-NEGOTIABLE. Every developer, every component, every page must follow these without exception.
---
## RULE 1: EVERYTHING IS ANIMATED
Nothing on this platform is static. Every interaction, every state change, every appearance has motion.
### Page Transitions
- Route changes: content fades/slides in (200-300ms ease-out)
- No hard page jumps — use transition wrappers on all route content
### Element Entrances
| Element | Animation | Duration | Easing |
|---------|-----------|----------|--------|
| Page content | fade-up (opacity 0→1, translateY 10px→0) | 300ms | ease-out |
| Cards (grid) | staggered fade-up (50ms delay between each) | 300ms | ease-out |
| Sidebar items | staggered slide-in from left | 200ms | ease-out |
| Modals | scale(0.95→1) + fade | 200ms | ease-out |
| Drawers | slide from right (RTL: from left) | 250ms | ease-out |
| Dropdowns | scale-y(0.95→1) + fade from top | 150ms | ease-out |
| Toasts | slide-in from right + fade | 300ms | spring |
| Tooltips | fade + scale(0.9→1) | 150ms | ease-out |
### Element Exits
| Element | Animation | Duration |
|---------|-----------|----------|
| Modals | scale(1→0.95) + fade out | 150ms |
| Drawers | slide out | 200ms |
| Toasts | slide-out + fade | 200ms |
| Deleted items | fade out + collapse height | 300ms |
| Dropdowns | fade out | 100ms |
### State Changes
| State Change | Animation |
|-------------|-----------|
| Button hover | background-color transition (150ms) + subtle scale(1.02) |
| Button press | scale(0.97) (50ms) → release |
| Card hover | shadow elevation increase + translateY(-2px) (200ms) |
| Link hover | color transition (150ms) + underline slide-in |
| Input focus | border-color transition (200ms) + subtle glow |
| Toggle switch | slide with spring physics (300ms) |
| Checkbox check | scale bounce (0→1.2→1) + stroke-dashoffset animation |
| Radio select | scale bounce with ring expand |
| Tab switch | indicator slides to new position (250ms ease) |
| Accordion open | height auto-animate + rotate chevron |
| Status badge change | color morph transition (300ms) |
| Progress bar | width transition (500ms ease-in-out) |
| Counter update | number rolls/counts up (not instant change) |
| Skeleton → content | crossfade (skeleton fades, content fades in) |
### Scroll Animations
| Trigger | Animation |
|---------|-----------|
| Element enters viewport | fade-up (intersection observer, once) |
| Sticky header shrinks | height + padding transition (200ms) |
| Parallax backgrounds | subtle translateY on scroll (landing page only) |
| Infinite scroll load | new items stagger in |
### Loading & Feedback
| Context | Animation |
|---------|-----------|
| Skeleton loading | shimmer sweep (left to right, infinite, 1.5s) |
| Spinner | rotate 360 (infinite, 600ms linear) |
| Button loading | content fades out, spinner fades in |
| Upload progress | bar fills smoothly (not jumpy) |
| Pull to refresh | elastic overscroll |
| Success action | checkmark draws itself (stroke animation) |
| Error shake | horizontal shake (3 oscillations, 300ms) |
| Typing indicator | 3 dots pulsing in sequence |
| Notification badge | scale bounce on count change |
| Unread dot | gentle pulse (opacity 0.5→1, infinite) |
### Data Changes
| Change | Animation |
|--------|-----------|
| List item added | slide-in + fade from top |
| List item removed | slide-out + fade + height collapse |
| List reorder | items smoothly translate to new positions (FLIP animation) |
| Sort change | items cross-fade to new order |
| Filter applied | grid re-layout with smooth position transitions |
| Counter increment | number rolls up |
| Chart data change | bars/lines morph to new values |
| Avatar status (online→offline) | dot color morphs |
### Micro-interactions
| Interaction | Animation |
|------------|-----------|
| Copy to clipboard | brief checkmark flash |
| Favorite/save | heart fills with scale bounce |
| Star rating hover | stars scale slightly on hover |
| File drop zone | border dashes animate + zone pulses on hover |
| Drag handle | subtle lift shadow on grab |
| Image load | blur(10px)→blur(0) (progressive reveal) |
| Video thumbnail hover | subtle zoom (scale 1→1.05) |
### CSS Implementation
```css
/* Base transition for all interactive elements */
* {
transition-property: color, background-color, border-color,
box-shadow, opacity, transform;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
### Animation Library
Use one of:
- **Framer Motion** (if React/Inertia)
- **Alpine.js x-transition** (if Blade + Alpine)
- **@starting-style + CSS transitions** (modern CSS)
- **GSAP** (for complex sequences on landing page)
- **AutoAnimate** (for list animations)
### Performance Rules
- Use `transform` and `opacity` only for animations (GPU-accelerated)
- Never animate `width`, `height`, `top`, `left` directly — use transform
- `will-change` only on elements that are actively animating
- No animation duration longer than 500ms (except loading spinners)
- Debounce scroll-triggered animations
- Disable complex animations on mobile if FPS drops below 30
---
## RULE 2: ZERO EMOJIS — ICONS ONLY
**ABSOLUTELY NO EMOJIS ANYWHERE IN THE PLATFORM.** Not in the UI, not in notifications, not in status messages, not in empty states, not in emails, not in error messages. NOWHERE.
Every place that would typically use an emoji uses a **proper SVG icon** instead.
### Icon Library
Primary: **Lucide Icons** (open source, consistent, 1000+ icons)
- Consistent stroke width
- Scales perfectly
- Tree-shakeable (only bundle used icons)
- Works with both light and dark themes
Fallback/Supplement: **Heroicons** (by Tailwind team) for any gaps
### Icon Usage Map
Instead of emojis, use these icons:
| What You'd Use | WRONG (emoji) | CORRECT (Lucide icon) |
|---------------|---------------|----------------------|
| Success | ~~🎉~~ | `<x-icon name="check-circle" />` |
| New message | ~~📩~~ | `<x-icon name="mail" />` |
| Warning | ~~⚠️~~ | `<x-icon name="alert-triangle" />` |
| Money/budget | ~~💰~~ | `<x-icon name="banknote" />` |
| Video | ~~🎬~~ | `<x-icon name="video" />` |
| Calendar/deadline | ~~📅~~ | `<x-icon name="calendar" />` |
| Globe/worldwide | ~~🌍~~ | `<x-icon name="globe" />` |
| Star/rating | ~~⭐~~ | `<x-icon name="star" />` |
| Fire/trending | ~~🔥~~ | `<x-icon name="trending-up" />` |
| Heart/favorite | ~~❤️~~ | `<x-icon name="heart" />` |
| Verified | ~~✓~~ | `<x-icon name="badge-check" />` |
| Camera | ~~📷~~ | `<x-icon name="camera" />` |
| Location | ~~📍~~ | `<x-icon name="map-pin" />` |
| Time/clock | ~~⏰~~ | `<x-icon name="clock" />` |
| Attachment | ~~📎~~ | `<x-icon name="paperclip" />` |
| Search | ~~🔍~~ | `<x-icon name="search" />` |
| Settings | ~~⚙️~~ | `<x-icon name="settings" />` |
| User | ~~👤~~ | `<x-icon name="user" />` |
| Upload | ~~📤~~ | `<x-icon name="upload" />` |
| Download | ~~📥~~ | `<x-icon name="download" />` |
| Link | ~~🔗~~ | `<x-icon name="link" />` |
| Lock/security | ~~🔒~~ | `<x-icon name="lock" />` |
| Trophy/achievement | ~~🏆~~ | `<x-icon name="trophy" />` |
| Lightbulb/idea | ~~💡~~ | `<x-icon name="lightbulb" />` |
| Megaphone/announce | ~~📢~~ | `<x-icon name="megaphone" />` |
| Thumbs up/like | ~~👍~~ | `<x-icon name="thumbs-up" />` |
| Chat/message | ~~💬~~ | `<x-icon name="message-circle" />` |
| Eye/view | ~~👁️~~ | `<x-icon name="eye" />` |
| Edit/pencil | ~~✏️~~ | `<x-icon name="pencil" />` |
| Trash/delete | ~~🗑️~~ | `<x-icon name="trash-2" />` |
| Check/done | ~~✅~~ | `<x-icon name="check" />` |
| Cross/close | ~~❌~~ | `<x-icon name="x" />` |
| Arrow right | ~~➡️~~ | `<x-icon name="arrow-right" />` |
| Refresh | ~~🔄~~ | `<x-icon name="refresh-cw" />` |
| Bell/notification | ~~🔔~~ | `<x-icon name="bell" />` |
| Folder | ~~📁~~ | `<x-icon name="folder" />` |
| Image | ~~🖼️~~ | `<x-icon name="image" />` |
| Mic/audio | ~~🎤~~ | `<x-icon name="mic" />` |
| Play | ~~▶️~~ | `<x-icon name="play" />` |
| Pause | ~~⏸️~~ | `<x-icon name="pause" />` |
### Icon Component
```blade
{{-- resources/views/components/icon.blade.php --}}
@props(['name', 'size' => 'md', 'class' => ''])
@php
$sizes = [
'xs' => 'w-3 h-3',
'sm' => 'w-4 h-4',
'md' => 'w-5 h-5',
'lg' => 'w-6 h-6',
'xl' => 'w-8 h-8',
];
@endphp
<svg {{ $attributes->merge(['class' => $sizes[$size] . ' ' . $class]) }}>
<use href="/icons/sprite.svg#{{ $name }}" />
</svg>
```
### Where Icons Appear (NOT emojis)
- Navigation sidebar items
- Status badges (with colored dot, not emoji)
- Notification titles
- Empty states
- Button icons
- Form field icons (prefix/suffix)
- Breadcrumbs
- Card headers
- Stat cards
- Tab labels
- Action menus
- Alert/toast icons
- Email template icons (inline SVG)
- Error pages (404, 500)
### Status Indicators (Dot + Text, not emoji)
```html
<!-- WRONG -->
<span>🟢 Active</span>
<!-- CORRECT -->
<span class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
<span>Active</span>
</span>
```
### Notification Titles (Correcting doc 12)
```
WRONG: "🎉 You've been accepted for 'Campaign Title'"
CORRECT: "You've been accepted for 'Campaign Title'"
(icon shown separately as UI element, not inline text)
```
### Email Template Icons
- Use inline SVG in emails (not emoji, not icon fonts)
- Simple, single-color icons that work in all email clients
- Fallback: colored circle with letter (A, B, C) for ancient email clients
### Messaging: User-Generated Emoji
- Users typing messages CAN use emoji in their message text (it's their content)
- But the PLATFORM UI itself (buttons, labels, system messages, notifications) NEVER uses emoji
- System messages in chat use icon prefix, not emoji:
```
WRONG: [System] 🎬 Creator submitted "Video" v1
CORRECT: [System] Creator submitted "Video" v1
(with a proper icon rendered in the UI element, not in the text string)
```
---
## RULE ENFORCEMENT
### For Developers:
1. **Never hardcode emoji characters** in Blade templates, PHP strings, or JavaScript
2. **Always use `<x-icon>`** component for visual indicators
3. **Grep check before commit:** `grep -rn "[\x{1F300}-\x{1F9FF}]" resources/ app/` should return 0 results
4. **Animation audit:** every new component must have enter/exit/hover animations defined
### For Reviewers:
- Any PR containing emoji in UI strings = reject
- Any PR with a static element (no transition) = request animation
- Any PR animating layout properties (width/height/top/left) = request refactor to transform
### Automated Checks:
```bash
# Pre-commit hook: reject emoji in source code (excluding user content/tests)
#!/bin/bash
if grep -rPn '[\x{1F300}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}]' \
resources/views/ app/ lang/ --include="*.php" --include="*.blade.php" --include="*.js"; then
echo "ERROR: Emoji found in source code. Use <x-icon> instead."
exit 1
fi
```
# 24 — Localization & Internationalization (i18n)
## Overview
UGC Heaven is MENA-first. Arabic and English are equal citizens from day one. This is not "add Arabic later" — it's baked into every component, every layout, every template.
---
## Supported Languages (Launch)
| Code | Language | Direction | Status |
|------|----------|-----------|--------|
| en | English | LTR | Primary |
| ar | Arabic | RTL | Primary |
### Future Additions (Phase 2+):
- fr (French) — North Africa
- tr (Turkish) — Turkey market
- ur (Urdu) — Pakistan market
---
## Language Detection Priority
```
1. User preference (stored in users.language_preference)
2. Session override (user switched via toggle)
3. URL prefix (if using /en/ or /ar/ routing — optional)
4. Accept-Language header
5. Default: English
```
---
## URL Strategy
**Option chosen: No URL prefix.** Language stored in session/user preference.
- `/campaigns` → renders in user's language
- Language toggle in header switches session + saves to user profile
Reason: cleaner URLs, no duplicate content SEO issues, simpler routing.
---
## Translation File Structure
```
lang/
├── en/
│ ├── auth.php # Login, register, password reset
│ ├── validation.php # Form validation messages
│ ├── pagination.php # Pagination text
│ ├── common.php # Shared (buttons, labels, common words)
│ ├── navigation.php # Sidebar, header, footer labels
│ ├── creator.php # Creator-specific strings
│ ├── company.php # Company-specific strings
│ ├── campaign.php # Campaign-related strings
│ ├── application.php # Application strings
│ ├── invitation.php # Invitation strings
│ ├── project.php # Project strings
│ ├── deliverable.php # Deliverable strings
│ ├── messaging.php # Messaging strings
│ ├── notification.php # Notification titles/bodies
│ ├── review.php # Review strings
│ ├── admin.php # Admin panel strings
│ ├── landing.php # Landing page (default, admin can override)
│ ├── errors.php # Error pages (404, 500, etc.)
│ ├── email.php # Email template strings
│ └── enums.php # Translated enum values
├── ar/
│ ├── auth.php
│ ├── validation.php
│ ├── ... (mirror of en/)
│ └── enums.php
└── vendor/ # Package translations
```
---
## Translation Examples
### auth.php
```php
// en/auth.php
return [
'login' => 'Log In',
'register' => 'Create Account',
'logout' => 'Log Out',
'forgot_password' => 'Forgot Password?',
'reset_password' => 'Reset Password',
'email' => 'Email Address',
'password' => 'Password',
'confirm_password' => 'Confirm Password',
'remember_me' => 'Remember Me',
'login_failed' => 'These credentials do not match our records.',
'account_suspended' => 'Your account has been suspended. Reason: :reason',
'account_banned' => 'Your account has been permanently banned.',
'email_unverified' => 'Please verify your email address to continue.',
'register_as' => 'Register as',
'creator' => 'Creator',
'company' => 'Company',
'already_have_account' => 'Already have an account?',
'dont_have_account' => 'Don\'t have an account?',
];
// ar/auth.php
return [
'login' => 'تسجيل الدخول',
'register' => 'إنشاء حساب',
'logout' => 'تسجيل الخروج',
'forgot_password' => 'نسيت كلمة المرور؟',
'reset_password' => 'إعادة تعيين كلمة المرور',
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'confirm_password' => 'تأكيد كلمة المرور',
'remember_me' => 'تذكرني',
'login_failed' => 'بيانات الدخول غير صحيحة.',
'account_suspended' => 'تم إيقاف حسابك. السبب: :reason',
'account_banned' => 'تم حظر حسابك بشكل دائم.',
'email_unverified' => 'يرجى تأكيد بريدك الإلكتروني للمتابعة.',
'register_as' => 'التسجيل كـ',
'creator' => 'صانع محتوى',
'company' => 'شركة',
'already_have_account' => 'لديك حساب بالفعل؟',
'dont_have_account' => 'ليس لديك حساب؟',
];
```
### enums.php (Translated Select Options)
```php
// en/enums.php
return [
'campaign_status' => [
'draft' => 'Draft',
'published' => 'Published',
'paused' => 'Paused',
'closed' => 'Closed',
'in_progress' => 'In Progress',
'completed' => 'Completed',
'cancelled' => 'Cancelled',
],
'experience_level' => [
'beginner' => 'Beginner (0-1 years)',
'intermediate' => 'Intermediate (1-3 years)',
'advanced' => 'Advanced (3-5 years)',
'expert' => 'Expert (5+ years)',
],
'niches' => [
'gaming' => 'Gaming',
'technology' => 'Technology',
'fitness_health' => 'Fitness & Health',
'food_cooking' => 'Food & Cooking',
'beauty' => 'Beauty',
'fashion' => 'Fashion',
// ... all niches
],
'availability' => [
'available' => 'Available',
'busy' => 'Busy',
'vacation' => 'On Vacation',
'not_accepting' => 'Not Accepting Work',
],
];
// ar/enums.php
return [
'campaign_status' => [
'draft' => 'مسودة',
'published' => 'منشورة',
'paused' => 'متوقفة',
'closed' => 'مغلقة',
'in_progress' => 'قيد التنفيذ',
'completed' => 'مكتملة',
'cancelled' => 'ملغاة',
],
'experience_level' => [
'beginner' => 'مبتدئ (0-1 سنة)',
'intermediate' => 'متوسط (1-3 سنوات)',
'advanced' => 'متقدم (3-5 سنوات)',
'expert' => 'خبير (5+ سنوات)',
],
'niches' => [
'gaming' => 'ألعاب',
'technology' => 'تكنولوجيا',
'fitness_health' => 'لياقة وصحة',
'food_cooking' => 'طعام وطبخ',
'beauty' => 'جمال',
'fashion' => 'أزياء',
// ... all niches
],
'availability' => [
'available' => 'متاح',
'busy' => 'مشغول',
'vacation' => 'في إجازة',
'not_accepting' => 'لا أقبل عملاً حالياً',
],
];
```
---
## RTL Layout System
### HTML Direction
```blade
<html lang="{{ app()->getLocale() }}" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
```
### Tailwind RTL Configuration
```js
// tailwind.config.js
module.exports = {
content: [...],
theme: { extend: {} },
plugins: [
require('tailwindcss-rtl'), // adds rtl: and ltr: variants
],
}
```
### RTL Rules
**Everything flips:**
- Text alignment: left → right
- Sidebar: left side → right side
- Margins/paddings: `ml-4``mr-4` (use `ms-4` for margin-start)
- Flex/grid direction: row → row-reverse
- Icons that imply direction: arrows, chevrons → flip horizontally
- Breadcrumb separators: `>``<`
- Progress bars: fill from right
- Slide animations: from-right → from-left
**Does NOT flip:**
- Numbers (always LTR, even in RTL context)
- Phone numbers
- Code snippets
- Icons that don't imply direction (close X, play, plus, etc.)
- Brand logos
- Video player controls
- Chart axes
### CSS Logical Properties (preferred over physical)
```css
/* WRONG — breaks in RTL */
.card { margin-left: 1rem; padding-right: 2rem; text-align: left; }
/* CORRECT — works in both directions */
.card { margin-inline-start: 1rem; padding-inline-end: 2rem; text-align: start; }
```
### Tailwind Logical Classes
```html
<!-- WRONG -->
<div class="ml-4 pr-6 text-left">
<!-- CORRECT (using RTL plugin) -->
<div class="ms-4 pe-6 text-start">
<!-- Or conditional -->
<div class="ltr:ml-4 rtl:mr-4">
```
### Testing RTL
- Every page must be tested in both LTR and RTL
- Add `?lang=ar` query param for quick testing
- Browser dev tools: toggle `dir="rtl"` on html element
---
## Number & Date Formatting
### Numbers
```php
// Format number according to locale
number_format($amount, 2, '.', ',') // English: 1,234.56
// Arabic: ١٬٢٣٤٫٥٦ (optional — many MENA users prefer Western numerals)
// Decision: Use Western numerals (0-9) in both languages for clarity
// But format separators per locale
```
### Dates
```php
// English: March 15, 2024
// Arabic: ١٥ مارس ٢٠٢٤
// Use Carbon with locale
Carbon::parse($date)->locale(app()->getLocale())->isoFormat('D MMMM YYYY')
```
### Relative Time
```php
// English: "3 hours ago", "in 2 days"
// Arabic: "منذ 3 ساعات", "بعد يومين"
Carbon::parse($date)->locale(app()->getLocale())->diffForHumans()
```
### Currency
```php
// Currencies supported (MENA focus)
$currencies = ['USD', 'EGP', 'SAR', 'AED', 'KWD', 'QAR', 'BHD', 'OMR', 'JOD', 'EUR', 'GBP'];
// Display: currency code + amount (universal, no symbol confusion)
// English: "EGP 5,000" or "USD 500"
// Arabic: "5,000 ج.م" or "500 دولار"
```
---
## User-Generated Content (Bilingual)
### Profile Content (bio, descriptions)
- Stored as-is (single language per user)
- No auto-translation in MVP
- Search indexes both languages (pg_trgm handles mixed scripts)
### Campaign Content
- Title + description stored in one language (company's choice)
- Future: optional `title_ar` / `title_en` fields for bilingual campaigns
- Search works against the stored language
### System-Generated Content
- All system messages, notifications, emails → translated via lang files
- Notification body uses user's language preference
- Email sent in recipient's language preference
---
## Language Switcher Component
```blade
{{-- Header language toggle --}}
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="flex items-center gap-2">
<x-icon name="globe" size="sm" />
<span>{{ app()->getLocale() === 'en' ? 'EN' : 'عربي' }}</span>
<x-icon name="chevron-down" size="xs" />
</button>
<div x-show="open" x-transition class="absolute mt-2 ...">
<a href="{{ route('locale.switch', 'en') }}">English</a>
<a href="{{ route('locale.switch', 'ar') }}">العربية</a>
</div>
</div>
```
### Locale Switch Logic
```php
// Route: POST /locale/{locale}
public function switch(string $locale): RedirectResponse
{
abort_unless(in_array($locale, ['en', 'ar']), 400);
session(['locale' => $locale]);
if (auth()->check()) {
auth()->user()->update(['language_preference' => $locale]);
}
return back();
}
```
---
## Translation Workflow
### For Developers:
1. Write all user-facing strings using `__('file.key')` or `@lang('file.key')`
2. Never hardcode English strings in templates
3. Add key to `en/` file first
4. Mark for translation: add to `ar/` file with English placeholder if Arabic not ready
5. Use `:variable` syntax for dynamic content: `__('campaign.deadline_in', ['days' => $days])`
### For Translators:
1. All strings in `lang/ar/` directory
2. Placeholders (`:variable`) must remain unchanged
3. Pluralization uses Laravel plural syntax:
```php
// en
'applications_count' => '{0} No applications|{1} 1 application|[2,*] :count applications',
// ar
'applications_count' => '{0} لا توجد طلبات|{1} طلب واحد|{2} طلبان|[3,10] :count طلبات|[11,*] :count طلب',
```
(Arabic has complex plural rules dual, few, many)
### Missing Translation Handling:
```php
// config/app.php
'fallback_locale' => 'en',
// If Arabic key missing → fall back to English (never show raw key to users)
```
---
## Email Localization
### Template Structure:
```
emails/
├── en/
│ ├── welcome.blade.php
│ ├── verification.blade.php
│ ├── application-accepted.blade.php
│ └── ...
└── ar/
├── welcome.blade.php
├── verification.blade.php
├── application-accepted.blade.php
└── ...
```
### Arabic Email Rules:
- Direction: RTL (`dir="rtl"` on table/wrapper)
- Font: system Arabic font (Tahoma, Arial — safe for email)
- Alignment: right-aligned text
- CTA buttons: text right-aligned inside button
---
## Validation Messages
All validation errors translated:
```php
// en/validation.php
'required' => 'The :attribute field is required.',
'min.string' => 'The :attribute must be at least :min characters.',
'email' => 'The :attribute must be a valid email address.',
// ar/validation.php
'required' => 'حقل :attribute مطلوب.',
'min.string' => 'يجب أن يحتوي :attribute على :min حرف على الأقل.',
'email' => 'يجب أن يكون :attribute بريد إلكتروني صالح.',
// Attribute names translated too:
// en/validation.php → attributes
'attributes' => [
'email' => 'email address',
'password' => 'password',
'first_name' => 'first name',
'company_name' => 'company name',
],
// ar/validation.php → attributes
'attributes' => [
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'first_name' => 'الاسم الأول',
'company_name' => 'اسم الشركة',
],
```
---
## Edge Cases
1. **User switches language mid-form** → form data preserved, labels change, validation messages in new language
2. **Notification sent in Arabic, user switches to English** → old notifications stay in Arabic (rendered at send time)
3. **Mixed Arabic/English in one text field** → allowed, browser handles bidi automatically
4. **Arabic text in slug** → slugs always English/ASCII (transliterated or manual)
5. **Search in Arabic finds English results** → no (search is per-language against stored content)
6. **Admin edits landing page in English only** → Arabic users see English content (no blank page)
7. **Email sent before user sets language** → use fallback (English)
8. **Plural forms in Arabic (dual, few, many, other)** → use full ICU plural syntax
9. **Numbers in Arabic (Eastern vs Western)** → use Western numerals (0-9) for clarity across all locales
10. **RTL + LTR content in same paragraph** → browser bidi algorithm handles, wrap LTR content in `<bdi>` if needed
# 25 — Deployment (Zero-Touch CapRover + GitLab)
## How It Works
```
Developer pushes to main on GitLab
|
CapRover detects push (webhook or poll)
|
CapRover reads captain-definition → finds Dockerfile
|
Docker builds multi-stage image (composer → node → production)
|
entrypoint.sh runs: migrations, cache, seed
|
supervisord starts: PHP-FPM, Nginx, Queue Worker, Cron
|
App is live at https://ugcheaven.caprover.al-arcade.com
```
No SSH. No manual steps. No `.env` to upload. Just push and it's live.
---
## Infrastructure Map
```
┌─────────────────────────────────────────────────────┐
│ CapRover Server: 18.192.166.221 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ ugcheaven │ │ ugc-heaven-db (PostgreSQL) │ │
│ │ (this app) │→→│ srv-captain--ugc-heaven-db │ │
│ │ Port 80 │ │ Port 5432 (internal) │ │
│ └──────┬───────┘ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │→→→→→→→→→→│ ugc-heaven-redis (Redis) │ │
│ │ │ srv-captain--ugc-heaven-redis│ │
│ │ │ Port 6379 (internal) │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ └→→→→→→→→→→│ ugcvideoserver (PeerTube) │ │
│ │ srv-captain--ugcvideoserver │ │
│ │ HTTPS (external API) │ │
│ └─────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ gitlab (GitLab CE) │ │
│ │ gitlab.caprover.al-arcade.com │ │
│ │ Source code repository │ │
│ └──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
```
---
## Configuration Philosophy: HARDCODED. PERIOD.
### No .env. No CapRover env vars. No environment variable injection.
All production values are written DIRECTLY into Laravel config files. The app reads nothing from the environment at runtime. Push code → CapRover builds → it runs with zero external configuration.
For local development only: a `.env` file (gitignored) can override via `env()` fallback pattern. But in production, the values in the config files are THE truth.
### What Config Files Look Like:
```php
// config/database.php — VALUES ARE DIRECT. NOT env() calls with defaults.
'pgsql' => [
'driver' => 'pgsql',
'host' => 'srv-captain--ugc-heaven-db',
'port' => '5432',
'database' => 'ugc_heaven',
'username' => 'ugcadmin',
'password' => 'UgcH3aven2024!',
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],
// config/app.php
'name' => 'UGC Heaven',
'env' => 'production',
'debug' => false,
'url' => 'https://ugcheaven.caprover.al-arcade.com',
'key' => 'base64:GENERATED_AT_INIT',
// config/cache.php
'default' => 'redis',
// config/session.php
'driver' => 'redis',
// config/queue.php
'default' => 'redis',
// config/mail.php
'default' => 'smtp',
'mailers' => ['smtp' => [
'host' => 'srv-captain--poste-io',
'port' => 587,
'encryption' => 'tls',
'username' => 'noreply@al-arcade.com',
'password' => 'Alarcade123#',
]],
'from' => ['address' => 'noreply@al-arcade.com', 'name' => 'UGC Heaven'],
```
### Production Defaults Registry (Complete):
| Config Key | Production Default |
|------------|-------------------|
| APP_NAME | UGC Heaven |
| APP_ENV | production |
| APP_DEBUG | false |
| APP_URL | https://ugcheaven.caprover.al-arcade.com |
| APP_LOCALE | en |
| APP_SUPPORTED_LOCALES | en,ar |
| DB_CONNECTION | pgsql |
| DB_HOST | srv-captain--ugc-heaven-db |
| DB_PORT | 5432 |
| DB_DATABASE | ugc_heaven |
| DB_USERNAME | ugcadmin |
| DB_PASSWORD | UgcH3aven2024! |
| REDIS_HOST | srv-captain--ugc-heaven-redis |
| REDIS_PORT | 6379 |
| REDIS_PASSWORD | null |
| CACHE_STORE | redis |
| SESSION_DRIVER | redis |
| SESSION_LIFETIME | 120 |
| QUEUE_CONNECTION | redis |
| BROADCAST_CONNECTION | reverb |
| FILESYSTEM_DISK | local |
| PEERTUBE_URL | https://ugcvideoserver.caprover.al-arcade.com |
| PEERTUBE_CLIENT_ID | b148jgk5ffimzszm8cqa4fut2o5u2jf7 |
| PEERTUBE_CLIENT_SECRET | sRXMboZI4941vn1cs3GSJIhtyCAVli32 |
| PEERTUBE_ADMIN_USERNAME | root |
| PEERTUBE_ADMIN_PASSWORD | Alarcade123# |
| MAIL_MAILER | smtp |
| MAIL_HOST | srv-captain--poste-io |
| MAIL_PORT | 587 |
| MAIL_ENCRYPTION | tls |
| MAIL_USERNAME | noreply@al-arcade.com |
| MAIL_PASSWORD | Alarcade123# |
| MAIL_FROM_ADDRESS | noreply@al-arcade.com |
| MAIL_FROM_NAME | UGC Heaven |
| LOG_CHANNEL | single |
---
## Redis Service (Must Create on CapRover)
Before first deploy, create a Redis app on CapRover:
```bash
# SSH to server
ssh -i /Users/mahmoudaglan/NewMigration/newServer.pem -o StrictHostKeyChecking=no ubuntu@18.192.166.221
# Get CapRover auth token
TOKEN=$(curl -s http://localhost:3000/api/v2/login \
-H 'Content-Type: application/json' \
-H 'x-namespace: captain' \
-d '{"password":"YOUR_CAPROVER_PASSWORD"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
# Create Redis app
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/register \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{"appName":"ugc-heaven-redis","hasPersistentData":true}'
# Deploy Redis image
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/update \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{
"appName": "ugc-heaven-redis",
"instanceCount": 1,
"captainDefinitionContent": "{\"schemaVersion\":2,\"imageName\":\"redis:7-alpine\"}"
}'
```
Internal hostname: `srv-captain--ugc-heaven-redis` (port 6379, no auth needed on internal network)
---
## CapRover App Setup
### Create the app:
```bash
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/register \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{"appName":"ugcheaven","hasPersistentData":true}'
```
### Enable HTTPS:
```bash
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/enablebasedomainssl \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{"appName":"ugcheaven"}'
```
### Add persistent storage (for uploaded files):
```bash
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/update \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{
"appName": "ugcheaven",
"volumes": [
{"containerPath": "/var/www/html/storage/app", "volumeName": "ugcheaven-storage"}
]
}'
```
### Link to GitLab repo:
In CapRover dashboard → App → Deployment → Method 3: Deploy from GitLab
- Repo: `https://gitlab.caprover.al-arcade.com/root/ugc-heaven-web-php.git`
- Branch: `main`
- Username: `root`
- Password: GitLab token
Or via API:
```bash
curl -s http://localhost:3000/api/v2/user/apps/appDefinitions/update \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d '{
"appName": "ugcheaven",
"appDeployTokenConfig": {
"enabled": true
}
}'
```
---
## Persistent Storage
| Path in Container | What's Stored | Persistence |
|-------------------|---------------|-------------|
| /var/www/html/storage/app | Uploaded files (images, docs) | Docker volume (survives redeploy) |
| /var/www/html/storage/logs | Application logs | Ephemeral (lost on redeploy, that's OK) |
| Database | All data | PostgreSQL container volume |
| Redis | Sessions, cache, queues | Redis container (ephemeral is OK — sessions recreate) |
| PeerTube | All videos | PeerTube's own volume |
---
## Deploy Checklist (First Time Only)
- [ ] Redis service created on CapRover
- [ ] PostgreSQL has `ugc_heaven` database ready (already done per ugc-heaven-db.md)
- [ ] CapRover app `ugcheaven` created
- [ ] HTTPS enabled
- [ ] Persistent storage volume attached
- [ ] GitLab repo linked to CapRover app
- [ ] First push triggers build + deploy
- [ ] Verify: `curl https://ugcheaven.caprover.al-arcade.com/api/health`
- [ ] Verify: admin can log in
### After first deploy, every subsequent push to `main` auto-deploys. Zero intervention.
---
## Rollback
If a deploy breaks:
1. CapRover keeps previous image versions
2. In CapRover dashboard → App → Deployment → select previous build → deploy
3. Or: `git revert` + push to main → auto-deploys the fix
---
## Monitoring
### Health check endpoint: `GET /api/health`
```json
{
"status": "healthy",
"services": {
"database": "up",
"redis": "up",
"peertube": "up",
"storage": "up"
},
"version": "1.0.0",
"timestamp": "2024-03-15T10:30:00Z"
}
```
### Logs:
```bash
# View app logs
ssh -i /Users/mahmoudaglan/NewMigration/newServer.pem ubuntu@18.192.166.221 \
"sudo docker logs \$(sudo docker ps -q --filter name=srv-captain--ugcheaven) --tail 100"
# View queue worker logs (same container, separate process)
# Logs go to stdout → Docker captures them
```
### Common Issues:
| Symptom | Cause | Fix |
|---------|-------|-----|
| 502 Bad Gateway | Container crashed | Check Docker logs, fix code, push |
| DB connection refused | Redis/Postgres not on same network | Verify CapRover overlay network |
| Migrations fail | Bad migration SQL | Fix migration, push again |
| Storage permission denied | Volume mount ownership | entrypoint.sh fixes this (chown) |
| Queue not processing | Supervisor crashed | Container restart (CapRover auto-restarts) |
# 26 — Visual Design Direction
## Style: Modern Gradient (Framer/Raycast Aesthetic)
The platform looks like a premium, cutting-edge product — not a generic bootstrap admin panel. Think Framer's dashboard meets Raycast's polish meets a MENA luxury brand.
---
## Visual DNA
### Core Characteristics:
- **Subtle gradients** on surfaces (not flat, not skeuomorphic — somewhere between)
- **Glass/frosted effects** on overlays, modals, and floating elements (backdrop-blur)
- **Bold but controlled** — confident color use without being garish
- **Depth via shadows and layers** — cards float, modals have depth, nothing is flat
- **Micro-texture** — subtle noise or grain on backgrounds (barely visible, adds richness)
- **Generous whitespace** — breathing room between elements
- **Smooth curves** — rounded corners (12-16px), pill buttons, soft shapes
- **Dark accents** — dark header/sidebar with gradient highlights
---
## Color System
### Light Mode:
```css
:root {
/* Backgrounds */
--color-background: #FAFBFC;
--color-surface: #FFFFFF;
--color-surface-hover: #F7F8FA;
--color-surface-elevated: #FFFFFF; /* cards, modals */
/* Primary gradient (hero, buttons, active states) */
--color-primary: #6366F1; /* indigo-500 base */
--color-primary-hover: #4F46E5; /* indigo-600 */
--color-primary-light: #EEF2FF; /* indigo-50 */
--gradient-primary: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%); /* indigo → violet */
/* Secondary */
--color-secondary: #06B6D4; /* cyan-500 */
--gradient-secondary: linear-gradient(135deg, #06B6D4 0%, #0EA5E9 100%); /* cyan → sky */
/* Accent (premium, success, highlights) */
--color-accent: #F59E0B; /* amber */
--gradient-accent: linear-gradient(135deg, #F59E0B 0%, #EF4444 100%); /* amber → rose (warm) */
/* Text */
--color-text-primary: #0F172A; /* slate-900 */
--color-text-secondary: #64748B; /* slate-500 */
--color-text-tertiary: #94A3B8; /* slate-400 */
--color-text-on-primary: #FFFFFF;
/* Borders */
--color-border: #E2E8F0; /* slate-200 */
--color-border-hover: #CBD5E1; /* slate-300 */
/* Sidebar (dark) */
--color-sidebar-bg: #0F172A; /* slate-900 */
--color-sidebar-bg-gradient: linear-gradient(180deg, #1E293B 0%, #0F172A 100%);
--color-sidebar-text: #94A3B8;
--color-sidebar-text-active: #FFFFFF;
--color-sidebar-item-active: rgba(99, 102, 241, 0.15); /* primary with opacity */
--color-sidebar-item-hover: rgba(255, 255, 255, 0.05);
/* Status */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* Glass effect */
--glass-bg: rgba(255, 255, 255, 0.7);
--glass-blur: blur(12px);
--glass-border: rgba(255, 255, 255, 0.2);
}
```
### Dark Mode:
```css
[data-theme="dark"] {
--color-background: #0B0F1A;
--color-surface: #141925;
--color-surface-hover: #1C2333;
--color-surface-elevated: #1E2538;
--gradient-primary: linear-gradient(135deg, #818CF8 0%, #A78BFA 100%);
--color-text-primary: #F1F5F9;
--color-text-secondary: #94A3B8;
--color-text-tertiary: #64748B;
--color-border: #1E293B;
--color-border-hover: #334155;
--color-sidebar-bg: #070A12;
--color-sidebar-bg-gradient: linear-gradient(180deg, #0F1420 0%, #070A12 100%);
--glass-bg: rgba(20, 25, 37, 0.8);
--glass-border: rgba(255, 255, 255, 0.08);
}
```
---
## Typography
| Use | Font | Weight | Size |
|-----|------|--------|------|
| Headings (EN) | Plus Jakarta Sans | 700 | scale |
| Body (EN) | Inter | 400, 500, 600 | 15px base |
| Headings (AR) | Tajawal | 700 | scale |
| Body (AR) | Tajawal | 400, 500, 600 | 15px base |
| Mono/code | JetBrains Mono | 400 | 13px |
### Type Scale:
```
xs: 12px / 1.4
sm: 13px / 1.4
base: 15px / 1.5
lg: 17px / 1.5
xl: 20px / 1.4
2xl: 24px / 1.3
3xl: 30px / 1.2
4xl: 40px / 1.1
5xl: 52px / 1.0
```
---
## Key UI Patterns
### Cards:
```css
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
transition: all 200ms ease;
}
.card:hover {
border-color: var(--color-border-hover);
box-shadow: 0 4px 12px rgba(0,0,0,0.06), 0 12px 28px rgba(0,0,0,0.04);
transform: translateY(-2px);
}
```
### Buttons (Primary):
```css
.btn-primary {
background: var(--gradient-primary);
color: white;
border-radius: 12px;
padding: 10px 20px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
transition: all 200ms ease;
}
.btn-primary:hover {
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0) scale(0.98);
}
```
### Glass Panels (Modals, Dropdowns):
```css
.glass-panel {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
```
### Sidebar:
```css
.sidebar {
background: var(--color-sidebar-bg-gradient);
border-right: 1px solid rgba(255, 255, 255, 0.06);
width: 260px;
}
.sidebar-item {
border-radius: 10px;
padding: 10px 14px;
color: var(--color-sidebar-text);
transition: all 150ms ease;
}
.sidebar-item:hover {
background: var(--color-sidebar-item-hover);
color: var(--color-sidebar-text-active);
}
.sidebar-item.active {
background: var(--color-sidebar-item-active);
color: var(--color-sidebar-text-active);
/* subtle glow on active */
box-shadow: inset 0 0 0 1px rgba(99, 102, 241, 0.2);
}
```
### Input Fields:
```css
.input {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 10px 14px;
font-size: 15px;
transition: all 200ms ease;
}
.input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
outline: none;
}
```
### Status Badges:
```css
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success { background: rgba(16, 185, 129, 0.1); color: #059669; }
.badge-warning { background: rgba(245, 158, 11, 0.1); color: #D97706; }
.badge-error { background: rgba(239, 68, 68, 0.1); color: #DC2626; }
.badge-info { background: rgba(59, 130, 246, 0.1); color: #2563EB; }
```
### Stat Cards (Dashboard):
```css
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
padding: 24px;
position: relative;
overflow: hidden;
}
.stat-card::before {
/* subtle gradient glow in corner */
content: '';
position: absolute;
top: -20px;
right: -20px;
width: 80px;
height: 80px;
background: var(--gradient-primary);
opacity: 0.08;
border-radius: 50%;
filter: blur(20px);
}
```
---
## Gradient Usage Rules
| Where | Gradient |
|-------|----------|
| Primary buttons | primary gradient (indigo → violet) |
| Active sidebar item indicator (left bar) | primary gradient (vertical) |
| Hero section background | dark + gradient mesh |
| CTA banners | primary gradient + subtle pattern |
| Stat card decorative element | primary gradient (low opacity, blurred) |
| Selected/active tab underline | primary gradient |
| Progress bars | primary gradient fill |
| Premium badges | accent gradient (amber → rose) |
| Skeleton loading shimmer | surface → lighter → surface sweep |
| Landing page orbs (decorative) | multi-color gradient blobs with blur |
### Gradient DON'Ts:
- Never gradient on body text
- Never gradient on borders (use solid or transparent)
- Never gradient that clashes with readability
- Never more than 2 gradient surfaces visible simultaneously (too busy)
- Never rainbow/multicolor gradients (tacky)
---
## Landing Page Aesthetic
```
Dark background (#0B0F1A)
+ floating gradient orbs (blurred, 30% opacity)
+ subtle dot grid pattern (barely visible)
+ glass-panel hero card
+ bold white headline + gradient text accent
+ animated gradient border on CTA button
+ mockup screenshots with glass frames + shadow
+ testimonial cards with glass effect
+ stats section with counting animations
```
---
## Dashboard Aesthetic
```
Light background (#FAFBFC)
+ dark gradient sidebar (left)
+ clean white header with glass blur on scroll
+ cards with subtle shadows (float on hover)
+ gradient primary buttons
+ status badges (colored dots, not emoji)
+ smooth transitions between all states
+ skeleton loaders with shimmer while loading
```
---
## Iconography Style
Lucide icons with these modifications:
- Stroke width: 1.75 (slightly thinner than default 2 for elegance)
- Size in sidebar: 20px
- Size in buttons: 18px
- Size in badges/inline: 14-16px
- Color: inherit from parent (follows text color)
---
## Motion Personality
The animation style should feel:
- **Confident** — not bouncy or playful (this isn't a game)
- **Smooth** — cubic-bezier easing, no linear
- **Quick** — 150-250ms for most things (snappy, not sluggish)
- **Subtle** — movement in pixels (2-4px), not dramatic slides
- **Layered** — stagger delays create depth (50ms between list items)
Easing presets:
```css
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* default for enter */
--ease-in: cubic-bezier(0.7, 0, 0.84, 0); /* for exits */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* for bouncy emphasis */
```
<?php
return [
'failed' => 'بيانات الاعتماد هذه لا تتطابق مع سجلاتنا.',
'password' => 'كلمة المرور المقدمة غير صحيحة.',
'throttle' => 'عدد كبير جدًا من محاولات تسجيل الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
'unauthorized_role' => 'ليس لديك صلاحية للوصول إلى هذا المورد.',
'login' => 'تسجيل الدخول',
'register' => 'تسجيل',
'logout' => 'تسجيل الخروج',
'email' => 'البريد الإلكتروني',
'password_label' => 'كلمة المرور',
'remember_me' => 'تذكرني',
'forgot_password' => 'نسيت كلمة المرور؟',
'reset_password' => 'إعادة تعيين كلمة المرور',
'confirm_password' => 'تأكيد كلمة المرور',
'already_registered' => 'لديك حساب بالفعل؟',
'verify_email' => 'تحقق من بريدك الإلكتروني',
'verification_sent' => 'تم إرسال رابط التحقق إلى بريدك الإلكتروني.',
'check_email' => 'قبل المتابعة، يرجى التحقق من بريدك الإلكتروني.',
'not_receive' => 'إذا لم تستلم البريد الإلكتروني',
'request_another' => 'اضغط هنا لطلب رابط آخر',
];
<?php
return [
'not_approved' => 'حساب شركتك قيد المراجعة.',
];
<?php
return [
'incomplete' => 'يرجى إكمال ملفك الشخصي للوصول إلى هذه الميزة.',
];
<?php
return [
'skip_to_content' => 'انتقل إلى المحتوى',
'loading' => 'جاري التحميل...',
'save' => 'حفظ',
'cancel' => 'إلغاء',
'delete' => 'حذف',
'edit' => 'تعديل',
'create' => 'إنشاء',
'search' => 'بحث',
'filter' => 'تصفية',
'sort' => 'ترتيب',
'back' => 'رجوع',
'next' => 'التالي',
'previous' => 'السابق',
'confirm' => 'تأكيد',
'close' => 'إغلاق',
'view' => 'عرض',
'download' => 'تحميل',
'upload' => 'رفع',
'submit' => 'إرسال',
'reset' => 'إعادة تعيين',
'no_results' => 'لا توجد نتائج',
'showing' => 'عرض :from إلى :to من :total نتيجة',
];
<?php
return [
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'unauthorized_role' => 'You do not have permission to access this resource.',
'login' => 'Login',
'register' => 'Register',
'logout' => 'Logout',
'email' => 'Email',
'password_label' => 'Password',
'remember_me' => 'Remember me',
'forgot_password' => 'Forgot your password?',
'reset_password' => 'Reset Password',
'confirm_password' => 'Confirm Password',
'already_registered' => 'Already registered?',
'verify_email' => 'Verify your email address',
'verification_sent' => 'A verification link has been sent to your email address.',
'check_email' => 'Before proceeding, please check your email for a verification link.',
'not_receive' => 'If you did not receive the email',
'request_another' => 'click here to request another',
];
<?php
return [
'not_approved' => 'Your company account is pending approval.',
];
<?php
return [
'incomplete' => 'Please complete your profile to access this feature.',
];
<?php
return [
'skip_to_content' => 'Skip to content',
'loading' => 'Loading...',
'save' => 'Save',
'cancel' => 'Cancel',
'delete' => 'Delete',
'edit' => 'Edit',
'create' => 'Create',
'search' => 'Search',
'filter' => 'Filter',
'sort' => 'Sort',
'back' => 'Back',
'next' => 'Next',
'previous' => 'Previous',
'confirm' => 'Confirm',
'close' => 'Close',
'view' => 'View',
'download' => 'Download',
'upload' => 'Upload',
'submit' => 'Submit',
'reset' => 'Reset',
'no_results' => 'No results found',
'showing' => 'Showing :from to :to of :total results',
];
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"vite": "^6.0.11"
},
"dependencies": {
"alpinejs": "^3.14.0",
"lucide": "^0.460.0"
}
}
# PeerTube (UGC Video Server) — API Reference
## Instance Details
| Key | Value |
|-----|-------|
| **Web UI** | `https://ugcvideoserver.caprover.al-arcade.com/` |
| **API Base URL** | `https://ugcvideoserver.caprover.al-arcade.com/api/v1/` |
| **Version** | PeerTube 8.2.1 |
| **Admin Email** | `info@al-arcade.com` |
| **Admin Username** | `root` |
| **Admin Password** | `Alarcade123#` |
| **Docker Service** | `srv-captain--ugcvideoserver` |
| **Internal Hostname** | `srv-captain--ugcvideoserver` |
| **DB Service** | `srv-captain--ugcvideoserver-db` |
| **Redis Service** | `srv-captain--ugcvideoserver-redis` |
### Database Credentials
| Key | Value |
|-----|-------|
| **DB User** | `peertubeuser` |
| **DB Password** | `36164471fa34ee3b705aee7f8b98c016` |
| **DB Host** | `srv-captain--ugcvideoserver-db` |
---
## Authentication
PeerTube uses OAuth2. All authenticated requests need a Bearer token.
### Step 1: Get OAuth Client Credentials (one-time)
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/oauth-clients/local
```
**Response:**
```json
{
"client_id": "b148jgk5ffimzszm8cqa4fut2o5u2jf7",
"client_secret": "sRXMboZI4941vn1cs3GSJIhtyCAVli32"
}
```
### Step 2: Get Access Token
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/token \
-d "client_id=b148jgk5ffimzszm8cqa4fut2o5u2jf7" \
-d "client_secret=sRXMboZI4941vn1cs3GSJIhtyCAVli32" \
-d "grant_type=password" \
-d "username=root" \
-d "password=Alarcade123#"
```
**Response:**
```json
{
"token_type": "Bearer",
"access_token": "<TOKEN>",
"refresh_token": "<REFRESH_TOKEN>",
"expires_in": 86399,
"refresh_token_expires_in": 2419199
}
```
### Step 3: Use Token in Requests
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/me \
-H "Authorization: Bearer <TOKEN>"
```
### Refresh Token
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/token \
-d "client_id=b148jgk5ffimzszm8cqa4fut2o5u2jf7" \
-d "client_secret=sRXMboZI4941vn1cs3GSJIhtyCAVli32" \
-d "grant_type=refresh_token" \
-d "refresh_token=<REFRESH_TOKEN>"
```
---
## Quick Auth (Copy-Paste)
```bash
# Get token in one command
TOKEN=$(curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/token \
-d "client_id=b148jgk5ffimzszm8cqa4fut2o5u2jf7" \
-d "client_secret=sRXMboZI4941vn1cs3GSJIhtyCAVli32" \
-d "grant_type=password" \
-d "username=root" \
-d "password=Alarcade123#" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
echo $TOKEN
```
---
## Videos
### Upload a Video
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/upload \
-H "Authorization: Bearer $TOKEN" \
-F "videofile=@/path/to/video.mp4" \
-F "channelId=1" \
-F "name=My Video Title" \
-F "privacy=1"
```
**Privacy values:** `1` = Public, `2` = Unlisted, `3` = Private, `4` = Internal
### Upload via Resumable (Large Files)
```bash
# 1. Initialize upload
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/upload-resumable \
-H "Authorization: Bearer $TOKEN" \
-H "X-Upload-Content-Type: video/mp4" \
-H "X-Upload-Content-Length: <FILE_SIZE_BYTES>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Large Video",
"channelId": 1,
"privacy": 1
}'
# Response header: Location: <upload_url>
# 2. Upload chunks
curl -s -X PUT "<upload_url>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Range: bytes 0-<CHUNK_END>/<TOTAL_SIZE>" \
--data-binary @chunk_file
```
### List Videos
```bash
# All public videos
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos?count=25&sort=-publishedAt"
# My videos (authenticated)
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/me/videos?count=25" \
-H "Authorization: Bearer $TOKEN"
# Search videos
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/search/videos?search=gameplay&count=25"
```
### Get Video Details
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<id_or_uuid>
```
### Update Video
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<id> \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Title",
"description": "New description",
"privacy": 1,
"tags": ["gameplay", "multiplayer"],
"category": 15
}'
```
### Delete Video
```bash
curl -s -X DELETE https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<id> \
-H "Authorization: Bearer $TOKEN"
```
### Update Video Thumbnail
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<id> \
-H "Authorization: Bearer $TOKEN" \
-F "thumbnailfile=@/path/to/thumb.jpg"
```
---
## Video Channels
### List Channels
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-channels?count=25"
```
### Create Channel
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-channels \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "gameplay_channel",
"displayName": "Gameplay Videos",
"description": "All our game recordings and trailers"
}'
```
### Update Channel
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-channels/gameplay_channel \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Official Gameplay",
"description": "Updated description"
}'
```
### Delete Channel
```bash
curl -s -X DELETE https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-channels/gameplay_channel \
-H "Authorization: Bearer $TOKEN"
```
---
## Video Playlists
### Create Playlist
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-playlists \
-H "Authorization: Bearer $TOKEN" \
-F "displayName=Season 1 Trailers" \
-F "privacy=1" \
-F "videoChannelId=1"
```
### Add Video to Playlist
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-playlists/<playlist_id>/videos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"videoId": <video_id>}'
```
### List Playlists
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-playlists?count=25"
```
### Reorder Playlist
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/video-playlists/<playlist_id>/videos/reorder \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"startPosition": 1, "insertAfterPosition": 3}'
```
---
## Users
### Create User
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "player1",
"password": "SecurePass123!",
"email": "player1@al-arcade.com",
"role": 2,
"videoQuota": -1,
"videoQuotaDaily": -1,
"channelName": "player1_channel"
}'
```
**Roles:** `0` = Admin, `1` = Moderator, `2` = User
### List Users
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/users?count=25" \
-H "Authorization: Bearer $TOKEN"
```
### Delete User
```bash
curl -s -X DELETE https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/<user_id> \
-H "Authorization: Bearer $TOKEN"
```
### Get My Account
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/users/me \
-H "Authorization: Bearer $TOKEN"
```
---
## Comments
### List Comments on a Video
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/comment-threads?count=25"
```
### Post a Comment
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/comment-threads \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Great video!"}'
```
### Reply to a Comment
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/comments/<comment_id> \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Thanks for watching!"}'
```
### Delete Comment
```bash
curl -s -X DELETE https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/comments/<comment_id> \
-H "Authorization: Bearer $TOKEN"
```
---
## Video Likes/Dislikes
### Like a Video
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/rate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"rating": "like"}'
```
**Rating values:** `like`, `dislike`, `none`
---
## Live Streaming
### Create a Live
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/live \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Live Gameplay Stream",
"channelId": 1,
"privacy": 1,
"saveReplay": true,
"permanentLive": false
}'
```
**Response includes `rtmpUrl` and `streamKey` for OBS/streaming software.**
### Get Live Info
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/live/<video_id> \
-H "Authorization: Bearer $TOKEN"
```
---
## Video Captions/Subtitles
### Add Caption
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/captions/en \
-H "Authorization: Bearer $TOKEN" \
-F "captionfile=@/path/to/captions.vtt"
```
### List Captions
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/captions
```
### Delete Caption
```bash
curl -s -X DELETE https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/captions/en \
-H "Authorization: Bearer $TOKEN"
```
---
## Server Administration
### Get Server Config
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/config
```
### Update Custom Config (Admin)
```bash
curl -s -X PUT https://ugcvideoserver.caprover.al-arcade.com/api/v1/config/custom \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instance": {"name": "AL-Arcade UGC"},
"signup": {"enabled": true, "limit": 100},
"import": {"videos": {"http": {"enabled": true}}},
"live": {"enabled": true}
}'
```
### Get Custom Config
```bash
curl -s https://ugcvideoserver.caprover.al-arcade.com/api/v1/config/custom \
-H "Authorization: Bearer $TOKEN"
```
---
## Video Import (from URL)
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/imports \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"targetUrl": "https://www.youtube.com/watch?v=VIDEO_ID",
"channelId": 1,
"name": "Imported Video",
"privacy": 1
}'
```
*Note: Video import needs to be enabled first via config/custom.*
---
## Abuse Reports (Moderation)
### List Reports
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/abuses?count=25" \
-H "Authorization: Bearer $TOKEN"
```
### Report a Video
```bash
curl -s -X POST https://ugcvideoserver.caprover.al-arcade.com/api/v1/abuses \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"video": {"id": <video_id>},
"reason": "Inappropriate content"
}'
```
---
## Video Statistics
### Get Video Stats
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/stats/overall" \
-H "Authorization: Bearer $TOKEN"
```
### Get Retention Stats
```bash
curl -s "https://ugcvideoserver.caprover.al-arcade.com/api/v1/videos/<video_id>/stats/retention" \
-H "Authorization: Bearer $TOKEN"
```
---
## Embedding Videos
```html
<iframe
width="560"
height="315"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
frameborder="0"
src="https://ugcvideoserver.caprover.al-arcade.com/videos/embed/<video_uuid>"
allowfullscreen
></iframe>
```
---
## Common Query Parameters
| Parameter | Description |
|-----------|-------------|
| `count` | Items per page (max 100) |
| `start` | Offset for pagination |
| `sort` | Sort field (prefix `-` for desc). Common: `-publishedAt`, `-views`, `-likes` |
| `search` | Search string |
| `categoryOneOf` | Filter by category ID |
| `tagsOneOf` | Filter by tags |
| `languageOneOf` | Filter by language |
| `privacyOneOf` | Filter by privacy (1,2,3,4) |
---
## Video Categories
| ID | Name |
|----|------|
| 1 | Music |
| 2 | Films |
| 3 | Vehicles |
| 4 | Art |
| 5 | Sports |
| 6 | Travels |
| 7 | Gaming |
| 8 | People |
| 9 | Comedy |
| 10 | Entertainment |
| 11 | News & Politics |
| 12 | How To |
| 13 | Education |
| 15 | Science & Technology |
| 16 | Animals |
| 17 | Kids |
| 18 | Food |
---
## Full Python Example
```python
import requests
BASE = "https://ugcvideoserver.caprover.al-arcade.com/api/v1"
CLIENT_ID = "b148jgk5ffimzszm8cqa4fut2o5u2jf7"
CLIENT_SECRET = "sRXMboZI4941vn1cs3GSJIhtyCAVli32"
# 1. Get token
token_resp = requests.post(f"{BASE}/users/token", data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "password",
"username": "root",
"password": "Alarcade123#"
}).json()
TOKEN = token_resp["access_token"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# 2. Create a channel
requests.post(f"{BASE}/video-channels", headers=HEADERS, json={
"name": "game_trailers",
"displayName": "Game Trailers",
"description": "Official game trailers and teasers"
})
# 3. Upload a video
with open("trailer.mp4", "rb") as f:
resp = requests.post(f"{BASE}/videos/upload", headers=HEADERS, files={
"videofile": ("trailer.mp4", f, "video/mp4")
}, data={
"channelId": 1,
"name": "Hospital Game - Official Trailer",
"description": "First look at our multiplayer hospital game",
"privacy": 1,
"tags": ["trailer", "multiplayer", "hospital"],
"category": 15
})
video = resp.json()["video"]
print(f"Uploaded: {video['uuid']}")
print(f"Watch: https://ugcvideoserver.caprover.al-arcade.com/w/{video['uuid']}")
print(f"Embed: https://ugcvideoserver.caprover.al-arcade.com/videos/embed/{video['uuid']}")
# 4. List all videos
videos = requests.get(f"{BASE}/videos", params={"count": 25, "sort": "-publishedAt"}).json()
for v in videos["data"]:
print(f" {v['name']} - {v['views']} views")
```
---
## SSH Access
```bash
# SSH to server
ssh -i /Users/mahmoudaglan/NewMigration/newServer.pem -o StrictHostKeyChecking=no ubuntu@18.192.166.221
# View PeerTube logs
sudo docker logs $(sudo docker ps -q --filter "name=srv-captain--ugcvideoserver.1") --tail 50
# Restart PeerTube
sudo docker service update --force srv-captain--ugcvideoserver
# Access PeerTube DB
sudo docker exec -it $(sudo docker ps -q --filter "name=srv-captain--ugcvideoserver-db") psql -U peertubeuser -d peertube
```
---
## Important Notes
- OAuth tokens expire in 24 hours. Use `refresh_token` to get a new one.
- Video upload max file size depends on server config (currently default).
- Supported formats: `.mp4`, `.webm`, `.ogv`, `.mkv`, `.mov`, `.avi`, `.flv`, `.mp3`, `.wav`, `.flac`, `.aac` and more.
- Live streaming must be enabled via admin config first.
- Video import (from YouTube/URLs) must be enabled via admin config first.
- The embed URL pattern is: `https://ugcvideoserver.caprover.al-arcade.com/videos/embed/<uuid>`
- The watch URL pattern is: `https://ugcvideoserver.caprover.al-arcade.com/w/<uuid>`
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
<testsuite name="Modules">
<directory>app/Modules/*/Tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="pgsql"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
</phpunit>
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
<?php
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../bootstrap/app.php')
->handleRequest(Request::capture());
User-agent: *
Disallow:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--color-background: #FAFBFC;
--color-surface: #FFFFFF;
--color-surface-hover: #F7F8FA;
--color-surface-elevated: #FFFFFF;
--color-primary: #6366F1;
--color-primary-hover: #4F46E5;
--color-primary-light: #EEF2FF;
--gradient-primary: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
--color-secondary: #06B6D4;
--gradient-secondary: linear-gradient(135deg, #06B6D4 0%, #0EA5E9 100%);
--color-accent: #F59E0B;
--gradient-accent: linear-gradient(135deg, #F59E0B 0%, #EF4444 100%);
--color-text-primary: #0F172A;
--color-text-secondary: #64748B;
--color-text-tertiary: #94A3B8;
--color-text-on-primary: #FFFFFF;
--color-border: #E2E8F0;
--color-border-hover: #CBD5E1;
--color-sidebar-bg: #0F172A;
--color-sidebar-bg-gradient: linear-gradient(180deg, #1E293B 0%, #0F172A 100%);
--color-sidebar-text: #94A3B8;
--color-sidebar-text-active: #FFFFFF;
--color-sidebar-item-active: rgba(99, 102, 241, 0.15);
--color-sidebar-item-hover: rgba(255, 255, 255, 0.05);
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
--glass-bg: rgba(255, 255, 255, 0.7);
--glass-blur: blur(12px);
--glass-border: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] {
--color-background: #0B0F1A;
--color-surface: #141925;
--color-surface-hover: #1C2333;
--color-surface-elevated: #1E2538;
--gradient-primary: linear-gradient(135deg, #818CF8 0%, #A78BFA 100%);
--color-text-primary: #F1F5F9;
--color-text-secondary: #94A3B8;
--color-text-tertiary: #64748B;
--color-border: #1E293B;
--color-border-hover: #334155;
--color-sidebar-bg: #070A12;
--color-sidebar-bg-gradient: linear-gradient(180deg, #0F1420 0%, #070A12 100%);
--glass-bg: rgba(20, 25, 37, 0.8);
--glass-border: rgba(255, 255, 255, 0.08);
}
* {
border-color: var(--color-border);
}
body {
background-color: var(--color-background);
color: var(--color-text-primary);
font-family: 'Inter', sans-serif;
}
[dir="rtl"] body {
font-family: 'Tajawal', sans-serif;
}
}
@layer components {
.btn-primary {
background: var(--gradient-primary);
color: white;
border-radius: 12px;
padding: 10px 20px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-primary:hover {
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0) scale(0.98);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.card:hover {
border-color: var(--color-border-hover);
box-shadow: 0 4px 12px rgba(0,0,0,0.06), 0 12px 28px rgba(0,0,0,0.04);
transform: translateY(-2px);
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.sidebar {
background: var(--color-sidebar-bg-gradient);
border-inline-end: 1px solid rgba(255, 255, 255, 0.06);
width: 260px;
}
.sidebar-item {
border-radius: 10px;
padding: 10px 14px;
color: var(--color-sidebar-text);
transition: all 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
.sidebar-item:hover {
background: var(--color-sidebar-item-hover);
color: var(--color-sidebar-text-active);
}
.sidebar-item.active {
background: var(--color-sidebar-item-active);
color: var(--color-sidebar-text-active);
box-shadow: inset 0 0 0 1px rgba(99, 102, 241, 0.2);
}
.input-field {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 10px 14px;
font-size: 15px;
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.input-field:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
outline: none;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success { background: rgba(16, 185, 129, 0.1); color: #059669; }
.badge-warning { background: rgba(245, 158, 11, 0.1); color: #D97706; }
.badge-error { background: rgba(239, 68, 68, 0.1); color: #DC2626; }
.badge-info { background: rgba(59, 130, 246, 0.1); color: #2563EB; }
}
@layer utilities {
.animate-stagger > * {
animation: fadeUp 300ms var(--ease-out) forwards;
opacity: 0;
}
.animate-stagger > *:nth-child(1) { animation-delay: 0ms; }
.animate-stagger > *:nth-child(2) { animation-delay: 50ms; }
.animate-stagger > *:nth-child(3) { animation-delay: 100ms; }
.animate-stagger > *:nth-child(4) { animation-delay: 150ms; }
.animate-stagger > *:nth-child(5) { animation-delay: 200ms; }
.animate-stagger > *:nth-child(6) { animation-delay: 250ms; }
.animate-stagger > *:nth-child(7) { animation-delay: 300ms; }
.animate-stagger > *:nth-child(8) { animation-delay: 350ms; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
import Alpine from 'alpinejs';
import { createIcons, icons } from 'lucide';
window.Alpine = Alpine;
Alpine.start();
createIcons({ icons });
document.addEventListener('livewire:navigated', () => {
createIcons({ icons });
});
@props([
'variant' => 'primary',
'size' => 'md',
'icon' => null,
'iconPosition' => 'start',
'href' => null,
'type' => 'button',
])
@php
$variants = [
'primary' => 'btn-primary',
'secondary' => 'bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-primary)] hover:border-[var(--color-border-hover)] hover:shadow-md',
'ghost' => 'bg-transparent hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)]',
'danger' => 'bg-[var(--color-error)] text-white hover:bg-red-600',
];
$sizes = [
'sm' => 'px-3 py-1.5 text-sm',
'md' => 'px-5 py-2.5 text-base',
'lg' => 'px-7 py-3 text-lg',
];
$classes = ($variants[$variant] ?? $variants['primary']) . ' ' . ($sizes[$size] ?? $sizes['md']);
$classes .= ' inline-flex items-center justify-center gap-2 font-semibold rounded-button transition-all duration-200';
@endphp
@if($href)
<a href="{{ $href }}" {{ $attributes->merge(['class' => $classes]) }}>
@if($icon && $iconPosition === 'start')
<x-ui.icon :name="$icon" size="sm" />
@endif
{{ $slot }}
@if($icon && $iconPosition === 'end')
<x-ui.icon :name="$icon" size="sm" />
@endif
</a>
@else
<button type="{{ $type }}" {{ $attributes->merge(['class' => $classes]) }}>
@if($icon && $iconPosition === 'start')
<x-ui.icon :name="$icon" size="sm" />
@endif
{{ $slot }}
@if($icon && $iconPosition === 'end')
<x-ui.icon :name="$icon" size="sm" />
@endif
</button>
@endif
@props([
'name',
'size' => 'md',
'class' => '',
])
@php
$sizes = [
'xs' => 'w-3 h-3',
'sm' => 'w-4 h-4',
'md' => 'w-5 h-5',
'lg' => 'w-6 h-6',
'xl' => 'w-8 h-8',
];
$sizeClass = $sizes[$size] ?? $sizes['md'];
@endphp
<i data-lucide="{{ $name }}" class="{{ $sizeClass }} {{ $class }}" style="stroke-width: 1.75"></i>
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}" data-theme="{{ session('theme', 'light') }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name') }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&family=Tajawal:wght@400;500;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="antialiased min-h-screen" x-data="{ sidebarOpen: false }">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-indigo-600">
{{ __('ui.skip_to_content') }}
</a>
{{ $slot }}
@livewireScripts
</body>
</html>
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Styles / Scripts -->
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/css/app.css', 'resources/js/app.js'])
@else
<style>
/* ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com */*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.-bottom-16{bottom:-4rem}.-left-16{left:-4rem}.-left-20{left:-5rem}.top-0{top:0}.z-0{z-index:0}.\!row-span-1{grid-row:span 1 / span 1!important}.-mx-3{margin-left:-.75rem;margin-right:-.75rem}.-ml-px{margin-left:-1px}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.\!hidden{display:none!important}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.size-12{width:3rem;height:3rem}.size-5{width:1.25rem;height:1.25rem}.size-6{width:1.5rem;height:1.5rem}.h-12{height:3rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-5{width:1.25rem}.w-\[calc\(100\%_\+_8rem\)\]{width:calc(100% + 8rem)}.w-auto{width:auto}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-\[877px\]{max-width:877px}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\!flex-row{flex-direction:row!important}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.bg-\[\#FF2D20\]\/10{background-color:#ff2d201a}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-white{--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #fff var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.to-zinc-900{--tw-gradient-to: #18181b var(--tw-gradient-to-position)}.stroke-\[\#FF2D20\]{stroke:#ff2d20}.object-cover{-o-object-fit:cover;object-fit:cover}.object-top{-o-object-position:top;object-position:top}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pt-3{padding-top:.75rem}.text-center{text-align:center}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-sm{font-size:.875rem;line-height:1.25rem}.text-sm\/relaxed{font-size:.875rem;line-height:1.625}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-5{line-height:1.25rem}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-black\/50{color:#00000080}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-\[0px_14px_34px_0px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow: 0px 14px 34px 0px rgba(0,0,0,.08);--tw-shadow-colored: 0px 14px 34px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity, 1))}.ring-gray-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity, 1))}.ring-transparent{--tw-ring-color: transparent}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity, 1))}.ring-white\/\[0\.05\]{--tw-ring-color: rgb(255 255 255 / .05)}.drop-shadow-\[0px_4px_34px_rgba\(0\,0\,0\,0\.06\)\]{--tw-drop-shadow: drop-shadow(0px 4px 34px rgba(0,0,0,.06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-\[0px_4px_34px_rgba\(0\,0\,0\,0\.25\)\]{--tw-drop-shadow: drop-shadow(0px 4px 34px rgba(0,0,0,.25));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.selection\:bg-\[\#FF2D20\] *::-moz-selection{--tw-bg-opacity: 1;background-color:rgb(255 45 32 / var(--tw-bg-opacity, 1))}.selection\:bg-\[\#FF2D20\] *::selection{--tw-bg-opacity: 1;background-color:rgb(255 45 32 / var(--tw-bg-opacity, 1))}.selection\:text-white *::-moz-selection{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.selection\:text-white *::selection{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.selection\:bg-\[\#FF2D20\]::-moz-selection{--tw-bg-opacity: 1;background-color:rgb(255 45 32 / var(--tw-bg-opacity, 1))}.selection\:bg-\[\#FF2D20\]::selection{--tw-bg-opacity: 1;background-color:rgb(255 45 32 / var(--tw-bg-opacity, 1))}.selection\:text-white::-moz-selection{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.selection\:text-white::selection{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-black:hover{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.hover\:text-black\/70:hover{color:#000000b3}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:ring-black\/20:hover{--tw-ring-color: rgb(0 0 0 / .2)}.focus\:z-10:focus{z-index:10}.focus\:border-blue-300:focus{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-\[\#FF2D20\]:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 45 32 / var(--tw-ring-opacity, 1))}.active\:bg-gray-100:active{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.active\:text-gray-500:active{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.active\:text-gray-700:active{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:size-16{width:4rem;height:4rem}.sm\:size-6{width:1.5rem;height:1.5rem}.sm\:flex-1{flex:1 1 0%}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:pt-5{padding-top:1.25rem}}@media (min-width: 768px){.md\:row-span-3{grid-row:span 3 / span 3}}@media (min-width: 1024px){.lg\:col-start-2{grid-column-start:2}.lg\:h-16{height:4rem}.lg\:max-w-7xl{max-width:80rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:items-end{align-items:flex-end}.lg\:justify-center{justify-content:center}.lg\:gap-8{gap:2rem}.lg\:p-10{padding:2.5rem}.lg\:pb-10{padding-bottom:2.5rem}.lg\:pt-0{padding-top:0}.lg\:text-\[\#FF2D20\]{--tw-text-opacity: 1;color:rgb(255 45 32 / var(--tw-text-opacity, 1))}}.rtl\:flex-row-reverse:where([dir=rtl],[dir=rtl] *){flex-direction:row-reverse}@media (prefers-color-scheme: dark){.dark\:block{display:block}.dark\:hidden{display:none}.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.dark\:via-zinc-900{--tw-gradient-to: rgb(24 24 27 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #18181b var(--tw-gradient-via-position), var(--tw-gradient-to)}.dark\:to-zinc-900{--tw-gradient-to: #18181b var(--tw-gradient-to-position)}.dark\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:text-white\/50{color:#ffffff80}.dark\:text-white\/70{color:#ffffffb3}.dark\:ring-zinc-800{--tw-ring-opacity: 1;--tw-ring-color: rgb(39 39 42 / var(--tw-ring-opacity, 1))}.dark\:hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white\/70:hover{color:#ffffffb3}.dark\:hover\:text-white\/80:hover{color:#fffc}.dark\:hover\:ring-zinc-700:hover{--tw-ring-opacity: 1;--tw-ring-color: rgb(63 63 70 / var(--tw-ring-opacity, 1))}.dark\:focus\:border-blue-700:focus{--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity, 1))}.dark\:focus\:border-blue-800:focus{--tw-border-opacity: 1;border-color:rgb(30 64 175 / var(--tw-border-opacity, 1))}.dark\:focus-visible\:ring-\[\#FF2D20\]:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 45 32 / var(--tw-ring-opacity, 1))}.dark\:focus-visible\:ring-white:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity, 1))}.dark\:active\:bg-gray-700:active{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:active\:text-gray-300:active{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}}
</style>
@endif
</head>
<body class="font-sans antialiased dark:bg-black dark:text-white/50">
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
<img id="background" class="absolute -left-20 top-0 max-w-[877px]" src="https://laravel.com/assets/img/welcome/background.svg" alt="Laravel background" />
<div class="relative min-h-screen flex flex-col items-center justify-center selection:bg-[#FF2D20] selection:text-white">
<div class="relative w-full max-w-2xl px-6 lg:max-w-7xl">
<header class="grid grid-cols-2 items-center gap-2 py-10 lg:grid-cols-3">
<div class="flex lg:justify-center lg:col-start-2">
<svg class="h-12 w-auto text-white lg:h-16 lg:text-[#FF2D20]" viewBox="0 0 62 65" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M61.8548 14.6253C61.8778 14.7102 61.8895 14.7978 61.8897 14.8858V28.5615C61.8898 28.737 61.8434 28.9095 61.7554 29.0614C61.6675 29.2132 61.5409 29.3392 61.3887 29.4265L49.9104 36.0351V49.1337C49.9104 49.4902 49.7209 49.8192 49.4118 49.9987L25.4519 63.7916C25.3971 63.8227 25.3372 63.8427 25.2774 63.8639C25.255 63.8714 25.2338 63.8851 25.2101 63.8913C25.0426 63.9354 24.8666 63.9354 24.6991 63.8913C24.6716 63.8838 24.6467 63.8689 24.6205 63.8589C24.5657 63.8389 24.5084 63.8215 24.456 63.7916L0.501061 49.9987C0.348882 49.9113 0.222437 49.7853 0.134469 49.6334C0.0465019 49.4816 0.000120578 49.3092 0 49.1337L0 8.10652C0 8.01678 0.0124642 7.92953 0.0348998 7.84477C0.0423783 7.8161 0.0598282 7.78993 0.0697995 7.76126C0.0884958 7.70891 0.105946 7.65531 0.133367 7.6067C0.152063 7.5743 0.179485 7.54812 0.20192 7.51821C0.230588 7.47832 0.256763 7.43719 0.290416 7.40229C0.319084 7.37362 0.356476 7.35243 0.388883 7.32751C0.425029 7.29759 0.457436 7.26518 0.498568 7.2415L12.4779 0.345059C12.6296 0.257786 12.8015 0.211853 12.9765 0.211853C13.1515 0.211853 13.3234 0.257786 13.475 0.345059L25.4531 7.2415H25.4556C25.4955 7.26643 25.5292 7.29759 25.5653 7.32626C25.5977 7.35119 25.6339 7.37362 25.6625 7.40104C25.6974 7.43719 25.7224 7.47832 25.7523 7.51821C25.7735 7.54812 25.8021 7.5743 25.8196 7.6067C25.8483 7.65656 25.8645 7.70891 25.8844 7.76126C25.8944 7.78993 25.9118 7.8161 25.9193 7.84602C25.9423 7.93096 25.954 8.01853 25.9542 8.10652V33.7317L35.9355 27.9844V14.8846C35.9355 14.7973 35.948 14.7088 35.9704 14.6253C35.9792 14.5954 35.9954 14.5692 36.0053 14.5405C36.0253 14.4882 36.0427 14.4346 36.0702 14.386C36.0888 14.3536 36.1163 14.3274 36.1375 14.2975C36.1674 14.2576 36.1923 14.2165 36.2272 14.1816C36.2559 14.1529 36.292 14.1317 36.3244 14.1068C36.3618 14.0769 36.3942 14.0445 36.4341 14.0208L48.4147 7.12434C48.5663 7.03694 48.7383 6.99094 48.9133 6.99094C49.0883 6.99094 49.2602 7.03694 49.4118 7.12434L61.3899 14.0208C61.4323 14.0457 61.4647 14.0769 61.5021 14.1055C61.5333 14.1305 61.5694 14.1529 61.5981 14.1803C61.633 14.2165 61.6579 14.2576 61.6878 14.2975C61.7103 14.3274 61.7377 14.3536 61.7551 14.386C61.7838 14.4346 61.8 14.4882 61.8199 14.5405C61.8312 14.5692 61.8474 14.5954 61.8548 14.6253ZM59.893 27.9844V16.6121L55.7013 19.0252L49.9104 22.3593V33.7317L59.8942 27.9844H59.893ZM47.9149 48.5566V37.1768L42.2187 40.4299L25.953 49.7133V61.2003L47.9149 48.5566ZM1.99677 9.83281V48.5566L23.9562 61.199V49.7145L12.4841 43.2219L12.4804 43.2194L12.4754 43.2169C12.4368 43.1945 12.4044 43.1621 12.3682 43.1347C12.3371 43.1097 12.3009 43.0898 12.2735 43.0624L12.271 43.0586C12.2386 43.0275 12.2162 42.9888 12.1887 42.9539C12.1638 42.9203 12.1339 42.8916 12.114 42.8567L12.1127 42.853C12.0903 42.8156 12.0766 42.7707 12.0604 42.7283C12.0442 42.6909 12.023 42.656 12.013 42.6161C12.0005 42.5688 11.998 42.5177 11.9931 42.4691C11.9881 42.4317 11.9781 42.3943 11.9781 42.3569V15.5801L6.18848 12.2446L1.99677 9.83281ZM12.9777 2.36177L2.99764 8.10652L12.9752 13.8513L22.9541 8.10527L12.9752 2.36177H12.9777ZM18.1678 38.2138L23.9574 34.8809V9.83281L19.7657 12.2459L13.9749 15.5801V40.6281L18.1678 38.2138ZM48.9133 9.14105L38.9344 14.8858L48.9133 20.6305L58.8909 14.8846L48.9133 9.14105ZM47.9149 22.3593L42.124 19.0252L37.9323 16.6121V27.9844L43.7219 31.3174L47.9149 33.7317V22.3593ZM24.9533 47.987L39.59 39.631L46.9065 35.4555L36.9352 29.7145L25.4544 36.3242L14.9907 42.3482L24.9533 47.987Z" fill="currentColor"/></svg>
</div>
@if (Route::has('login'))
<nav class="-mx-3 flex flex-1 justify-end">
@auth
<a
href="{{ url('/dashboard') }}"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Dashboard
</a>
@else
<a
href="{{ route('login') }}"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Log in
</a>
@if (Route::has('register'))
<a
href="{{ route('register') }}"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Register
</a>
@endif
@endauth
</nav>
@endif
</header>
<main class="mt-6">
<div class="grid gap-6 lg:grid-cols-2 lg:gap-8">
<a
href="https://laravel.com/docs"
id="docs-card"
class="flex flex-col items-start gap-6 overflow-hidden rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] md:row-span-3 lg:p-10 lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div id="screenshot-container" class="relative flex w-full flex-1 items-stretch">
<img
src="https://laravel.com/assets/img/welcome/docs-light.svg"
alt="Laravel documentation screenshot"
class="aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.06)] dark:hidden"
onerror="
document.getElementById('screenshot-container').classList.add('!hidden');
document.getElementById('docs-card').classList.add('!row-span-1');
document.getElementById('docs-card-content').classList.add('!flex-row');
document.getElementById('background').classList.add('!hidden');
"
/>
<img
src="https://laravel.com/assets/img/welcome/docs-dark.svg"
alt="Laravel documentation screenshot"
class="hidden aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.25)] dark:block"
/>
<div
class="absolute -bottom-16 -left-16 h-40 w-[calc(100%_+_8rem)] bg-gradient-to-b from-transparent via-white to-white dark:via-zinc-900 dark:to-zinc-900"
></div>
</div>
<div class="relative flex items-center gap-6 lg:items-end">
<div id="docs-card-content" class="flex items-start gap-6 lg:flex-col">
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#FF2D20" d="M23 4a1 1 0 0 0-1.447-.894L12.224 7.77a.5.5 0 0 1-.448 0L2.447 3.106A1 1 0 0 0 1 4v13.382a1.99 1.99 0 0 0 1.105 1.79l9.448 4.728c.14.065.293.1.447.1.154-.005.306-.04.447-.105l9.453-4.724a1.99 1.99 0 0 0 1.1-1.789V4ZM3 6.023a.25.25 0 0 1 .362-.223l7.5 3.75a.251.251 0 0 1 .138.223v11.2a.25.25 0 0 1-.362.224l-7.5-3.75a.25.25 0 0 1-.138-.22V6.023Zm18 11.2a.25.25 0 0 1-.138.224l-7.5 3.75a.249.249 0 0 1-.329-.099.249.249 0 0 1-.033-.12V9.772a.251.251 0 0 1 .138-.224l7.5-3.75a.25.25 0 0 1 .362.224v11.2Z"/><path fill="#FF2D20" d="m3.55 1.893 8 4.048a1.008 1.008 0 0 0 .9 0l8-4.048a1 1 0 0 0-.9-1.785l-7.322 3.706a.506.506 0 0 1-.452 0L4.454.108a1 1 0 0 0-.9 1.785H3.55Z"/></svg>
</div>
<div class="pt-3 sm:pt-5 lg:pt-0">
<h2 class="text-xl font-semibold text-black dark:text-white">Documentation</h2>
<p class="mt-4 text-sm/relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
</p>
</div>
</div>
<svg class="size-6 shrink-0 stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</div>
</a>
<a
href="https://laracasts.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M24 8.25a.5.5 0 0 0-.5-.5H.5a.5.5 0 0 0-.5.5v12a2.5 2.5 0 0 0 2.5 2.5h19a2.5 2.5 0 0 0 2.5-2.5v-12Zm-7.765 5.868a1.221 1.221 0 0 1 0 2.264l-6.626 2.776A1.153 1.153 0 0 1 8 18.123v-5.746a1.151 1.151 0 0 1 1.609-1.035l6.626 2.776ZM19.564 1.677a.25.25 0 0 0-.177-.427H15.6a.106.106 0 0 0-.072.03l-4.54 4.543a.25.25 0 0 0 .177.427h3.783c.027 0 .054-.01.073-.03l4.543-4.543ZM22.071 1.318a.047.047 0 0 0-.045.013l-4.492 4.492a.249.249 0 0 0 .038.385.25.25 0 0 0 .14.042h5.784a.5.5 0 0 0 .5-.5v-2a2.5 2.5 0 0 0-1.925-2.432ZM13.014 1.677a.25.25 0 0 0-.178-.427H9.101a.106.106 0 0 0-.073.03l-4.54 4.543a.25.25 0 0 0 .177.427H8.4a.106.106 0 0 0 .073-.03l4.54-4.543ZM6.513 1.677a.25.25 0 0 0-.177-.427H2.5A2.5 2.5 0 0 0 0 3.75v2a.5.5 0 0 0 .5.5h1.4a.106.106 0 0 0 .073-.03l4.54-4.543Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laracasts</h2>
<p class="mt-4 text-sm/relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
</p>
</div>
<svg class="size-6 shrink-0 self-center stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</a>
<a
href="https://laravel-news.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M8.75 4.5H5.5c-.69 0-1.25.56-1.25 1.25v4.75c0 .69.56 1.25 1.25 1.25h3.25c.69 0 1.25-.56 1.25-1.25V5.75c0-.69-.56-1.25-1.25-1.25Z"/><path d="M24 10a3 3 0 0 0-3-3h-2V2.5a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2V20a3.5 3.5 0 0 0 3.5 3.5h17A3.5 3.5 0 0 0 24 20V10ZM3.5 21.5A1.5 1.5 0 0 1 2 20V3a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 .5.5v17c0 .295.037.588.11.874a.5.5 0 0 1-.484.625L3.5 21.5ZM22 20a1.5 1.5 0 1 1-3 0V9.5a.5.5 0 0 1 .5-.5H21a1 1 0 0 1 1 1v10Z"/><path d="M12.751 6.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 7.3v-.5a.75.75 0 0 1 .751-.753ZM12.751 10.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 11.3v-.5a.75.75 0 0 1 .751-.753ZM4.751 14.047h10a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-10A.75.75 0 0 1 4 15.3v-.5a.75.75 0 0 1 .751-.753ZM4.75 18.047h7.5a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-7.5A.75.75 0 0 1 4 19.3v-.5a.75.75 0 0 1 .75-.753Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laravel News</h2>
<p class="mt-4 text-sm/relaxed">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
</p>
</div>
<svg class="size-6 shrink-0 self-center stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</a>
<div class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]">
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<g fill="#FF2D20">
<path
d="M16.597 12.635a.247.247 0 0 0-.08-.237 2.234 2.234 0 0 1-.769-1.68c.001-.195.03-.39.084-.578a.25.25 0 0 0-.09-.267 8.8 8.8 0 0 0-4.826-1.66.25.25 0 0 0-.268.181 2.5 2.5 0 0 1-2.4 1.824.045.045 0 0 0-.045.037 12.255 12.255 0 0 0-.093 3.86.251.251 0 0 0 .208.214c2.22.366 4.367 1.08 6.362 2.118a.252.252 0 0 0 .32-.079 10.09 10.09 0 0 0 1.597-3.733ZM13.616 17.968a.25.25 0 0 0-.063-.407A19.697 19.697 0 0 0 8.91 15.98a.25.25 0 0 0-.287.325c.151.455.334.898.548 1.328.437.827.981 1.594 1.619 2.28a.249.249 0 0 0 .32.044 29.13 29.13 0 0 0 2.506-1.99ZM6.303 14.105a.25.25 0 0 0 .265-.274 13.048 13.048 0 0 1 .205-4.045.062.062 0 0 0-.022-.07 2.5 2.5 0 0 1-.777-.982.25.25 0 0 0-.271-.149 11 11 0 0 0-5.6 2.815.255.255 0 0 0-.075.163c-.008.135-.02.27-.02.406.002.8.084 1.598.246 2.381a.25.25 0 0 0 .303.193 19.924 19.924 0 0 1 5.746-.438ZM9.228 20.914a.25.25 0 0 0 .1-.393 11.53 11.53 0 0 1-1.5-2.22 12.238 12.238 0 0 1-.91-2.465.248.248 0 0 0-.22-.187 18.876 18.876 0 0 0-5.69.33.249.249 0 0 0-.179.336c.838 2.142 2.272 4 4.132 5.353a.254.254 0 0 0 .15.048c1.41-.01 2.807-.282 4.117-.802ZM18.93 12.957l-.005-.008a.25.25 0 0 0-.268-.082 2.21 2.21 0 0 1-.41.081.25.25 0 0 0-.217.2c-.582 2.66-2.127 5.35-5.75 7.843a.248.248 0 0 0-.09.299.25.25 0 0 0 .065.091 28.703 28.703 0 0 0 2.662 2.12.246.246 0 0 0 .209.037c2.579-.701 4.85-2.242 6.456-4.378a.25.25 0 0 0 .048-.189 13.51 13.51 0 0 0-2.7-6.014ZM5.702 7.058a.254.254 0 0 0 .2-.165A2.488 2.488 0 0 1 7.98 5.245a.093.093 0 0 0 .078-.062 19.734 19.734 0 0 1 3.055-4.74.25.25 0 0 0-.21-.41 12.009 12.009 0 0 0-10.4 8.558.25.25 0 0 0 .373.281 12.912 12.912 0 0 1 4.826-1.814ZM10.773 22.052a.25.25 0 0 0-.28-.046c-.758.356-1.55.635-2.365.833a.25.25 0 0 0-.022.48c1.252.43 2.568.65 3.893.65.1 0 .2 0 .3-.008a.25.25 0 0 0 .147-.444c-.526-.424-1.1-.917-1.673-1.465ZM18.744 8.436a.249.249 0 0 0 .15.228 2.246 2.246 0 0 1 1.352 2.054c0 .337-.08.67-.23.972a.25.25 0 0 0 .042.28l.007.009a15.016 15.016 0 0 1 2.52 4.6.25.25 0 0 0 .37.132.25.25 0 0 0 .096-.114c.623-1.464.944-3.039.945-4.63a12.005 12.005 0 0 0-5.78-10.258.25.25 0 0 0-.373.274c.547 2.109.85 4.274.901 6.453ZM9.61 5.38a.25.25 0 0 0 .08.31c.34.24.616.561.8.935a.25.25 0 0 0 .3.127.631.631 0 0 1 .206-.034c2.054.078 4.036.772 5.69 1.991a.251.251 0 0 0 .267.024c.046-.024.093-.047.141-.067a.25.25 0 0 0 .151-.23A29.98 29.98 0 0 0 15.957.764a.25.25 0 0 0-.16-.164 11.924 11.924 0 0 0-2.21-.518.252.252 0 0 0-.215.076A22.456 22.456 0 0 0 9.61 5.38Z"
/>
</g>
</svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Vibrant Ecosystem</h2>
<p class="mt-4 text-sm/relaxed">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white dark:focus-visible:ring-[#FF2D20]">Forge</a>, <a href="https://vapor.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Vapor</a>, <a href="https://nova.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Nova</a>, <a href="https://envoyer.io" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Envoyer</a>, and <a href="https://herd.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Herd</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Echo</a>, <a href="https://laravel.com/docs/horizon" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Telescope</a>, and more.
</p>
</div>
</div>
</div>
</main>
<footer class="py-16 text-center text-sm text-black dark:text-white/70">
Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
</footer>
</div>
</div>
</div>
</body>
</html>
<?php
use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
$services = [
'database' => 'up',
'redis' => 'up',
'peertube' => 'up',
'storage' => 'up',
];
try {
\Illuminate\Support\Facades\DB::connection()->getPdo();
} catch (\Exception $e) {
$services['database'] = 'down';
}
try {
\Illuminate\Support\Facades\Redis::ping();
} catch (\Exception $e) {
$services['redis'] = 'down';
}
$allUp = !in_array('down', $services, true);
return response()->json([
'status' => $allUp ? 'healthy' : 'degraded',
'services' => $services,
'version' => '1.0.0',
'timestamp' => now()->toIso8601String(),
], $allUp ? 200 : 503);
});
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
})->name('home');
Route::get('/locale/{locale}', function (string $locale) {
if (in_array($locale, config('app.supported_locales', ['en', 'ar']))) {
session(['locale' => $locale]);
app()->setLocale($locale);
}
return redirect()->back();
})->name('locale.switch');
# UGC Marketplace — Full Extended Feature List
## Admin Features
### User Management
* View all users
* Search users
* Ban users
* Suspend users
* Soft delete users
* Restore users
* Verify users
* Change user roles
* Merge duplicate accounts
* View login history
* View activity history
* View reported content
* View reported users
### Company Management
* Approve companies
* Reject companies
* Verify companies
* Suspend companies
* View company statistics
* View company projects
* View company ratings
* View company complaints
### Creator Management
* Approve creators
* Reject creators
* Verify creators
* Suspend creators
* View creator portfolio
* View creator history
* View creator ratings
* View creator complaints
### Moderation
* Content moderation
* Portfolio moderation
* Campaign moderation
* Message moderation
* Review moderation
* Scam investigation
* Dispute resolution
### Analytics
* Total creators
* Total companies
* Active campaigns
* Active projects
* Completed projects
* Submission statistics
* Creator growth
* Company growth
* Retention metrics
---
# Company Features
## Company Profile
### Basic Information
* Company name
* Logo
* Description
* Website
* Industry
* Country
* Team size
### Branding
* Brand guidelines
* Brand colors
* Brand assets
* Brand documents
### Verification
* Verification status
* Trust badge
---
# Creator Features
## Creator Profile
### Identity
* Display name
* Username
* Bio
* Profile picture
* Cover image
### Professional Information
* Experience level
* Languages
* Country
* City
* Timezone
### Content Categories
* Gaming
* Technology
* Apps
* Fitness
* Food
* Beauty
* Fashion
* Travel
* Education
* Finance
* Business
* Automotive
* Pets
* Lifestyle
* Parenting
### Skills
* Video creation
* Script writing
* Voiceover
* Acting
* Editing
* Motion graphics
* Product photography
* Livestreaming
### Equipment
* Phone models
* Cameras
* Microphones
* Lighting
* Green screen
* Studio setup
### Social Links
* TikTok
* Instagram
* YouTube
* Facebook
* X
* Snapchat
* LinkedIn
### Availability
* Available
* Busy
* Vacation
---
# Portfolio Features
## Video Portfolio
* Upload videos
* Organize videos
* Create collections
* Featured videos
* Video categories
## Portfolio Metadata
* Title
* Description
* Tags
* Industry
* Brand worked with
## Portfolio Visibility
* Public
* Private
* Unlisted
---
# Campaign Features
## Campaign Creation
### General Information
* Title
* Description
* Campaign objective
### Requirements
* Creator type
* Gender preference
* Age range
* Country requirements
* Language requirements
* Equipment requirements
### Deliverables
* Number of videos
* Video length
* Aspect ratio
* Format requirements
### Timeline
* Application deadline
* Start date
* Due date
### Attachments
* Brand guide
* Product photos
* Scripts
* References
* Documents
### Visibility
* Public campaign
* Invite-only campaign
---
# Campaign Discovery
### Search
* Keyword search
### Filters
* Category
* Country
* Language
* Deliverable type
* Deadline
### Sorting
* Newest
* Oldest
* Most applicants
* Deadline soon
---
# Applications
### Creator Actions
* Apply
* Withdraw application
* Update application
### Application Data
* Cover message
* Portfolio references
* Experience notes
### Application Status
* Pending
* Viewed
* Shortlisted
* Accepted
* Rejected
* Withdrawn
---
# Creator Discovery
## Search Creators
### Filters
* Country
* Language
* Gender
* Age range
* Niche
* Experience level
* Equipment quality
* Availability
### Sorting
* Newest
* Most experienced
* Highest rated
* Most completed projects
---
# Invitations
### Company Actions
* Send invitation
* Cancel invitation
* Bulk invite creators
### Creator Actions
* Accept invitation
* Decline invitation
---
# Project Features
## Project Dashboard
### Overview
* Project details
* Creator information
* Company information
* Deliverables
* Timeline
### Statuses
* Not Started
* In Progress
* Waiting Review
* Revision Requested
* Approved
* Completed
* Cancelled
---
# Deliverables
## Deliverable Types
* Video
* Image
* Script
* Voice recording
* Raw footage
* Document
### Deliverable States
* Draft
* Submitted
* Under Review
* Revision Requested
* Approved
---
# Video Review System
### Submission Features
* Upload video
* Replace video
* Version history
### Review Features
* Leave comments
* Timestamp comments
* Request changes
* Approve submission
### Revision Features
* Multiple revisions
* Revision history
* Revision comparison
---
# Messaging
## Direct Messaging
* Creator to company
* Company to creator
### Features
* Attachments
* Video sharing
* Image sharing
* Emoji reactions
* Read receipts
---
# Notifications
## In-App Notifications
### Events
* New application
* Application accepted
* Application rejected
* New invitation
* Invitation accepted
* Deliverable submitted
* Deliverable approved
* Message received
---
# Review System
## Company Reviews Creator
### Metrics
* Communication
* Professionalism
* Quality
* Reliability
## Creator Reviews Company
### Metrics
* Communication
* Clarity
* Professionalism
* Fairness
---
# Reputation System
### Creator Reputation
* Completion rate
* Response rate
* Approval rate
* Review score
### Company Reputation
* Review score
* Project completion history
* Response speed
---
# Reporting System
### Report User
* Scam
* Harassment
* Spam
* Fake profile
### Report Content
* Inappropriate content
* Copyright violation
* Fraud
---
# Matching System
### Creator Recommendations
Based on:
* Skills
* Categories
* Language
* Country
* Portfolio
### Campaign Recommendations
Based on:
* Past applications
* Categories
* Skills
* Interests
---
# Team Features (Future)
### Company Teams
* Owner
* Manager
* Reviewer
* Member
### Permissions
* Create campaigns
* Review submissions
* Invite creators
* View analytics
---
# AI Features (Future)
### Creator Side
* Profile optimization
* Portfolio analysis
* Suggested campaigns
### Company Side
* Campaign writing assistant
* Creator recommendations
* Brief generation
---
# Enterprise Features (Future)
### Agencies
* Multiple brands
* Multiple teams
* Shared assets
### Large Companies
* Approval workflows
* Department permissions
* Advanced reporting
---
# Mobile App Features (Future)
### Creator App
* Apply to campaigns
* Upload videos
* Respond to messages
* Receive notifications
### Company App
* Review submissions
* Approve deliverables
* Message creators
---
If you eventually build *everything* that a mature UGC marketplace needs, you're looking at roughly **150–250 individual features** grouped into these systems, which is comparable in complexity to an early-stage Upwork/Fiverr specifically optimized for UGC production.
That's actually the strongest design. Don't think of them as two separate flows; think of them as two acquisition channels leading to the same project workflow.
Channel A: Campaign Discovery
Company creates campaign
Creators discover campaign
Creators apply
Company reviews applicants
Company selects creator
Project starts
This helps creators find work.
Channel B: Creator Discovery
Creator builds profile
Creator uploads portfolio
Company searches creators
Company sends invitation
Creator accepts
Project starts
This helps companies find talent.
After Selection, Everything Becomes The Same
Regardless of how they met:
Project Created
Requirements Shared
Script Submission (optional)
Video Submission
Feedback
Revisions
Approval
Completed
This is important because you only build one project management system.
Core Entities
Users
Companies
Creators
CreatorPortfolios
Campaigns
CampaignApplications
ProjectInvitations
Projects
Deliverables
VideoSubmissions
Reviews
Messages
Creator Profile Should Be Rich
A company should be able to filter by:
Gender
Age range
Country
Languages
Niches (Gaming, Beauty, Fitness, SaaS, Finance, Food, etc.)
Video styles
Equipment quality
Social links
Portfolio videos
Example:
Mahmoud
────────────
Languages:
Arabic, English
Location:
Egypt
Niches:
Gaming
Tech
Apps
Portfolio:
Video #1
Video #2
Video #3
Response Rate:
95%
Campaign Structure
A campaign should contain:
Title
Description
Budget (optional)
Product/Service
Required Creator Count
Requirements
Deadline
Attachments
Status
One Feature I'd Add Early
A "recommended match" system.
When a company opens a campaign:
Recommended Creators
- Creator A
- Creator B
- Creator C
When a creator logs in:
Recommended Campaigns
- Campaign A
- Campaign B
- Campaign C
Even a simple matching algorithm based on tags, language, country, and niche will make the platform feel much more valuable.
\ No newline at end of file
*
!private/
!public/
!.gitignore
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json
import defaultTheme from 'tailwindcss/defaultTheme';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/**/*.blade.php',
'./resources/**/*.js',
'./app/Modules/**/resources/**/*.blade.php',
],
darkMode: ['class', '[data-theme="dark"]'],
theme: {
extend: {
fontFamily: {
heading: ['Plus Jakarta Sans', ...defaultTheme.fontFamily.sans],
body: ['Inter', ...defaultTheme.fontFamily.sans],
arabic: ['Tajawal', ...defaultTheme.fontFamily.sans],
},
fontSize: {
'xs': ['12px', { lineHeight: '1.4' }],
'sm': ['13px', { lineHeight: '1.4' }],
'base': ['15px', { lineHeight: '1.5' }],
'lg': ['17px', { lineHeight: '1.5' }],
'xl': ['20px', { lineHeight: '1.4' }],
'2xl': ['24px', { lineHeight: '1.3' }],
'3xl': ['30px', { lineHeight: '1.2' }],
'4xl': ['40px', { lineHeight: '1.1' }],
'5xl': ['52px', { lineHeight: '1.0' }],
},
borderRadius: {
'card': '16px',
'button': '12px',
'input': '10px',
'sidebar': '10px',
'badge': '20px',
},
transitionTimingFunction: {
'out-expo': 'cubic-bezier(0.16, 1, 0.3, 1)',
'in-expo': 'cubic-bezier(0.7, 0, 0.84, 0)',
'spring': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
},
animation: {
'fade-in': 'fadeIn 200ms var(--ease-out) forwards',
'fade-up': 'fadeUp 300ms var(--ease-out) forwards',
'slide-in': 'slideIn 250ms var(--ease-out) forwards',
'scale-in': 'scaleIn 200ms var(--ease-out) forwards',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
fadeUp: {
'0%': { opacity: '0', transform: 'translateY(8px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideIn: {
'0%': { opacity: '0', transform: 'translateX(-8px)' },
'100%': { opacity: '1', transform: 'translateX(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
},
},
plugins: [],
};
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
# UGC Heaven Database
## Connection Details
| Key | Value |
|-----|-------|
| **Host (public)** | `18.192.166.221` |
| **Port (public)** | `5433` |
| **Host (internal/CapRover apps)** | `srv-captain--ugc-heaven-db` |
| **Port (internal)** | `5432` |
| **User** | `ugcadmin` |
| **Password** | `UgcH3aven2024!` |
| **Default DB** | `ugc_heaven` |
| **Version** | PostgreSQL 16.14 (Alpine) |
| **Docker Service** | `srv-captain--ugc-heaven-db` |
| **Storage** | Persistent volume: `ugc-heaven-db-data` |
---
## Connection Strings
```bash
# External apps (public internet)
postgresql://ugcadmin:UgcH3aven2024!@18.192.166.221:5433/ugc_heaven
# CapRover apps (internal Docker network)
postgresql://ugcadmin:UgcH3aven2024!@srv-captain--ugc-heaven-db:5432/ugc_heaven
```
---
## Quick Connect
### psql (CLI)
```bash
PGPASSWORD='UgcH3aven2024!' psql -h 18.192.166.221 -p 5433 -U ugcadmin -d ugc_heaven
```
### Python (psycopg2)
```python
import psycopg2
conn = psycopg2.connect(
host="18.192.166.221",
port=5433,
user="ugcadmin",
password="UgcH3aven2024!",
dbname="ugc_heaven"
)
```
### Node.js (pg)
```javascript
const { Pool } = require('pg');
const pool = new Pool({
host: '18.192.166.221',
port: 5433,
user: 'ugcadmin',
password: 'UgcH3aven2024!',
database: 'ugc_heaven',
ssl: false
});
```
### Go
```go
dsn := "host=18.192.166.221 port=5433 user=ugcadmin password=UgcH3aven2024! dbname=ugc_heaven sslmode=disable"
```
### C# / Unity
```csharp
string connStr = "Host=18.192.166.221;Port=5433;Username=ugcadmin;Password=UgcH3aven2024!;Database=ugc_heaven";
```
### Laravel (.env)
```env
DB_CONNECTION=pgsql
DB_HOST=18.192.166.221
DB_PORT=5433
DB_DATABASE=ugc_heaven
DB_USERNAME=ugcadmin
DB_PASSWORD=UgcH3aven2024!
```
### Flutter (Dart)
```dart
final connection = await Connection.open(Endpoint(
host: '18.192.166.221',
port: 5433,
database: 'ugc_heaven',
username: 'ugcadmin',
password: 'UgcH3aven2024!',
));
```
---
## Admin Operations
### Create a New Database
```sql
CREATE DATABASE my_new_app;
```
### Create a New User (for an app)
```sql
CREATE USER myapp_user WITH PASSWORD 'secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE my_new_app TO myapp_user;
```
### Create a Read-Only User
```sql
CREATE USER readonly_user WITH PASSWORD 'readonly_pass';
GRANT CONNECT ON DATABASE ugc_heaven TO readonly_user;
GRANT USAGE ON SCHEMA public TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_user;
```
### List All Databases
```sql
\l
-- or
SELECT datname FROM pg_database WHERE datistemplate = false;
```
### List All Tables
```sql
\dt
-- or
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
```
### Check Database Size
```sql
SELECT pg_size_pretty(pg_database_size('ugc_heaven'));
```
### Check All Database Sizes
```sql
SELECT datname, pg_size_pretty(pg_database_size(datname))
FROM pg_database WHERE datistemplate = false ORDER BY pg_database_size(datname) DESC;
```
---
## Useful Extensions
```sql
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enable full-text search extras
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Enable JSON path queries
CREATE EXTENSION IF NOT EXISTS jsonb_plperl;
-- Enable crypto functions
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Check available extensions
SELECT name, comment FROM pg_available_extensions ORDER BY name;
```
---
## SSH Access (Server-Side)
```bash
# SSH to server
ssh -i /Users/mahmoudaglan/NewMigration/newServer.pem -o StrictHostKeyChecking=no ubuntu@18.192.166.221
# Connect to DB from inside container
sudo docker exec -it $(sudo docker ps -q --filter "name=srv-captain--ugc-heaven-db") psql -U ugcadmin -d ugc_heaven
# View logs
sudo docker logs $(sudo docker ps -q --filter "name=srv-captain--ugc-heaven-db") --tail 50
# Restart
sudo docker service update --force srv-captain--ugc-heaven-db
# Backup a database
sudo docker exec $(sudo docker ps -q --filter "name=srv-captain--ugc-heaven-db") pg_dump -U ugcadmin ugc_heaven > backup.sql
# Restore a database
cat backup.sql | sudo docker exec -i $(sudo docker ps -q --filter "name=srv-captain--ugc-heaven-db") psql -U ugcadmin -d ugc_heaven
```
---
## phpMyAdmin Alternative (pgAdmin)
Access via the existing phpMyAdmin won't work for Postgres. If you want a web UI, you can connect using:
- **Supabase Studio** (already running on your server)
- Or install pgAdmin as another CapRover app later
---
## Notes
- `ugcadmin` is a **superuser** — it can create databases, users, extensions, and manage everything.
- Port `5433` is exposed publicly — any app on the internet can connect with the credentials.
- For CapRover apps, use the internal hostname `srv-captain--ugc-heaven-db` on port `5432` (faster, no SSL needed).
- Data persists across container restarts via the Docker volume `ugc-heaven-db-data`.
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment