diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index c8ef8b6..0000000 --- a/.dockerignore +++ /dev/null @@ -1,62 +0,0 @@ -# Git -.git -.gitignore -.gitattributes - -# Documentation -*.md -LICENSE -docs/ - -# Environment files -.env -.env.* -!.env.example - -# Dependencies (will be installed fresh) -vendor/ -node_modules/ - -# Storage (will be mounted as volumes) -storage/app/* -storage/framework/cache/* -storage/framework/sessions/* -storage/framework/views/* -storage/logs/* -bootstrap/cache/* - -# Testing -phpunit.xml -.phpunit.result.cache -tests/ -coverage/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Docker -Dockerfile* -docker-compose*.yml -.dockerignore - -# Frontend old -frontend-old/ - -# Build artifacts -public/build/ -public/hot -public/mix-manifest.json - -# Development -.php_cs.cache -.php-cs-fixer.cache -phpstan.neon -.editorconfig \ No newline at end of file diff --git a/.env.dusk.local b/.env.dusk.local deleted file mode 100644 index 6f0c215..0000000 --- a/.env.dusk.local +++ /dev/null @@ -1,24 +0,0 @@ -APP_NAME=DishPlanner -APP_ENV=testing -APP_KEY=base64:KSKZNT+cJuaBRBv4Y2HQqav6hzREKoLkNIKN8yszU1Q= -APP_DEBUG=true -APP_URL=http://dishplanner_app:8000 - -LOG_CHANNEL=single - -# Test database -DB_CONNECTION=mysql -DB_HOST=db -DB_PORT=3306 -DB_DATABASE=dishplanner_test -DB_USERNAME=dishplanner -DB_PASSWORD=dishplanner - -BROADCAST_DRIVER=log -CACHE_DRIVER=array -FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=file -SESSION_LIFETIME=120 - -MAIL_MAILER=array \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index ef44df9..0000000 --- a/.env.example +++ /dev/null @@ -1,66 +0,0 @@ -# Application Settings -APP_NAME=DishPlanner -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_TIMEZONE=UTC -APP_URL=http://localhost:8000 - -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -# Logging -LOG_CHANNEL=stack -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -# Database Connection (Docker) -DB_CONNECTION=mysql -DB_HOST=db # Use 'db' for Docker, 'localhost' for local -DB_PORT=3306 -DB_DATABASE=dishplanner -DB_USERNAME=dishplanner -DB_PASSWORD=dishplanner -DB_ROOT_PASSWORD=root # For Docker MariaDB root user - -# Session & Cache -SESSION_DRIVER=file # Use 'database' for production -SESSION_LIFETIME=120 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -CACHE_STORE=file -CACHE_PREFIX=dishplanner - -# Queue -QUEUE_CONNECTION=sync # Use 'database' for production - -BROADCAST_CONNECTION=log -FILESYSTEM_DISK=local - -# Redis (Optional - uncomment if using Redis) -# REDIS_CLIENT=phpredis -# REDIS_HOST=redis # Use 'redis' for Docker -# REDIS_PASSWORD=null -# REDIS_PORT=6379 - -# Mail Settings -MAIL_MAILER=log # Use 'smtp' for production -MAIL_HOST=mailhog # Use 'mailhog' for Docker dev -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="noreply@dishplanner.local" -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -VITE_APP_NAME="${APP_NAME}" diff --git a/.gitignore b/.gitignore index a9a82cb..a09c56d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1 @@ -/composer.lock -/.phpunit.cache -/coverage -/node_modules -/public/build -/public/hot -/public/storage -/storage/*.key -/storage/pail -/vendor -.env -.env.backup -.env.production -.phpactor.json -.phpunit.result.cache -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log -/auth.json -/.fleet /.idea -/.nova -/.vscode -/.zed -/.vite diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json deleted file mode 100644 index 71b5d08..0000000 --- a/.vite/deps/_metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "hash": "9e76e24f", - "configHash": "16a1459d", - "lockfileHash": "e3b0c442", - "browserHash": "8e8bb46e", - "optimized": {}, - "chunks": {} -} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json deleted file mode 100644 index 3dbc1ca..0000000 --- a/.vite/deps/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 902cec7..0000000 --- a/Dockerfile +++ /dev/null @@ -1,123 +0,0 @@ -# Production Dockerfile with FrankenPHP -FROM dunglas/frankenphp:latest-php8.3-alpine - -# Install system dependencies -RUN apk add --no-cache \ - nodejs \ - npm \ - git \ - mysql-client - -# Install PHP extensions -RUN install-php-extensions \ - pdo_mysql \ - opcache \ - zip \ - gd \ - intl \ - bcmath - -# Install Composer -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /app - -# Set fixed production environment variables -ENV APP_ENV=production \ - APP_DEBUG=false \ - DB_CONNECTION=mysql \ - DB_HOST=db \ - DB_PORT=3306 \ - SESSION_DRIVER=database \ - CACHE_DRIVER=file \ - QUEUE_CONNECTION=database \ - LOG_CHANNEL=stack \ - LOG_LEVEL=error \ - MAIL_MAILER=smtp \ - MAIL_ENCRYPTION=tls - -# Copy application code first -COPY . . - -# Install PHP dependencies (production only) -RUN composer install --no-dev --no-interaction --optimize-autoloader - -# Install ALL Node dependencies (including dev for building) -RUN npm ci - -# Build frontend assets -RUN npm run build - -# Remove node_modules after build to save space -RUN rm -rf node_modules - -# Laravel optimizations -RUN php artisan config:cache \ - && php artisan route:cache \ - && composer dump-autoload --optimize - -# Set permissions -RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache - -# Configure Caddy -RUN cat > /etc/caddy/Caddyfile < /start-prod.sh <<'EOF' -#!/bin/sh -set -e - -# Wait for database to be ready -echo "Waiting for database..." -for i in $(seq 1 30); do - if mysqladmin ping -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" --silent 2>/dev/null; then - echo "Database is ready!" - break - fi - echo "Waiting for database... ($i/30)" - sleep 2 -done - -# Run migrations -echo "Running migrations..." -php artisan migrate --force || echo "Migrations failed or already up-to-date" - -# Start FrankenPHP -exec frankenphp run --config /etc/caddy/Caddyfile -EOF - -RUN chmod +x /start-prod.sh - -# Start with our script -CMD ["/start-prod.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index c36d143..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,128 +0,0 @@ -# Development Dockerfile with FrankenPHP -FROM dunglas/frankenphp:latest-php8.3-alpine - -# Install system dependencies + development tools -RUN apk add --no-cache \ - nodejs \ - npm \ - git \ - mysql-client \ - vim \ - bash \ - nano - -# Install PHP extensions including xdebug for development -RUN install-php-extensions \ - pdo_mysql \ - opcache \ - zip \ - gd \ - intl \ - bcmath \ - xdebug - -# Install Composer -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /app - -# Configure PHP for development -RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" - -# Configure Xdebug (disabled by default to reduce noise) -RUN echo "xdebug.mode=off" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo ";xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo ";xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo ";xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - -# Configure Caddy for development (simpler, no worker mode) -RUN cat > /etc/caddy/Caddyfile < /start.sh <<'EOF' -#!/bin/sh -set -e - -# Create .env file if it doesn't exist -if [ ! -f ".env" ]; then - echo "Creating .env file from .env.example..." - cp .env.example .env -fi - -# Install dependencies if volumes are empty -if [ ! -f "vendor/autoload.php" ]; then - echo "Installing composer dependencies..." - composer install -fi - -# Handle node_modules with care - clean install if having issues -if [ ! -f "node_modules/.bin/vite" ]; then - echo "Installing npm dependencies..." - # Clean any remnants first - rm -rf node_modules/.* 2>/dev/null || true - rm -rf /app/.npm 2>/dev/null || true - # Fresh install with cache in tmp to avoid permission issues - npm install --cache /tmp/.npm -else - echo "Node modules already installed, skipping npm install" -fi - -# Clear Laravel caches -php artisan config:clear || true -php artisan cache:clear || true - -# Wait for database and run migrations -echo "Waiting for database..." -sleep 5 -php artisan migrate --force || echo "Migration failed or not needed" - -# Run development seeder (only in dev environment) -echo "Running development seeder..." -php artisan db:seed --class=DevelopmentSeeder --force || echo "Seeding skipped or already done" - -# Generate app key if not set -if [ -z "$APP_KEY" ] || [ "$APP_KEY" = "base64:YOUR_KEY_HERE" ]; then - echo "Generating application key..." - php artisan key:generate -fi - -# Start Vite dev server in background -npm run dev & - -# Start FrankenPHP -exec frankenphp run --config /etc/caddy/Caddyfile -EOF - -RUN chmod +x /start.sh - -# Expose ports -EXPOSE 8000 5173 - -# Use the startup script -CMD ["/start.sh"] \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 01617f9..0000000 --- a/Makefile +++ /dev/null @@ -1,123 +0,0 @@ -# Dish Planner - Docker Commands - -.PHONY: help -help: ## Show this help message - @echo "Dish Planner - Docker Management" - @echo "" - @echo "Usage: make [command]" - @echo "" - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' - -# Development Commands -.PHONY: dev -dev: ## Start development environment - docker compose up -d - @echo "Development server running at http://localhost:8000" - @echo "Mailhog available at http://localhost:8025" - -.PHONY: dev-build -dev-build: ## Build and start development environment - docker compose build - docker compose up -d - -.PHONY: dev-stop -dev-stop: ## Stop development environment - docker compose down - -.PHONY: dev-clean -dev-clean: ## Stop and remove volumes (CAUTION: removes database) - docker compose down -v - -.PHONY: logs -logs: ## Show application logs - docker compose logs -f app - -.PHONY: logs-db -logs-db: ## Show database logs - docker compose logs -f db - -# Production Commands -.PHONY: prod-build -prod-build: ## Build production image for Codeberg - ./bin/build-push.sh - -.PHONY: prod-build-tag -prod-build-tag: ## Build with specific tag (usage: make prod-build-tag TAG=v1.0.0) - ./bin/build-push.sh $(TAG) - -.PHONY: prod-login -prod-login: ## Login to Codeberg registry - podman login codeberg.org - -# Laravel Commands -.PHONY: artisan -artisan: ## Run artisan command (usage: make artisan cmd="migrate") - docker compose exec app php artisan $(cmd) - -.PHONY: composer -composer: ## Run composer command (usage: make composer cmd="require package") - docker compose exec app composer $(cmd) - -.PHONY: npm -npm: ## Run npm command (usage: make npm cmd="install package") - docker compose exec app npm $(cmd) - -.PHONY: migrate -migrate: ## Run database migrations - docker compose exec app php artisan migrate - -.PHONY: seed -seed: ## Seed the database - docker compose exec app php artisan db:seed - -.PHONY: fresh -fresh: ## Fresh migrate and seed - docker compose exec app php artisan migrate:fresh --seed - -.PHONY: tinker -tinker: ## Start Laravel tinker - docker compose exec app php artisan tinker - -.PHONY: test -test: ## Run tests - docker compose exec app php artisan test - -# Utility Commands -.PHONY: shell -shell: ## Enter app container shell - docker compose exec app sh - -.PHONY: db-shell -db-shell: ## Enter database shell - docker compose exec db mariadb -u dishplanner -pdishplanner dishplanner - -.PHONY: clear -clear: ## Clear all Laravel caches - docker compose exec app php artisan cache:clear - docker compose exec app php artisan config:clear - docker compose exec app php artisan route:clear - docker compose exec app php artisan view:clear - -.PHONY: optimize -optimize: ## Optimize Laravel for production - docker compose exec app php artisan config:cache - docker compose exec app php artisan route:cache - docker compose exec app php artisan view:cache - docker compose exec app php artisan livewire:discover - -# Installation -.PHONY: install -install: ## First time setup - @echo "Setting up Dish Planner..." - @cp -n .env.example .env || true - @echo "Generating application key..." - @docker compose build - @docker compose up -d - @sleep 5 - @docker compose exec app php artisan key:generate - @docker compose exec app php artisan migrate - @echo "" - @echo "โœ… Installation complete!" - @echo "Access the app at: http://localhost:8000" - @echo "Create your first planner and user to get started." \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index ea4cdbd..0000000 --- a/README.md +++ /dev/null @@ -1,295 +0,0 @@ -# ๐Ÿฝ๏ธ Dish Planner - -A Laravel-based meal planning application that helps households organize and schedule their dishes among multiple users. Built with Laravel, Livewire, and FrankenPHP for a modern, single-container deployment. - -## โœจ Features - -- **Multi-user dish management** - Assign dishes to specific household members -- **Smart scheduling** - Automatic schedule generation with recurrence patterns -- **Calendar view** - Visual 31-day schedule overview -- **Real-time updates** - Livewire-powered reactive interface -- **Dark theme UI** - Modern interface with purple/pink accents -- **Single container deployment** - Simplified hosting with FrankenPHP - -## ๐Ÿš€ Quick Start - -### Prerequisites -- Docker and Docker Compose -- Make (optional, for convenience commands) - -### First Time Setup - -```bash -# Clone the repository -git clone https://github.com/yourusername/dish-planner.git -cd dish-planner - -# Quick install with Make -make install - -# Or manually: -cp .env.example .env -docker compose build -docker compose up -d -docker compose exec app php artisan key:generate -docker compose exec app php artisan migrate -``` - -The application will be available at **http://localhost:8000** - -## ๐Ÿ”ง Development - -### Starting the Development Environment - -```bash -# Start all services -make dev - -# Or with Docker Compose directly -docker compose up -d -``` - -**Available services:** -- **App**: http://localhost:8000 (Laravel + FrankenPHP) -- **Vite**: http://localhost:5173 (Asset hot-reload) -- **Mailhog**: http://localhost:8025 (Email testing) -- **Database**: localhost:3306 (MariaDB) - -### Common Development Commands - -```bash -# View logs -make logs # App logs -make logs-db # Database logs - -# Laravel commands -make artisan cmd="migrate" # Run artisan commands -make tinker # Start Laravel tinker -make test # Run tests - -# Database -make migrate # Run migrations -make seed # Seed database -make fresh # Fresh migrate with seeds - -# Testing -make test # Run tests -composer test:coverage-html # Run tests with coverage report (generates coverage/index.html) - -# Utilities -make shell # Enter app container -make db-shell # Enter database shell -make clear # Clear all caches -``` - -### Project Structure - -``` -dish-planner/ -โ”œโ”€โ”€ app/ -โ”‚ โ”œโ”€โ”€ Livewire/ # Livewire components -โ”‚ โ”‚ โ”œโ”€โ”€ Auth/ # Authentication -โ”‚ โ”‚ โ”œโ”€โ”€ Dishes/ # Dish management -โ”‚ โ”‚ โ”œโ”€โ”€ Schedule/ # Schedule calendar -โ”‚ โ”‚ โ””โ”€โ”€ Users/ # User management -โ”‚ โ””โ”€โ”€ Models/ # Eloquent models -โ”œโ”€โ”€ resources/ -โ”‚ โ””โ”€โ”€ views/ -โ”‚ โ””โ”€โ”€ livewire/ # Livewire component views -โ”œโ”€โ”€ docker-compose.yml # Development environment -โ”œโ”€โ”€ docker-compose.prod.yml # Production environment -โ”œโ”€โ”€ Dockerfile # Production image -โ”œโ”€โ”€ Dockerfile.dev # Development image -โ””โ”€โ”€ Makefile # Convenience commands -``` - -## ๐Ÿšข Production Deployment - -### Building for Production - -```bash -# Build production image -make prod-build - -# Start production environment -make prod - -# Or with Docker Compose -docker compose -f docker-compose.prod.yml build -docker compose -f docker-compose.prod.yml up -d -``` - -### Production Environment Variables - -Required environment variables for production: - -```env -# Required - Generate APP_KEY (see instructions below) -APP_KEY=base64:your-generated-key-here -APP_URL=https://your-domain.com - -# Database Configuration -DB_DATABASE=dishplanner -DB_USERNAME=dishplanner -DB_PASSWORD=strong-password-here -DB_ROOT_PASSWORD=strong-root-password - -# Optional Email Configuration -MAIL_HOST=your-smtp-host -MAIL_PORT=587 -MAIL_USERNAME=your-username -MAIL_PASSWORD=your-password -MAIL_FROM_ADDRESS=noreply@your-domain.com -``` - -#### Generating APP_KEY - -The APP_KEY is critical for encryption and must be kept consistent across deployments. Generate one using any of these methods: - -**Option 1: Using OpenSSL (Linux/Mac/Windows with Git Bash)** -```bash -echo "base64:$(openssl rand -base64 32)" -``` - -**Option 2: Using Node.js (Cross-platform)** -```bash -node -e "console.log('base64:' + require('crypto').randomBytes(32).toString('base64'))" -``` - -**Option 3: Using Python (Cross-platform)** -```bash -python -c "import base64, os; print('base64:' + base64.b64encode(os.urandom(32)).decode())" -``` - -**Option 4: Online Generator** -Generate a random 32-character string at https://randomkeygen.com/ and prepend with `base64:` - -โš ๏ธ **Important**: Save this key securely! If lost, you won't be able to decrypt existing data. - -### Deployment with DockGE - -The production setup is optimized for DockGE deployment with just 2 containers: - -1. **app** - Laravel application with FrankenPHP -2. **db** - MariaDB database - -Simply import the `docker-compose.prod.yml` into DockGE and configure your environment variables. - -## ๐Ÿ› ๏ธ Technology Stack - -- **Backend**: Laravel 12 with Livewire 3 -- **Web Server**: FrankenPHP (PHP 8.3 + Caddy) -- **Database**: MariaDB 11 -- **Frontend**: Blade + Livewire + Alpine.js -- **Styling**: TailwindCSS with custom dark theme -- **Assets**: Vite for bundling - -## ๐Ÿ“ฆ Docker Architecture - -### Development (`docker-compose.yml`) -- **Hot reload** - Volume mounts for live code editing -- **Debug tools** - Xdebug configured for debugging -- **Email testing** - Mailhog for capturing emails -- **Asset watching** - Vite dev server for instant updates - -### Production (`docker-compose.prod.yml`) -- **Optimized** - Multi-stage builds with caching -- **Secure** - No debug tools, proper permissions -- **Health checks** - Automatic container monitoring -- **Single container** - FrankenPHP serves everything - -## ๐Ÿ”จ Make Commands Reference - -```bash -# Development -make dev # Start development environment -make dev-build # Build and start development -make dev-stop # Stop development environment -make dev-clean # Stop and remove volumes (CAUTION) - -# Production -make prod # Start production environment -make prod-build # Build production image -make prod-stop # Stop production environment -make prod-logs # Show production logs - -# Laravel -make artisan cmd="..." # Run artisan command -make composer cmd="..." # Run composer command -make npm cmd="..." # Run npm command -make migrate # Run migrations -make seed # Seed database -make fresh # Fresh migrate and seed -make tinker # Start tinker session -make test # Run tests - -# Utilities -make shell # Enter app container -make db-shell # Enter database shell -make logs # Show app logs -make logs-db # Show database logs -make clear # Clear all caches -make optimize # Optimize for production -``` - -## ๐Ÿ› Troubleshooting - -### Container won't start -```bash -# Check logs -docker compose logs app - -# Rebuild containers -docker compose build --no-cache -docker compose up -d -``` - -### Database connection issues -```bash -# Verify database is running -docker compose ps - -# Check database logs -docker compose logs db - -# Try manual connection -make db-shell -``` - -### Permission issues -```bash -# Fix storage permissions -docker compose exec app chmod -R 777 storage bootstrap/cache -``` - -### Clear all caches -```bash -make clear -# Or manually -docker compose exec app php artisan cache:clear -docker compose exec app php artisan config:clear -docker compose exec app php artisan route:clear -docker compose exec app php artisan view:clear -``` - -## ๐Ÿ“„ License - -This project is open-source software licensed under the [MIT license](LICENSE.md). - -## ๐Ÿค Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -## ๐Ÿ“ž Support - -For issues and questions, please use the [GitHub Issues](https://github.com/yourusername/dish-planner/issues) page. - ---- - -Built with โค๏ธ using Laravel and Livewire \ No newline at end of file diff --git a/app/Actions/User/CreateUserAction.php b/app/Actions/User/CreateUserAction.php deleted file mode 100644 index 07e91a9..0000000 --- a/app/Actions/User/CreateUserAction.php +++ /dev/null @@ -1,84 +0,0 @@ - $data['name'], - 'planner_id' => $data['planner_id'], - ]); - - // Create the user - $user = User::create([ - 'name' => $data['name'], - 'planner_id' => $data['planner_id'], - ]); - - if (!$user) { - throw new Exception('User creation returned null'); - } - - Log::info('CreateUserAction: User creation result', [ - 'user_id' => $user->id, - 'name' => $user->name, - 'planner_id' => $user->planner_id, - ]); - - // Verify the user was actually created - $createdUser = User::find($user->id); - if (!$createdUser) { - throw new Exception('User creation did not persist to database'); - } - - if ($createdUser->name !== $data['name']) { - throw new Exception('User creation data mismatch'); - } - - DB::commit(); - - Log::info('CreateUserAction: User successfully created', [ - 'user_id' => $user->id, - 'name' => $user->name, - 'planner_id' => $user->planner_id, - ]); - - return $user; - - } catch (Exception $e) { - DB::rollBack(); - - Log::error('CreateUserAction: User creation failed', [ - 'name' => $data['name'] ?? 'N/A', - 'planner_id' => $data['planner_id'] ?? 'N/A', - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - - throw $e; - } - } -} diff --git a/app/Actions/User/DeleteUserAction.php b/app/Actions/User/DeleteUserAction.php deleted file mode 100644 index 40a5b09..0000000 --- a/app/Actions/User/DeleteUserAction.php +++ /dev/null @@ -1,79 +0,0 @@ - $user->id, - 'user_name' => $user->name, - 'planner_id' => $user->planner_id, - ]); - - // Check for related data - $userDishCount = $user->userDishes()->count(); - $dishCount = $user->dishes()->count(); - - Log::info('DeleteUserAction: User relationship counts', [ - 'user_id' => $user->id, - 'user_dishes_count' => $userDishCount, - 'dishes_count' => $dishCount, - ]); - - // Store user info before deletion for verification - $userId = $user->id; - $userName = $user->name; - - // Delete the user (cascading deletes should handle related records) - $result = $user->delete(); - - Log::info('DeleteUserAction: Delete result', [ - 'result' => $result, - 'user_id' => $userId, - ]); - - if (! $result) { - throw new Exception('User deletion returned false'); - } - - // Verify the deletion actually happened - $stillExists = User::find($userId); - if ($stillExists) { - throw new Exception('User deletion did not persist to database'); - } - - DB::commit(); - - Log::info('DeleteUserAction: User successfully deleted', [ - 'user_id' => $userId, - 'user_name' => $userName, - ]); - - return true; - - } catch (Exception $e) { - DB::rollBack(); - - Log::error('DeleteUserAction: User deletion failed', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - - throw $e; - } - } -} diff --git a/app/Actions/User/EditUserAction.php b/app/Actions/User/EditUserAction.php deleted file mode 100644 index ccb931a..0000000 --- a/app/Actions/User/EditUserAction.php +++ /dev/null @@ -1,63 +0,0 @@ - $user->id, - 'old_name' => $user->name, - 'new_name' => $data['name'], - 'planner_id' => $user->planner_id, - ]); - - $result = $user->update([ - 'name' => $data['name'], - ]); - - Log::info('EditUserAction: Update result', [ - 'result' => $result, - 'user_id' => $user->id, - ]); - - if (!$result) { - throw new \Exception('User update returned false'); - } - - // Verify the update actually happened - $user->refresh(); - if ($user->name !== $data['name']) { - throw new \Exception('User update did not persist to database'); - } - - DB::commit(); - - Log::info('EditUserAction: User successfully updated', [ - 'user_id' => $user->id, - 'updated_name' => $user->name, - ]); - - return true; - - } catch (\Exception $e) { - DB::rollBack(); - - Log::error('EditUserAction: User update failed', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - - throw $e; - } - } -} \ No newline at end of file diff --git a/app/Enums/AppModeEnum.php b/app/Enums/AppModeEnum.php deleted file mode 100644 index 9a75881..0000000 --- a/app/Enums/AppModeEnum.php +++ /dev/null @@ -1,24 +0,0 @@ -validate([ - 'email' => ['required', 'email'], - 'password' => ['required'], - ]); - - if (Auth::attempt($credentials, $request->boolean('remember'))) { - $request->session()->regenerate(); - - return redirect()->intended(route('dashboard')); - } - - throw ValidationException::withMessages([ - 'email' => 'These credentials do not match our records.', - ]); - } - - public function logout(Request $request) - { - Auth::logout(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return redirect('/'); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php deleted file mode 100644 index 8a4c546..0000000 --- a/app/Http/Controllers/Auth/RegisterController.php +++ /dev/null @@ -1,37 +0,0 @@ -validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:planners'], - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $user = Planner::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => Hash::make($request->password), - ]); - - Auth::login($user); - - return redirect(route('dashboard')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php deleted file mode 100644 index 24d0dc9..0000000 --- a/app/Http/Controllers/SubscriptionController.php +++ /dev/null @@ -1,112 +0,0 @@ -user(); - - if ($planner->subscribed()) { - return redirect()->route('dashboard'); - } - - $plan = $request->input('plan', 'monthly'); - $priceId = $plan === 'yearly' - ? config('services.stripe.price_yearly') - : config('services.stripe.price_monthly'); - - return $planner->newSubscription('default', $priceId) - ->checkout([ - 'success_url' => route('subscription.success') . '?session_id={CHECKOUT_SESSION_ID}', - 'cancel_url' => route('subscription.index'), - ]); - } - - public function success(Request $request): RedirectResponse - { - $sessionId = $request->query('session_id'); - - if ($sessionId) { - $planner = $request->user(); - $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId, [ - 'expand' => ['subscription'], - ]); - - if ($session->subscription && ! $planner->subscribed()) { - $subscription = $session->subscription; - - $planner->subscriptions()->create([ - 'type' => 'default', - 'stripe_id' => $subscription->id, - 'stripe_status' => $subscription->status, - 'stripe_price' => $subscription->items->data[0]->price->id ?? null, - 'quantity' => $subscription->items->data[0]->quantity ?? 1, - 'trial_ends_at' => $subscription->trial_end ? now()->setTimestamp($subscription->trial_end) : null, - 'ends_at' => null, - ]); - } - } - - return redirect()->route('dashboard')->with('success', 'Subscription activated!'); - } - - public function billing(Request $request) - { - $planner = $request->user(); - $subscription = $planner->subscription(); - - if (! $subscription) { - return redirect()->route('subscription.index'); - } - - $planType = match ($subscription->stripe_price) { - config('services.stripe.price_yearly') => 'Yearly', - config('services.stripe.price_monthly') => 'Monthly', - default => 'Unknown', - }; - - $nextBillingDate = null; - if ($subscription->stripe_status === 'active') { - try { - $stripeSubscription = Cashier::stripe()->subscriptions->retrieve($subscription->stripe_id); - $nextBillingDate = $stripeSubscription->current_period_end - ? now()->setTimestamp($stripeSubscription->current_period_end) - : null; - } catch (\Exception $e) { - // Stripe API error - continue without next billing date - } - } - - return view('billing.index', [ - 'subscription' => $subscription, - 'planner' => $planner, - 'planType' => $planType, - 'nextBillingDate' => $nextBillingDate, - ]); - } - - public function cancel(Request $request): RedirectResponse - { - $planner = $request->user(); - - if (! $planner->subscribed()) { - return back()->with('error', 'No active subscription found.'); - } - - $planner->subscription()->cancel(); - - return back()->with('success', 'Subscription canceled. Access will continue until the end of your billing period.'); - } - - public function billingPortal(Request $request) - { - return $request->user()->redirectToBillingPortal(route('billing')); - } -} diff --git a/app/Http/Middleware/ForceJsonResponse.php b/app/Http/Middleware/ForceJsonResponse.php deleted file mode 100644 index 0de547b..0000000 --- a/app/Http/Middleware/ForceJsonResponse.php +++ /dev/null @@ -1,20 +0,0 @@ -is('api/*')) { - $request->headers->set('Accept', 'application/json'); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/RequireSubscription.php b/app/Http/Middleware/RequireSubscription.php deleted file mode 100644 index c07b1d8..0000000 --- a/app/Http/Middleware/RequireSubscription.php +++ /dev/null @@ -1,25 +0,0 @@ -user(); - - if (! $planner?->subscribed()) { - return redirect()->route('subscription.index'); - } - - return $next($request); - } -} diff --git a/app/Livewire/Dishes/DishesList.php b/app/Livewire/Dishes/DishesList.php deleted file mode 100644 index ffe8049..0000000 --- a/app/Livewire/Dishes/DishesList.php +++ /dev/null @@ -1,132 +0,0 @@ - 'required|string|max:255', - 'selectedUsers' => 'array', - ]; - - public function render() - { - $dishes = Dish::with('users') - ->orderBy('name') - ->paginate(10); - - $users = User::where('planner_id', auth()->id()) - ->orderBy('name') - ->get(); - - return view('livewire.dishes.dishes-list', [ - 'dishes' => $dishes, - 'users' => $users - ]); - } - - public function create() - { - $this->reset(['name', 'selectedUsers']); - $this->resetValidation(); - $this->showCreateModal = true; - } - - public function store() - { - $this->validate(); - - $dish = Dish::create([ - 'name' => $this->name, - 'planner_id' => auth()->id(), - ]); - - // Attach selected users - if (!empty($this->selectedUsers)) { - $dish->users()->attach($this->selectedUsers); - } - - $this->showCreateModal = false; - $this->reset(['name', 'selectedUsers']); - - session()->flash('success', 'Dish created successfully.'); - } - - public function edit(Dish $dish) - { - $this->editingDish = $dish; - $this->name = $dish->name; - $this->selectedUsers = $dish->users->pluck('id')->toArray(); - $this->resetValidation(); - $this->showEditModal = true; - } - - public function update() - { - $this->validate(); - - $this->editingDish->update([ - 'name' => $this->name, - ]); - - // Sync users - $this->editingDish->users()->sync($this->selectedUsers); - - $this->showEditModal = false; - $this->reset(['name', 'selectedUsers', 'editingDish']); - - session()->flash('success', 'Dish updated successfully.'); - } - - public function confirmDelete(Dish $dish) - { - $this->deletingDish = $dish; - $this->showDeleteModal = true; - } - - public function delete() - { - $this->deletingDish->users()->detach(); - $this->deletingDish->delete(); - $this->showDeleteModal = false; - $this->deletingDish = null; - - session()->flash('success', 'Dish deleted successfully.'); - } - - public function cancel() - { - $this->showCreateModal = false; - $this->showEditModal = false; - $this->showDeleteModal = false; - $this->reset(['name', 'selectedUsers', 'editingDish', 'deletingDish']); - } - - public function toggleAllUsers(): void - { - $users = User::where('planner_id', auth()->id())->get(); - if (count($this->selectedUsers) === $users->count()) { - $this->selectedUsers = []; - } else { - $this->selectedUsers = $users->pluck('id')->map(fn($id) => (string) $id)->toArray(); - } - } -} \ No newline at end of file diff --git a/app/Livewire/Schedule/ScheduleCalendar.php b/app/Livewire/Schedule/ScheduleCalendar.php deleted file mode 100644 index 9d2a911..0000000 --- a/app/Livewire/Schedule/ScheduleCalendar.php +++ /dev/null @@ -1,433 +0,0 @@ -currentMonth = now()->month; - $this->currentYear = now()->year; - $this->loadCalendar(); - } - - protected $listeners = ['schedule-generated' => 'refreshCalendar']; - - public function render(): View - { - return view('livewire.schedule.schedule-calendar'); - } - - public function refreshCalendar(): void - { - $this->loadCalendar(); - } - - public function loadCalendar(): void - { - $service = new ScheduleCalendarService(); - $this->calendarDays = $service->getCalendarDays( - auth()->user(), - $this->currentMonth, - $this->currentYear - ); - } - - public function previousMonth(): void - { - if ($this->currentMonth === 1) { - $this->currentMonth = 12; - $this->currentYear--; - } else { - $this->currentMonth--; - } - $this->loadCalendar(); - } - - public function nextMonth(): void - { - if ($this->currentMonth === 12) { - $this->currentMonth = 1; - $this->currentYear++; - } else { - $this->currentMonth++; - } - $this->loadCalendar(); - } - - public function regenerateForUserDate($date, $userId): void - { - if (!$this->authorizeUser($userId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - $this->regenerateDate = $date; - $this->regenerateUserId = $userId; - $this->showRegenerateModal = true; - } - - public function confirmRegenerate(): void - { - try { - if (!$this->authorizeUser($this->regenerateUserId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - $action = new DeleteScheduledUserDishForDateAction(); - $action->execute( - auth()->user(), - Carbon::parse($this->regenerateDate), - $this->regenerateUserId - ); - - $this->showRegenerateModal = false; - $this->loadCalendar(); - - session()->flash('success', 'Schedule regenerated for the selected date!'); - } catch (Exception $e) { - Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $this->regenerateDate]); - session()->flash('error', 'Unable to regenerate schedule. Please try again.'); - } - } - - public function skipDay($date, $userId): void - { - try { - if (!$this->authorizeUser($userId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - $action = new SkipScheduledUserDishForDateAction(); - $action->execute( - auth()->user(), - Carbon::parse($date), - $userId - ); - - $this->loadCalendar(); - - session()->flash('success', 'Day skipped successfully!'); - } catch (Exception $e) { - Log::error('Skip day failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]); - session()->flash('error', 'Unable to skip day. Please try again.'); - } - } - - private function authorizeUser(int $userId): bool - { - $user = User::find($userId); - return $user && $user->planner_id === auth()->id(); - } - - public function cancel(): void - { - $this->showRegenerateModal = false; - $this->regenerateDate = null; - $this->regenerateUserId = null; - $this->showEditDishModal = false; - $this->editDate = null; - $this->editUserId = null; - $this->selectedDishId = null; - $this->availableDishes = []; - $this->showAddDishModal = false; - $this->addDate = null; - $this->addUserIds = []; - $this->addSelectedDishId = null; - $this->addAvailableUsers = []; - $this->addAvailableDishes = []; - } - - public function removeDish($date, $userId): void - { - try { - if (!$this->authorizeUser($userId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - $schedule = Schedule::where('planner_id', auth()->id()) - ->where('date', $date) - ->first(); - - if ($schedule) { - ScheduledUserDish::where('schedule_id', $schedule->id) - ->where('user_id', $userId) - ->delete(); - } - - $this->loadCalendar(); - session()->flash('success', 'Dish removed successfully!'); - } catch (Exception $e) { - Log::error('Remove dish failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]); - session()->flash('error', 'Unable to remove dish. Please try again.'); - } - } - - public function openAddDishModal($date): void - { - $this->addDate = $date; - - // Load all users for this planner - $this->addAvailableUsers = User::where('planner_id', auth()->id()) - ->orderBy('name') - ->get(); - - $this->addAvailableDishes = []; - $this->addUserIds = []; - $this->addSelectedDishId = null; - - $this->showAddDishModal = true; - } - - public function toggleAllUsers(): void - { - if (count($this->addUserIds) === count($this->addAvailableUsers)) { - $this->addUserIds = []; - } else { - $this->addUserIds = $this->addAvailableUsers->pluck('id')->map(fn($id) => (string) $id)->toArray(); - } - $this->updateAvailableDishes(); - } - - public function updatedAddUserIds(): void - { - $this->updateAvailableDishes(); - } - - private function updateAvailableDishes(): void - { - if (empty($this->addUserIds)) { - $this->addAvailableDishes = []; - } else { - // Load dishes that ALL selected users have in common - $selectedCount = count($this->addUserIds); - $this->addAvailableDishes = Dish::whereHas('users', function ($query) { - $query->whereIn('users.id', $this->addUserIds); - }, '=', $selectedCount)->orderBy('name')->get(); - } - $this->addSelectedDishId = null; - } - - public function saveAddDish(): void - { - try { - if (empty($this->addUserIds)) { - session()->flash('error', 'Please select at least one user.'); - return; - } - - if (!$this->addSelectedDishId) { - session()->flash('error', 'Please select a dish.'); - return; - } - - // Find or create the schedule for this date - $schedule = Schedule::firstOrCreate( - [ - 'planner_id' => auth()->id(), - 'date' => $this->addDate, - ], - ['is_skipped' => false] - ); - - $addedCount = 0; - $skippedCount = 0; - - foreach ($this->addUserIds as $userId) { - if (!$this->authorizeUser((int) $userId)) { - $skippedCount++; - continue; - } - - // Check if user already has a dish scheduled for this date - $existing = ScheduledUserDish::where('schedule_id', $schedule->id) - ->where('user_id', $userId) - ->first(); - - if ($existing) { - $skippedCount++; - continue; - } - - // Find the UserDish for this user and dish - $userDish = UserDish::where('user_id', $userId) - ->where('dish_id', $this->addSelectedDishId) - ->first(); - - if (!$userDish) { - $skippedCount++; - continue; - } - - // Create the scheduled user dish - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $userId, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - $addedCount++; - } - - $this->closeAddDishModal(); - $this->loadCalendar(); - - if ($addedCount > 0 && $skippedCount > 0) { - session()->flash('success', "Dish added for {$addedCount} user(s). {$skippedCount} user(s) skipped (already scheduled)."); - } elseif ($addedCount > 0) { - session()->flash('success', "Dish added for {$addedCount} user(s)!"); - } else { - session()->flash('error', 'No users could be scheduled. They may already have dishes for this date.'); - } - } catch (Exception $e) { - Log::error('Add dish failed', ['exception' => $e]); - session()->flash('error', 'Unable to add dish. Please try again.'); - } - } - - private function closeAddDishModal(): void - { - $this->showAddDishModal = false; - $this->addDate = null; - $this->addUserIds = []; - $this->addSelectedDishId = null; - $this->addAvailableUsers = []; - $this->addAvailableDishes = []; - } - - public function editDish($date, $userId): void - { - if (!$this->authorizeUser($userId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - $this->editDate = $date; - $this->editUserId = $userId; - - // Load dishes available for this user (via UserDish pivot) - $this->availableDishes = Dish::whereHas('users', function ($query) use ($userId) { - $query->where('users.id', $userId); - })->orderBy('name')->get(); - - // Get currently selected dish for this date/user if exists - $schedule = Schedule::where('planner_id', auth()->id()) - ->where('date', $date) - ->first(); - - if ($schedule) { - $scheduledUserDish = ScheduledUserDish::where('schedule_id', $schedule->id) - ->where('user_id', $userId) - ->first(); - - if ($scheduledUserDish && $scheduledUserDish->userDish) { - $this->selectedDishId = $scheduledUserDish->userDish->dish_id; - } - } - - $this->showEditDishModal = true; - } - - public function saveDish(): void - { - try { - if (!$this->authorizeUser($this->editUserId)) { - session()->flash('error', 'Unauthorized action.'); - return; - } - - if (!$this->selectedDishId) { - session()->flash('error', 'Please select a dish.'); - return; - } - - // Find or create the schedule for this date - $schedule = Schedule::firstOrCreate( - [ - 'planner_id' => auth()->id(), - 'date' => $this->editDate, - ], - ['is_skipped' => false] - ); - - // Find the UserDish for this user and dish - $userDish = UserDish::where('user_id', $this->editUserId) - ->where('dish_id', $this->selectedDishId) - ->first(); - - if (!$userDish) { - session()->flash('error', 'This dish is not assigned to this user.'); - return; - } - - // Update or create the scheduled user dish - ScheduledUserDish::updateOrCreate( - [ - 'schedule_id' => $schedule->id, - 'user_id' => $this->editUserId, - ], - [ - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ] - ); - - $this->showEditDishModal = false; - $this->editDate = null; - $this->editUserId = null; - $this->selectedDishId = null; - $this->availableDishes = []; - - $this->loadCalendar(); - session()->flash('success', 'Dish updated successfully!'); - } catch (Exception $e) { - Log::error('Save dish failed', ['exception' => $e]); - session()->flash('error', 'Unable to save dish. Please try again.'); - } - } - - public function getMonthNameProperty(): string - { - $service = new ScheduleCalendarService(); - return $service->getMonthName($this->currentMonth, $this->currentYear); - } -} diff --git a/app/Livewire/Schedule/ScheduleGenerator.php b/app/Livewire/Schedule/ScheduleGenerator.php deleted file mode 100644 index a2f3b71..0000000 --- a/app/Livewire/Schedule/ScheduleGenerator.php +++ /dev/null @@ -1,141 +0,0 @@ -selectedMonth = now()->month; - $this->selectedYear = now()->year; - - $this->selectedUsers = User::where('planner_id', auth()->id()) - ->pluck('id') - ->toArray(); - } - - public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View - { - $users = User::where('planner_id', auth()->id()) - ->orderBy('name') - ->get(); - - $years = range(now()->year - self::YEARS_IN_PAST, now()->year + self::YEARS_IN_FUTURE); - - return view('livewire.schedule.schedule-generator', [ - 'users' => $users, - 'months' => $this->getMonthNames(), - 'years' => $years - ]); - } - - public function generate(): void - { - $this->validate([ - 'selectedUsers' => 'required|array|min:1', - 'selectedMonth' => 'required|integer|min:1|max:12', - 'selectedYear' => 'required|integer|min:' . (now()->year - self::YEARS_IN_PAST) . '|max:' . (now()->year + self::YEARS_IN_FUTURE), - ]); - - $this->isGenerating = true; - - try { - $action = new GenerateScheduleForMonthAction(); - $action->execute( - auth()->user(), - $this->selectedMonth, - $this->selectedYear, - $this->selectedUsers, - $this->clearExisting - ); - - $this->isGenerating = false; - $this->dispatch('schedule-generated'); - - session()->flash('success', 'Schedule generated successfully for ' . - $this->getSelectedMonthName() . ' ' . $this->selectedYear); - - } catch (\Exception $e) { - $this->isGenerating = false; - Log::error('Schedule generation failed', ['exception' => $e]); - session()->flash('error', 'Unable to generate schedule. Please try again.'); - } - } - - public function regenerateForDate($date): void - { - try { - $action = new RegenerateScheduleForDateForUsersAction(); - $action->execute( - auth()->user(), - Carbon::parse($date), - $this->selectedUsers - ); - - $this->dispatch('schedule-generated'); - session()->flash('success', 'Schedule regenerated for ' . Carbon::parse($date)->format('M d, Y')); - - } catch (\Exception $e) { - Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $date]); - session()->flash('error', 'Unable to regenerate schedule. Please try again.'); - } - } - - public function clearMonth(): void - { - try { - $action = new ClearScheduleForMonthAction(); - $action->execute( - auth()->user(), - $this->selectedMonth, - $this->selectedYear, - $this->selectedUsers - ); - - $this->dispatch('schedule-generated'); - session()->flash('success', 'Schedule cleared for ' . - $this->getSelectedMonthName() . ' ' . $this->selectedYear); - - } catch (\Exception $e) { - Log::error('Clear month failed', ['exception' => $e]); - session()->flash('error', 'Unable to clear schedule. Please try again.'); - } - } - - public function toggleAdvancedOptions() - { - $this->showAdvancedOptions = !$this->showAdvancedOptions; - } - - private function getMonthNames(): array - { - return [ - 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', - 5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August', - 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December' - ]; - } - - private function getSelectedMonthName(): string - { - return $this->getMonthNames()[$this->selectedMonth]; - } -} diff --git a/app/Livewire/Users/UsersList.php b/app/Livewire/Users/UsersList.php deleted file mode 100644 index e9b3762..0000000 --- a/app/Livewire/Users/UsersList.php +++ /dev/null @@ -1,126 +0,0 @@ - 'required|string|max:255', - ]; - - public function render(): View - { - $users = User::where('planner_id', auth()->id()) - ->orderBy('name') - ->paginate(10); - - return view('livewire.users.users-list', [ - 'users' => $users - ]); - } - - public function create(): void - { - $this->reset(['name']); - $this->resetValidation(); - $this->showCreateModal = true; - } - - public function store(): void - { - $this->validate(); - - try { - (new CreateUserAction())->execute([ - 'name' => $this->name, - 'planner_id' => auth()->id(), - ]); - - $this->showCreateModal = false; - $this->reset(['name']); - - session()->flash('success', 'User created successfully.'); - } catch (Exception $e) { - session()->flash('error', 'Failed to create user: ' . $e->getMessage()); - } - } - - public function edit(User $user): void - { - $this->editingUser = $user; - $this->name = $user->name; - $this->resetValidation(); - $this->showEditModal = true; - } - - public function update(): void - { - $this->validate(); - - try { - (new EditUserAction())->execute($this->editingUser, ['name' => $this->name]); - - $this->showEditModal = false; - $this->reset(['name', 'editingUser']); - - session()->flash('success', 'User updated successfully.'); - - // Force component to re-render with fresh data - $this->resetPage(); - } catch (Exception $e) { - session()->flash('error', 'Failed to update user: ' . $e->getMessage()); - } - } - - public function confirmDelete(User $user): void - { - $this->deletingUser = $user; - $this->showDeleteModal = true; - } - - public function delete(): void - { - try { - (new DeleteUserAction())->execute($this->deletingUser); - - $this->showDeleteModal = false; - $this->deletingUser = null; - - session()->flash('success', 'User deleted successfully.'); - - // Force component to re-render with fresh data - $this->resetPage(); - } catch (Exception $e) { - session()->flash('error', 'Failed to delete user: ' . $e->getMessage()); - } - } - - public function cancel(): void - { - $this->showCreateModal = false; - $this->showEditModal = false; - $this->showDeleteModal = false; - $this->reset(['name', 'editingUser', 'deletingUser']); - } -} diff --git a/app/helpers.php b/app/helpers.php deleted file mode 100644 index 59bc43a..0000000 --- a/app/helpers.php +++ /dev/null @@ -1,17 +0,0 @@ -isApp(); - } -} - -if (! function_exists('is_mode_saas')) { - function is_mode_saas(): bool - { - return AppModeEnum::current()->isSaas(); - } -} \ No newline at end of file diff --git a/.editorconfig b/backend/.editorconfig similarity index 100% rename from .editorconfig rename to backend/.editorconfig diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..11024ef --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,71 @@ +APP_NAME=DishPlanner +APP_ENV=local +APP_KEY=base64:Z3WnYIG9I6xxft15P1EO31WHinj1R36eM/iN3ouyFBM= +APP_DEBUG=true +APP_TIMEZONE=UTC +APP_URL=http://localhost:8000 + +SANCTUM_STATEFUL_DOMAINS=localhost:3000 +SESSION_DOMAIN=localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +WWWGROUP=1000 + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=dishplanner +DB_USERNAME=dpuser +DB_PASSWORD=dppass + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=file +CACHE_PREFIX=dishplanner + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/backend/.gitattributes similarity index 100% rename from .gitattributes rename to backend/.gitattributes diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..be5cd4b --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,24 @@ +/composer.lock +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1b43073 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,35 @@ +# Use official PHP base with required extensions +FROM php:8.2-fpm + +# Install system dependencies & PHP extensions +RUN apt-get update && apt-get install -y \ + git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev zip \ + && docker-php-ext-install pdo pdo_mysql zip mbstring exif pcntl bcmath + +# Install Composer globally +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working dir +WORKDIR /var/www + +# Copy app files +COPY . . + +# Install PHP dependencies +RUN composer install --no-dev --optimize-autoloader + +# Laravel optimizations +RUN php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache + +# Set correct permissions +RUN chown -R www-data:www-data /var/www \ + && chmod -R 755 /var/www/storage + +USER www-data + +# Expose port 9000 (default for php-fpm) +EXPOSE 9000 + +CMD ["php-fpm"] diff --git a/LICENSE.md b/backend/LICENSE.md similarity index 100% rename from LICENSE.md rename to backend/LICENSE.md diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..f768271 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,14 @@ +# Dish Planner + +Plan your future dishes + +## Development + +### Installation + +This project uses Laravel Sail, so install all composer packages, get your docker up, and run + +```shell +./vendor/bin/sail up -d +``` + diff --git a/app/Console/Commands/GenerateSchedulesCommand.php b/backend/app/Console/Commands/GenerateSchedulesCommand.php similarity index 100% rename from app/Console/Commands/GenerateSchedulesCommand.php rename to backend/app/Console/Commands/GenerateSchedulesCommand.php diff --git a/app/Exceptions/CustomException.php b/backend/app/Exceptions/CustomException.php similarity index 100% rename from app/Exceptions/CustomException.php rename to backend/app/Exceptions/CustomException.php diff --git a/app/Http/Controllers/Api/ApiController.php b/backend/app/Http/Controllers/Api/ApiController.php similarity index 100% rename from app/Http/Controllers/Api/ApiController.php rename to backend/app/Http/Controllers/Api/ApiController.php diff --git a/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php similarity index 100% rename from app/Http/Controllers/Controller.php rename to backend/app/Http/Controllers/Controller.php diff --git a/app/Http/Middleware/RequireSaasMode.php b/backend/app/Http/Middleware/ForceJsonResponse.php similarity index 73% rename from app/Http/Middleware/RequireSaasMode.php rename to backend/app/Http/Middleware/ForceJsonResponse.php index 0950eb6..753249e 100644 --- a/app/Http/Middleware/RequireSaasMode.php +++ b/backend/app/Http/Middleware/ForceJsonResponse.php @@ -6,14 +6,12 @@ use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; -class RequireSaasMode +class ForceJsonResponse { public function handle(Request $request, Closure $next): Response { - if (! is_mode_saas()) { - abort(404); - } + $request->headers->set('Accept', 'application/json'); return $next($request); } -} \ No newline at end of file +} diff --git a/app/Http/Resources/MinimalScheduleResource.php b/backend/app/Http/Resources/MinimalScheduleResource.php similarity index 100% rename from app/Http/Resources/MinimalScheduleResource.php rename to backend/app/Http/Resources/MinimalScheduleResource.php diff --git a/app/Http/Resources/MinimalScheduledUserDishResource.php b/backend/app/Http/Resources/MinimalScheduledUserDishResource.php similarity index 100% rename from app/Http/Resources/MinimalScheduledUserDishResource.php rename to backend/app/Http/Resources/MinimalScheduledUserDishResource.php diff --git a/app/Http/Resources/ScheduledUserDishResource.php b/backend/app/Http/Resources/ScheduledUserDishResource.php similarity index 100% rename from app/Http/Resources/ScheduledUserDishResource.php rename to backend/app/Http/Resources/ScheduledUserDishResource.php diff --git a/app/Http/Resources/UserDishResource.php b/backend/app/Http/Resources/UserDishResource.php similarity index 100% rename from app/Http/Resources/UserDishResource.php rename to backend/app/Http/Resources/UserDishResource.php diff --git a/app/Http/Resources/UserResource.php b/backend/app/Http/Resources/UserResource.php similarity index 100% rename from app/Http/Resources/UserResource.php rename to backend/app/Http/Resources/UserResource.php diff --git a/app/Http/Resources/UserWithDishesResource.php b/backend/app/Http/Resources/UserWithDishesResource.php similarity index 100% rename from app/Http/Resources/UserWithDishesResource.php rename to backend/app/Http/Resources/UserWithDishesResource.php diff --git a/app/Http/Resources/UserWithUserDishesResource.php b/backend/app/Http/Resources/UserWithUserDishesResource.php similarity index 100% rename from app/Http/Resources/UserWithUserDishesResource.php rename to backend/app/Http/Resources/UserWithUserDishesResource.php diff --git a/app/Models/Dish.php b/backend/app/Models/Dish.php similarity index 100% rename from app/Models/Dish.php rename to backend/app/Models/Dish.php diff --git a/app/Models/MinimumRecurrence.php b/backend/app/Models/MinimumRecurrence.php similarity index 100% rename from app/Models/MinimumRecurrence.php rename to backend/app/Models/MinimumRecurrence.php diff --git a/app/Models/Planner.php b/backend/app/Models/Planner.php similarity index 78% rename from app/Models/Planner.php rename to backend/app/Models/Planner.php index f8307fd..156f26c 100644 --- a/app/Models/Planner.php +++ b/backend/app/Models/Planner.php @@ -6,17 +6,15 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Cashier\Billable; use Laravel\Sanctum\HasApiTokens; /** * @property int $id * @property static PlannerFactory factory($count = null, $state = []) - * @method static first() */ class Planner extends Authenticatable { - use Billable, HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; protected $fillable = [ 'name', 'email', 'password', @@ -26,10 +24,6 @@ class Planner extends Authenticatable 'password', 'remember_token', ]; - protected $casts = [ - 'password' => 'hashed', - ]; - public function schedules(): HasMany { return $this->hasMany(Schedule::class); diff --git a/app/Models/Schedule.php b/backend/app/Models/Schedule.php similarity index 90% rename from app/Models/Schedule.php rename to backend/app/Models/Schedule.php index d1e95e6..a055479 100644 --- a/app/Models/Schedule.php +++ b/backend/app/Models/Schedule.php @@ -27,7 +27,6 @@ * @method static create(array $array) * @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and') * @method static ScheduleFactory factory($count = null, $state = []) - * @method static firstOrCreate(array $array, false[] $array1) */ class Schedule extends Model { @@ -57,6 +56,6 @@ public function scheduledUserDishes(): HasMany public function hasAllUsersScheduled(): bool { - return $this->scheduledUserDishes->count() === User::where('planner_id', $this->planner_id)->count(); + return $this->scheduledUserDishes->count() === User::all()->count(); } } diff --git a/app/Models/ScheduledUserDish.php b/backend/app/Models/ScheduledUserDish.php similarity index 84% rename from app/Models/ScheduledUserDish.php rename to backend/app/Models/ScheduledUserDish.php index eb54251..0bff736 100644 --- a/app/Models/ScheduledUserDish.php +++ b/backend/app/Models/ScheduledUserDish.php @@ -12,25 +12,17 @@ * @property int $id * @property int $schedule_id * @property Schedule $schedule - * @property int $user_id - * @property User $user * @property int $user_dish_id * @property UserDish $userDish * @property bool $is_skipped * @method static create(array $array) * @method static ScheduledUserDishFactory factory($count = null, $state = []) - * @method static firstOrCreate(array $array, array $array1) */ class ScheduledUserDish extends Model { use HasFactory; - protected $fillable = [ - 'schedule_id', - 'user_id', - 'user_dish_id', - 'is_skipped' - ]; + protected $fillable = ['schedule_id', 'user_id', 'user_dish_id', 'is_skipped']; protected $casts = [ 'is_skipped' => 'boolean', diff --git a/app/Models/Scopes/BelongsToPlanner.php b/backend/app/Models/Scopes/BelongsToPlanner.php similarity index 100% rename from app/Models/Scopes/BelongsToPlanner.php rename to backend/app/Models/Scopes/BelongsToPlanner.php diff --git a/app/Models/User.php b/backend/app/Models/User.php similarity index 74% rename from app/Models/User.php rename to backend/app/Models/User.php index 6639c90..9675e87 100644 --- a/app/Models/User.php +++ b/backend/app/Models/User.php @@ -9,7 +9,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Notifications\Notifiable; /** * @property int $id @@ -19,17 +20,22 @@ * @property Collection $userDishes * @method static User findOrFail(int $user_id) * @method static UserFactory factory($count = null, $state = []) - * @method static create(array $array) - * @method static where(string $string, int|string|null $id) */ -class User extends Model +class User extends Authenticatable { /** @use HasFactory */ - use HasFactory; + use HasFactory, Notifiable; protected $fillable = [ 'planner_id', 'name', + 'email', + 'password', + ]; + + protected $hidden = [ + 'password', + 'remember_token', ]; protected static function booted(): void @@ -37,6 +43,19 @@ protected static function booted(): void static::addGlobalScope(new BelongsToPlanner); } + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + public function dishes(): BelongsToMany { return $this->belongsToMany(Dish::class, 'user_dishes', 'user_id', 'dish_id'); diff --git a/app/Models/UserDish.php b/backend/app/Models/UserDish.php similarity index 100% rename from app/Models/UserDish.php rename to backend/app/Models/UserDish.php diff --git a/app/Models/UserDishRecurrence.php b/backend/app/Models/UserDishRecurrence.php similarity index 100% rename from app/Models/UserDishRecurrence.php rename to backend/app/Models/UserDishRecurrence.php diff --git a/app/Models/WeeklyRecurrence.php b/backend/app/Models/WeeklyRecurrence.php similarity index 100% rename from app/Models/WeeklyRecurrence.php rename to backend/app/Models/WeeklyRecurrence.php diff --git a/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php similarity index 94% rename from app/Providers/AppServiceProvider.php rename to backend/app/Providers/AppServiceProvider.php index 3067e8c..0bbbb1e 100755 --- a/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -4,12 +4,10 @@ use App\Exceptions\CustomException; use App\Models\Dish; -use App\Models\Planner; use App\Models\Schedule; use App\Models\ScheduledUserDish; use App\Models\User; use App\Models\UserDish; -use Laravel\Cashier\Cashier; use DishPlanner\Dish\Policies\DishPolicy; use DishPlanner\Schedule\Policies\SchedulePolicy; use DishPlanner\ScheduledUserDish\Policies\ScheduledUserDishPolicy; @@ -47,8 +45,6 @@ public function render($request, Throwable $e) public function boot(): void { - Cashier::useCustomerModel(Planner::class); - Gate::policy(Dish::class, DishPolicy::class); Gate::policy(Schedule::class, SchedulePolicy::class); Gate::policy(ScheduledUserDish::class, ScheduledUserDishPolicy::class); diff --git a/app/Services/OutputService.php b/backend/app/Services/OutputService.php similarity index 100% rename from app/Services/OutputService.php rename to backend/app/Services/OutputService.php diff --git a/app/WeekdaysEnum.php b/backend/app/WeekdaysEnum.php similarity index 100% rename from app/WeekdaysEnum.php rename to backend/app/WeekdaysEnum.php diff --git a/artisan b/backend/artisan similarity index 100% rename from artisan rename to backend/artisan diff --git a/bin/build_and_push.sh b/backend/bin/build_and_push.sh similarity index 100% rename from bin/build_and_push.sh rename to backend/bin/build_and_push.sh diff --git a/bin/update.sh b/backend/bin/update.sh similarity index 100% rename from bin/update.sh rename to backend/bin/update.sh diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..265ca64 --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,61 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->append(ForceJsonResponse::class); + $middleware->append(StartSession::class); + $middleware->append(HandleCors::class); + }) + ->withExceptions(function (Exceptions $exceptions) { + $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { + if ($request->is('api/*')) { + return true; + } + + return $request->expectsJson(); + }); + + /** @var OutputService $outputService */ + $outputService = resolve(OutputService::class); + + $exceptions->render(fn (ValidationException $e, Request $request) => $outputService + ->response(false, null, [$e->getMessage()], 404) + ); + + $exceptions->render(fn (NotFoundHttpException $e, Request $request) => response()->json( + $outputService->response(false, null, ['MODEL_NOT_FOUND']), + 404 + )); + + $exceptions->render(fn (AccessDeniedHttpException $e, Request $request) => response()->json( + $outputService->response(false, null, [$e->getMessage()]), + 403 + )); + }) + ->withCommands([ + GenerateScheduleCommand::class, + ]) + ->create(); diff --git a/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore old mode 100755 new mode 100644 similarity index 100% rename from bootstrap/cache/.gitignore rename to backend/bootstrap/cache/.gitignore diff --git a/bootstrap/providers.php b/backend/bootstrap/providers.php similarity index 100% rename from bootstrap/providers.php rename to backend/bootstrap/providers.php diff --git a/composer.json b/backend/composer.json similarity index 79% rename from composer.json rename to backend/composer.json index 2ecb17b..f33f99c 100644 --- a/composer.json +++ b/backend/composer.json @@ -10,15 +10,12 @@ "license": "MIT", "require": { "php": "^8.2", - "laravel/cashier": "^16.1", "laravel/framework": "^12.9.2", "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.9", - "livewire/livewire": "^3.7" + "laravel/tinker": "^2.9" }, "require-dev": { "fakerphp/faker": "^1.23", - "laravel/dusk": "^8.3", "laravel/pail": "^1.1", "laravel/pint": "^1.13", "laravel/sail": "^1.26", @@ -27,9 +24,6 @@ "phpunit/phpunit": "^11.0.1" }, "autoload": { - "files": [ - "app/helpers.php" - ], "psr-4": { "App\\": "app/", "DishPlanner\\": "src/DishPlanner/", @@ -61,17 +55,6 @@ "dev": [ "Composer\\Config::disableProcessTimeout", "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" - ], - "test": [ - "@php artisan test" - ], - "test:coverage": [ - "Composer\\Config::disableProcessTimeout", - "@php -d xdebug.mode=coverage artisan test --coverage" - ], - "test:coverage-html": [ - "Composer\\Config::disableProcessTimeout", - "@php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-html coverage --coverage-text" ] }, "extra": { diff --git a/config/app.php b/backend/config/app.php similarity index 90% rename from config/app.php rename to backend/config/app.php index 09dd952..f467267 100644 --- a/config/app.php +++ b/backend/config/app.php @@ -13,7 +13,7 @@ | */ - 'name' => env('APP_NAME', 'Dish Planner'), + 'name' => env('APP_NAME', 'Laravel'), /* |-------------------------------------------------------------------------- @@ -28,18 +28,6 @@ 'env' => env('APP_ENV', 'production'), - /* - |-------------------------------------------------------------------------- - | Application Mode - |-------------------------------------------------------------------------- - | - | Determines the application deployment mode: 'app' for self-hosted, - | 'saas' for multi-tenant SaaS, 'demo' for demonstration instances. - | - */ - - 'mode' => env('APP_MODE', 'app'), - /* |-------------------------------------------------------------------------- | Application Debug Mode diff --git a/config/auth.php b/backend/config/auth.php similarity index 100% rename from config/auth.php rename to backend/config/auth.php diff --git a/config/cache.php b/backend/config/cache.php similarity index 100% rename from config/cache.php rename to backend/config/cache.php diff --git a/config/cors.php b/backend/config/cors.php similarity index 100% rename from config/cors.php rename to backend/config/cors.php diff --git a/config/database.php b/backend/config/database.php similarity index 100% rename from config/database.php rename to backend/config/database.php diff --git a/config/filesystems.php b/backend/config/filesystems.php similarity index 100% rename from config/filesystems.php rename to backend/config/filesystems.php diff --git a/config/logging.php b/backend/config/logging.php similarity index 100% rename from config/logging.php rename to backend/config/logging.php diff --git a/config/mail.php b/backend/config/mail.php similarity index 100% rename from config/mail.php rename to backend/config/mail.php diff --git a/config/queue.php b/backend/config/queue.php similarity index 100% rename from config/queue.php rename to backend/config/queue.php diff --git a/config/sanctum.php b/backend/config/sanctum.php similarity index 100% rename from config/sanctum.php rename to backend/config/sanctum.php diff --git a/config/services.php b/backend/config/services.php similarity index 77% rename from config/services.php rename to backend/config/services.php index cf3ce61..27a3617 100644 --- a/config/services.php +++ b/backend/config/services.php @@ -35,14 +35,4 @@ ], ], - 'stripe' => [ - 'key' => env('STRIPE_KEY'), - 'secret' => env('STRIPE_SECRET'), - 'webhook' => [ - 'secret' => env('STRIPE_WEBHOOK_SECRET'), - ], - 'price_monthly' => env('STRIPE_PRICE_MONTHLY'), - 'price_yearly' => env('STRIPE_PRICE_YEARLY'), - ], - ]; diff --git a/config/session.php b/backend/config/session.php similarity index 100% rename from config/session.php rename to backend/config/session.php diff --git a/database/.gitignore b/backend/database/.gitignore similarity index 100% rename from database/.gitignore rename to backend/database/.gitignore diff --git a/database/factories/DishFactory.php b/backend/database/factories/DishFactory.php similarity index 100% rename from database/factories/DishFactory.php rename to backend/database/factories/DishFactory.php diff --git a/database/factories/MinimumRecurrenceFactory.php b/backend/database/factories/MinimumRecurrenceFactory.php similarity index 100% rename from database/factories/MinimumRecurrenceFactory.php rename to backend/database/factories/MinimumRecurrenceFactory.php diff --git a/database/factories/PlannerFactory.php b/backend/database/factories/PlannerFactory.php similarity index 100% rename from database/factories/PlannerFactory.php rename to backend/database/factories/PlannerFactory.php diff --git a/database/factories/ScheduleFactory.php b/backend/database/factories/ScheduleFactory.php similarity index 100% rename from database/factories/ScheduleFactory.php rename to backend/database/factories/ScheduleFactory.php diff --git a/database/factories/ScheduledUserDishFactory.php b/backend/database/factories/ScheduledUserDishFactory.php similarity index 100% rename from database/factories/ScheduledUserDishFactory.php rename to backend/database/factories/ScheduledUserDishFactory.php diff --git a/database/factories/UserDishFactory.php b/backend/database/factories/UserDishFactory.php similarity index 100% rename from database/factories/UserDishFactory.php rename to backend/database/factories/UserDishFactory.php diff --git a/database/factories/UserDishRecurrenceFactory.php b/backend/database/factories/UserDishRecurrenceFactory.php similarity index 100% rename from database/factories/UserDishRecurrenceFactory.php rename to backend/database/factories/UserDishRecurrenceFactory.php diff --git a/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php similarity index 100% rename from database/factories/UserFactory.php rename to backend/database/factories/UserFactory.php diff --git a/database/factories/WeeklyRecurrenceFactory.php b/backend/database/factories/WeeklyRecurrenceFactory.php similarity index 100% rename from database/factories/WeeklyRecurrenceFactory.php rename to backend/database/factories/WeeklyRecurrenceFactory.php diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php similarity index 100% rename from database/migrations/0001_01_01_000000_create_users_table.php rename to backend/database/migrations/0001_01_01_000000_create_users_table.php diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php similarity index 100% rename from database/migrations/0001_01_01_000001_create_cache_table.php rename to backend/database/migrations/0001_01_01_000001_create_cache_table.php diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php similarity index 100% rename from database/migrations/0001_01_01_000002_create_jobs_table.php rename to backend/database/migrations/0001_01_01_000002_create_jobs_table.php diff --git a/database/migrations/2025_01_18_004639_create_dishes_table.php b/backend/database/migrations/2025_01_18_004639_create_dishes_table.php similarity index 100% rename from database/migrations/2025_01_18_004639_create_dishes_table.php rename to backend/database/migrations/2025_01_18_004639_create_dishes_table.php diff --git a/database/migrations/2025_02_02_130855_user_dishes.php b/backend/database/migrations/2025_02_02_130855_user_dishes.php similarity index 100% rename from database/migrations/2025_02_02_130855_user_dishes.php rename to backend/database/migrations/2025_02_02_130855_user_dishes.php diff --git a/database/migrations/2025_02_08_231219_create_schedules_table.php b/backend/database/migrations/2025_02_08_231219_create_schedules_table.php similarity index 95% rename from database/migrations/2025_02_08_231219_create_schedules_table.php rename to backend/database/migrations/2025_02_08_231219_create_schedules_table.php index 8f304b4..98dbb15 100755 --- a/database/migrations/2025_02_08_231219_create_schedules_table.php +++ b/backend/database/migrations/2025_02_08_231219_create_schedules_table.php @@ -29,7 +29,7 @@ public function up(): void $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('user_dish_id')->references('id')->on('user_dishes')->onDelete('cascade'); - $table->unique(['schedule_id', 'user_id']); + $table->unique(['schedule_id', 'user_dish_id']); $table->index('user_dish_id'); }); } diff --git a/database/migrations/2025_03_03_204906_recurrence_types.php b/backend/database/migrations/2025_03_03_204906_recurrence_types.php similarity index 100% rename from database/migrations/2025_03_03_204906_recurrence_types.php rename to backend/database/migrations/2025_03_03_204906_recurrence_types.php diff --git a/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php b/backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php similarity index 100% rename from database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php rename to backend/database/migrations/2025_04_19_001446_create_personal_access_tokens_table.php diff --git a/database/migrations/2025_04_19_195152_create_sessions_table.php b/backend/database/migrations/2025_04_19_195152_create_sessions_table.php similarity index 100% rename from database/migrations/2025_04_19_195152_create_sessions_table.php rename to backend/database/migrations/2025_04_19_195152_create_sessions_table.php diff --git a/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php similarity index 100% rename from database/seeders/DatabaseSeeder.php rename to backend/database/seeders/DatabaseSeeder.php diff --git a/backend/database/seeders/DishesSeeder.php b/backend/database/seeders/DishesSeeder.php new file mode 100644 index 0000000..4c91d47 --- /dev/null +++ b/backend/database/seeders/DishesSeeder.php @@ -0,0 +1,33 @@ +first()], + [$users->last()], + [$users->first(), $users->last()], + ]); + + $planner = Planner::all()->first() ?? Planner::factory()->create(); + + collect([ + 'lasagne', 'pizza', 'burger', 'fries', 'salad', 'sushi', 'pancakes', 'ice cream', 'spaghetti', 'mac and cheese', + 'steak', 'chicken', 'beef', 'pork', 'fish', 'chips', 'cake', + ])->map(fn (string $name) => Dish::factory() + ->create([ + 'planner_id' => $planner->id, + 'name' => $name, + ]) + )->each(fn (Dish $dish) => $dish->users()->attach($userOptions->random())); + } +} diff --git a/database/seeders/PlannersSeeder.php b/backend/database/seeders/PlannersSeeder.php similarity index 100% rename from database/seeders/PlannersSeeder.php rename to backend/database/seeders/PlannersSeeder.php diff --git a/database/seeders/ScheduleSeeder.php b/backend/database/seeders/ScheduleSeeder.php similarity index 100% rename from database/seeders/ScheduleSeeder.php rename to backend/database/seeders/ScheduleSeeder.php diff --git a/database/seeders/UsersSeeder.php b/backend/database/seeders/UsersSeeder.php similarity index 100% rename from database/seeders/UsersSeeder.php rename to backend/database/seeders/UsersSeeder.php diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..f296bc9 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,52 @@ +services: + laravel.test: + build: + context: './vendor/laravel/sail/runtimes/8.4' + dockerfile: Dockerfile + args: + WWWGROUP: '${WWWGROUP}' + image: 'sail-8.4/app' + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-80}:80' + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + environment: + WWWUSER: '${WWWUSER}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + IGNITION_LOCAL_SITES_PATH: '${PWD}' + volumes: + - '.:/var/www/html' + networks: + - sail + depends_on: + - mysql + mysql: + image: 'mysql/mysql-server:8.0' + ports: + - '${FORWARD_DB_PORT:-3306}:3306' + environment: + MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_ROOT_HOST: '%' + MYSQL_DATABASE: '${DB_DATABASE}' + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + volumes: + - 'sail-mysql:/var/lib/mysql' + networks: + - sail + healthcheck: + test: + - CMD + - mysqladmin + - ping + - '-p${DB_PASSWORD}' + retries: 3 + timeout: 5s +networks: + sail: + driver: bridge +volumes: + sail-mysql: + driver: local diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..0d10472 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "axios": "^1.7.4", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "vite": "^6.0" + } +} diff --git a/phpunit.xml b/backend/phpunit.xml similarity index 74% rename from phpunit.xml rename to backend/phpunit.xml index 4eee333..24bb646 100644 --- a/phpunit.xml +++ b/backend/phpunit.xml @@ -15,21 +15,8 @@ app - src - - app/Console - app/Exceptions - app/Providers - - - - - - - - diff --git a/postcss.config.js b/backend/postcss.config.js similarity index 100% rename from postcss.config.js rename to backend/postcss.config.js diff --git a/public/.htaccess b/backend/public/.htaccess old mode 100755 new mode 100644 similarity index 100% rename from public/.htaccess rename to backend/public/.htaccess diff --git a/public/favicon.ico b/backend/public/favicon.ico old mode 100755 new mode 100644 similarity index 100% rename from public/favicon.ico rename to backend/public/favicon.ico diff --git a/public/index.php b/backend/public/index.php old mode 100755 new mode 100644 similarity index 100% rename from public/index.php rename to backend/public/index.php diff --git a/public/robots.txt b/backend/public/robots.txt old mode 100755 new mode 100644 similarity index 100% rename from public/robots.txt rename to backend/public/robots.txt diff --git a/backend/resources/css/app.css b/backend/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/backend/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/resources/js/app.js b/backend/resources/js/app.js similarity index 100% rename from resources/js/app.js rename to backend/resources/js/app.js diff --git a/resources/js/bootstrap.js b/backend/resources/js/bootstrap.js similarity index 100% rename from resources/js/bootstrap.js rename to backend/resources/js/bootstrap.js diff --git a/resources/views/welcome.blade.php b/backend/resources/views/welcome.blade.php similarity index 100% rename from resources/views/welcome.blade.php rename to backend/resources/views/welcome.blade.php diff --git a/routes/api.php b/backend/routes/api.php similarity index 100% rename from routes/api.php rename to backend/routes/api.php diff --git a/routes/api/auth.php b/backend/routes/api/auth.php similarity index 100% rename from routes/api/auth.php rename to backend/routes/api/auth.php diff --git a/routes/api/dishes.php b/backend/routes/api/dishes.php similarity index 100% rename from routes/api/dishes.php rename to backend/routes/api/dishes.php diff --git a/routes/api/schedule.php b/backend/routes/api/schedule.php similarity index 100% rename from routes/api/schedule.php rename to backend/routes/api/schedule.php diff --git a/routes/api/scheduledUserDishes.php b/backend/routes/api/scheduledUserDishes.php similarity index 100% rename from routes/api/scheduledUserDishes.php rename to backend/routes/api/scheduledUserDishes.php diff --git a/routes/api/users.php b/backend/routes/api/users.php similarity index 100% rename from routes/api/users.php rename to backend/routes/api/users.php diff --git a/routes/console.php b/backend/routes/console.php similarity index 100% rename from routes/console.php rename to backend/routes/console.php diff --git a/backend/routes/web.php b/backend/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/backend/routes/web.php @@ -0,0 +1,7 @@ +setUpHasPlanner(); - } - public function test_it_syncs_users_to_a_user_dish(): void { $userCount = 4; diff --git a/tests/Feature/Dish/CreateDishTest.php b/backend/tests/Feature/Dish/CreateDishTest.php similarity index 96% rename from tests/Feature/Dish/CreateDishTest.php rename to backend/tests/Feature/Dish/CreateDishTest.php index 8733917..1f551e1 100755 --- a/tests/Feature/Dish/CreateDishTest.php +++ b/backend/tests/Feature/Dish/CreateDishTest.php @@ -16,12 +16,6 @@ class CreateDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_create_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/Dish/DeleteDishTest.php b/backend/tests/Feature/Dish/DeleteDishTest.php similarity index 95% rename from tests/Feature/Dish/DeleteDishTest.php rename to backend/tests/Feature/Dish/DeleteDishTest.php index b773776..efbcd54 100644 --- a/tests/Feature/Dish/DeleteDishTest.php +++ b/backend/tests/Feature/Dish/DeleteDishTest.php @@ -16,12 +16,6 @@ class DeleteDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_delete_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/Dish/ListDishesTest.php b/backend/tests/Feature/Dish/ListDishesTest.php similarity index 94% rename from tests/Feature/Dish/ListDishesTest.php rename to backend/tests/Feature/Dish/ListDishesTest.php index 2ae999c..0f2bff9 100755 --- a/tests/Feature/Dish/ListDishesTest.php +++ b/backend/tests/Feature/Dish/ListDishesTest.php @@ -14,12 +14,6 @@ class ListDishesTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_see_list_of_dishes(): void { $planner = $this->planner; diff --git a/tests/Feature/Dish/RemoveUsersFromDishTest.php b/backend/tests/Feature/Dish/RemoveUsersFromDishTest.php similarity index 96% rename from tests/Feature/Dish/RemoveUsersFromDishTest.php rename to backend/tests/Feature/Dish/RemoveUsersFromDishTest.php index b794a49..d300140 100755 --- a/tests/Feature/Dish/RemoveUsersFromDishTest.php +++ b/backend/tests/Feature/Dish/RemoveUsersFromDishTest.php @@ -16,12 +16,6 @@ class RemoveUsersFromDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_syncs_users_to_a_user_dish(): void { $userCount = 4; diff --git a/tests/Feature/Dish/ShowDishTest.php b/backend/tests/Feature/Dish/ShowDishTest.php similarity index 93% rename from tests/Feature/Dish/ShowDishTest.php rename to backend/tests/Feature/Dish/ShowDishTest.php index 755ccaa..8990f35 100755 --- a/tests/Feature/Dish/ShowDishTest.php +++ b/backend/tests/Feature/Dish/ShowDishTest.php @@ -14,12 +14,6 @@ class ShowDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_see_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/Dish/SyncUsersForDishTest.php b/backend/tests/Feature/Dish/SyncUsersForDishTest.php similarity index 96% rename from tests/Feature/Dish/SyncUsersForDishTest.php rename to backend/tests/Feature/Dish/SyncUsersForDishTest.php index 2eb46b4..69b0224 100755 --- a/tests/Feature/Dish/SyncUsersForDishTest.php +++ b/backend/tests/Feature/Dish/SyncUsersForDishTest.php @@ -16,12 +16,6 @@ class SyncUsersForDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_syncs_users_to_a_user_dish(): void { $userCount = 4; diff --git a/tests/Feature/Dish/UpdateDishTest.php b/backend/tests/Feature/Dish/UpdateDishTest.php similarity index 95% rename from tests/Feature/Dish/UpdateDishTest.php rename to backend/tests/Feature/Dish/UpdateDishTest.php index 468e9c8..907d03e 100755 --- a/tests/Feature/Dish/UpdateDishTest.php +++ b/backend/tests/Feature/Dish/UpdateDishTest.php @@ -14,12 +14,6 @@ class UpdateDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_update_dish(): void { $planner = $this->planner; diff --git a/backend/tests/Feature/PlannerLoginTest.php b/backend/tests/Feature/PlannerLoginTest.php new file mode 100644 index 0000000..eccf3a5 --- /dev/null +++ b/backend/tests/Feature/PlannerLoginTest.php @@ -0,0 +1,98 @@ +create([ + 'email' => 'planner@example.com', + 'password' => Hash::make('secret123'), + ]); + + $response = $this + ->actingAs($planner) + ->post(route('api.auth.login'), [ + 'email' => 'planner@example.com', + 'password' => 'secret123', + ]); + + $response->assertOk(); + $this->assertAuthenticatedAs($planner); + } + + public function test_login_fails_with_invalid_credentials(): void + { + Planner::factory()->create([ + 'email' => 'planner@example.com', + 'password' => Hash::make('secret123'), + ]); + + $response = $this->postJson(route('api.auth.login'), [ + 'email' => 'planner@example.com', + 'password' => 'wrongpassword', + ]); + + $response->assertUnauthorized(); + } + + public function test_a_logged_in_planner_can_log_out(): void + { + $planner = Planner::factory()->create([ + 'password' => Hash::make('secret123'), + ]); + + $this->post(route('api.auth.login'), [ + 'email' => $planner->email, + 'password' => 'secret123', + ]); + + $response = $this->post(route('api.auth.logout')); + + $response->assertOk(); + $this->assertGuest(); // nobody should be logged in after logout + } + + public function test_planner_can_register(): void + { + $schedulesCount = Schedule::all()->count(); + + $response = $this->post(route('api.auth.register'), [ + 'name' => 'High Functioning Planner', + 'email' => 'planner@example.com', + 'password' => 'secret123', + 'password_confirmation' => 'secret123', + ]); + + $response->assertCreated(); + + $this->assertDatabaseHas('planners', [ + 'email' => 'planner@example.com', + ]); + + $this->assertGreaterThan($schedulesCount, Schedule::all()->count()); + } + + public function test_it_returns_the_authenticated_planner(): void + { + $planner = Planner::factory()->create(); + + $this + ->actingAs($planner) + ->get(route('api.auth.me')) + ->assertOk() + ->assertJsonFragment([ + 'email' => $planner->email, + 'name' => $planner->name, + ]); + } +} diff --git a/tests/Feature/Schedule/GenerateScheduleTest.php b/backend/tests/Feature/Schedule/GenerateScheduleTest.php similarity index 98% rename from tests/Feature/Schedule/GenerateScheduleTest.php rename to backend/tests/Feature/Schedule/GenerateScheduleTest.php index 79bd6f6..7ed1888 100644 --- a/tests/Feature/Schedule/GenerateScheduleTest.php +++ b/backend/tests/Feature/Schedule/GenerateScheduleTest.php @@ -21,12 +21,6 @@ class GenerateScheduleTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_generate_schedule(): void { $planner = $this->planner; diff --git a/tests/Feature/Schedule/ListScheduleTest.php b/backend/tests/Feature/Schedule/ListScheduleTest.php similarity index 97% rename from tests/Feature/Schedule/ListScheduleTest.php rename to backend/tests/Feature/Schedule/ListScheduleTest.php index 4f24cb1..be3dc3d 100644 --- a/tests/Feature/Schedule/ListScheduleTest.php +++ b/backend/tests/Feature/Schedule/ListScheduleTest.php @@ -17,12 +17,6 @@ class ListScheduleTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_full_calendar_dishes_list_for_a_given_date_range(): void { $planner = $this->planner; diff --git a/tests/Feature/Schedule/ReadScheduleTest.php b/backend/tests/Feature/Schedule/ReadScheduleTest.php similarity index 97% rename from tests/Feature/Schedule/ReadScheduleTest.php rename to backend/tests/Feature/Schedule/ReadScheduleTest.php index 26343be..3c473b6 100644 --- a/tests/Feature/Schedule/ReadScheduleTest.php +++ b/backend/tests/Feature/Schedule/ReadScheduleTest.php @@ -18,12 +18,6 @@ class ReadScheduleTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_single_day_can_be_read(): void { $planner = $this->planner; diff --git a/tests/Feature/Schedule/ScheduleUserDishTest.php b/backend/tests/Feature/Schedule/ScheduleUserDishTest.php similarity index 100% rename from tests/Feature/Schedule/ScheduleUserDishTest.php rename to backend/tests/Feature/Schedule/ScheduleUserDishTest.php diff --git a/tests/Feature/Schedule/UpdateScheduleTest.php b/backend/tests/Feature/Schedule/UpdateScheduleTest.php similarity index 95% rename from tests/Feature/Schedule/UpdateScheduleTest.php rename to backend/tests/Feature/Schedule/UpdateScheduleTest.php index 1d770b6..ab10c86 100644 --- a/tests/Feature/Schedule/UpdateScheduleTest.php +++ b/backend/tests/Feature/Schedule/UpdateScheduleTest.php @@ -16,12 +16,6 @@ class UpdateScheduleTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_mark_day_as_skipped(): void { $planner = $this->planner; diff --git a/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php similarity index 95% rename from tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php rename to backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php index 67626de..3acf60b 100644 --- a/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php +++ b/backend/tests/Feature/ScheduledUserDish/CreateScheduledUserDishTest.php @@ -17,12 +17,6 @@ class CreateScheduledUserDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_schedule_user_dishes(): void { $planner = $this->planner; @@ -32,7 +26,7 @@ public function test_planner_can_schedule_user_dishes(): void $dish = Dish::factory()->planner($planner)->create(); $dish->users()->attach($users); - $scheduleDate = now()->addDays(7)->format('Y-m-d'); + $scheduleDate = '2025-12-13'; $targetUserDish = $dish->userDishes->random(); @@ -98,7 +92,7 @@ public function test_planner_cannot_schedule_user_dishes_from_other_planner(): v $dish = Dish::factory()->planner($otherPlanner)->create(); $dish->users()->attach($users); - $scheduleDate = now()->addDays(7)->format('Y-m-d'); + $scheduleDate = '2025-12-13'; $targetUserDish = $dish->userDishes->random(); diff --git a/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php similarity index 96% rename from tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php rename to backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php index 4527623..22b0486 100755 --- a/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php +++ b/backend/tests/Feature/ScheduledUserDish/DeleteScheduledUserDishTest.php @@ -17,12 +17,6 @@ class DeleteScheduledUserDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_delete_a_scheduled_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php similarity index 97% rename from tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php rename to backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php index 5571d3f..8c44ec5 100644 --- a/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php +++ b/backend/tests/Feature/ScheduledUserDish/ReadScheduledUserDishTest.php @@ -18,12 +18,6 @@ class ReadScheduledUserDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_single_dish_can_be_read(): void { $planner = $this->planner; diff --git a/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php b/backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php similarity index 98% rename from tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php rename to backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php index 68953dc..580055a 100644 --- a/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php +++ b/backend/tests/Feature/ScheduledUserDish/UpdateScheduledUserDishTest.php @@ -21,12 +21,6 @@ class UpdateScheduledUserDishTest extends TestCase use DishesTestTrait; use ScheduledDishesTestTrait; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_dish_update_succeeds(): void { $planner = $this->planner; diff --git a/tests/Feature/User/CreateUserTest.php b/backend/tests/Feature/User/CreateUserTest.php similarity index 90% rename from tests/Feature/User/CreateUserTest.php rename to backend/tests/Feature/User/CreateUserTest.php index 76be687..3f3ce11 100644 --- a/tests/Feature/User/CreateUserTest.php +++ b/backend/tests/Feature/User/CreateUserTest.php @@ -14,12 +14,6 @@ class CreateUserTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_create_user(): void { $planner = $this->planner; diff --git a/tests/Feature/User/DeleteUserTest.php b/backend/tests/Feature/User/DeleteUserTest.php similarity index 93% rename from tests/Feature/User/DeleteUserTest.php rename to backend/tests/Feature/User/DeleteUserTest.php index b67e55d..3526de9 100644 --- a/tests/Feature/User/DeleteUserTest.php +++ b/backend/tests/Feature/User/DeleteUserTest.php @@ -14,12 +14,6 @@ class DeleteUserTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_delete_user(): void { $planner = $this->planner; diff --git a/tests/Feature/User/Dish/ListUserDishesTest.php b/backend/tests/Feature/User/Dish/ListUserDishesTest.php similarity index 94% rename from tests/Feature/User/Dish/ListUserDishesTest.php rename to backend/tests/Feature/User/Dish/ListUserDishesTest.php index 8515193..5b12ab4 100644 --- a/tests/Feature/User/Dish/ListUserDishesTest.php +++ b/backend/tests/Feature/User/Dish/ListUserDishesTest.php @@ -16,12 +16,6 @@ class ListUserDishesTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_see_user_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/User/Dish/RemoveDishesForUserTest.php b/backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php similarity index 92% rename from tests/Feature/User/Dish/RemoveDishesForUserTest.php rename to backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php index 2f7e15a..a04ced3 100755 --- a/tests/Feature/User/Dish/RemoveDishesForUserTest.php +++ b/backend/tests/Feature/User/Dish/RemoveDishesForUserTest.php @@ -15,12 +15,6 @@ class RemoveDishesForUserTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_can_remove_dish_for_a_user(): void { $this->assertDatabaseEmpty(UserDish::class); diff --git a/tests/Feature/User/Dish/ShowUserDishTest.php b/backend/tests/Feature/User/Dish/ShowUserDishTest.php similarity index 95% rename from tests/Feature/User/Dish/ShowUserDishTest.php rename to backend/tests/Feature/User/Dish/ShowUserDishTest.php index b1e294f..02cdb5b 100644 --- a/tests/Feature/User/Dish/ShowUserDishTest.php +++ b/backend/tests/Feature/User/Dish/ShowUserDishTest.php @@ -16,12 +16,6 @@ class ShowUserDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_see_user_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php b/backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php similarity index 98% rename from tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php rename to backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php index 205f634..ec04b68 100755 --- a/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php +++ b/backend/tests/Feature/User/Dish/StoreRecurrenceForUserDishTest.php @@ -19,12 +19,6 @@ class StoreRecurrenceForUserDishTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_adds_fixed_recurrence_to_user_dish(): void { $planner = $this->planner; diff --git a/tests/Feature/User/ListUsersTest.php b/backend/tests/Feature/User/ListUsersTest.php similarity index 95% rename from tests/Feature/User/ListUsersTest.php rename to backend/tests/Feature/User/ListUsersTest.php index df4a53b..c2b694d 100644 --- a/tests/Feature/User/ListUsersTest.php +++ b/backend/tests/Feature/User/ListUsersTest.php @@ -16,12 +16,6 @@ class ListUsersTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_see_list_of_users(): void { $planner = $this->planner; diff --git a/tests/Feature/User/ShowUserTest.php b/backend/tests/Feature/User/ShowUserTest.php similarity index 93% rename from tests/Feature/User/ShowUserTest.php rename to backend/tests/Feature/User/ShowUserTest.php index e85c3c4..acc9f32 100644 --- a/tests/Feature/User/ShowUserTest.php +++ b/backend/tests/Feature/User/ShowUserTest.php @@ -14,12 +14,6 @@ class ShowUserTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_see_user(): void { $planner = $this->planner; diff --git a/tests/Feature/User/ShowUserWithDishesTest.php b/backend/tests/Feature/User/ShowUserWithDishesTest.php similarity index 94% rename from tests/Feature/User/ShowUserWithDishesTest.php rename to backend/tests/Feature/User/ShowUserWithDishesTest.php index 9a90a1a..09a9b67 100644 --- a/tests/Feature/User/ShowUserWithDishesTest.php +++ b/backend/tests/Feature/User/ShowUserWithDishesTest.php @@ -15,12 +15,6 @@ class ShowUserWithDishesTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_list_user_dishes(): void { $planner = $this->planner; diff --git a/tests/Feature/User/UpdateUserTest.php b/backend/tests/Feature/User/UpdateUserTest.php similarity index 94% rename from tests/Feature/User/UpdateUserTest.php rename to backend/tests/Feature/User/UpdateUserTest.php index 20c5e20..429a240 100644 --- a/tests/Feature/User/UpdateUserTest.php +++ b/backend/tests/Feature/User/UpdateUserTest.php @@ -14,12 +14,6 @@ class UpdateUserTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_planner_can_update_user(): void { $planner = $this->planner; diff --git a/tests/TestCase.php b/backend/tests/TestCase.php similarity index 100% rename from tests/TestCase.php rename to backend/tests/TestCase.php diff --git a/tests/Traits/DishesTestTrait.php b/backend/tests/Traits/DishesTestTrait.php similarity index 100% rename from tests/Traits/DishesTestTrait.php rename to backend/tests/Traits/DishesTestTrait.php diff --git a/backend/tests/Traits/HasPlanner.php b/backend/tests/Traits/HasPlanner.php new file mode 100644 index 0000000..995e214 --- /dev/null +++ b/backend/tests/Traits/HasPlanner.php @@ -0,0 +1,20 @@ +create(); + + $this->planner = $planner; + } + +} diff --git a/tests/Traits/ScheduledDishesTestTrait.php b/backend/tests/Traits/ScheduledDishesTestTrait.php similarity index 100% rename from tests/Traits/ScheduledDishesTestTrait.php rename to backend/tests/Traits/ScheduledDishesTestTrait.php diff --git a/tests/Unit/Actions/RegenerateScheduleDayActionTest.php b/backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php similarity index 90% rename from tests/Unit/Actions/RegenerateScheduleDayActionTest.php rename to backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php index c451b8c..20e8f66 100644 --- a/tests/Unit/Actions/RegenerateScheduleDayActionTest.php +++ b/backend/tests/Unit/Actions/RegenerateScheduleDayActionTest.php @@ -17,12 +17,6 @@ class RegenerateScheduleDayActionTest extends TestCase use RefreshDatabase; use DishesTestTrait; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_regenerates_for_a_single_schedule(): void { $planner = $this->planner; diff --git a/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php b/backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php similarity index 97% rename from tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php rename to backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php index 6facf19..4d1f670 100644 --- a/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php +++ b/backend/tests/Unit/Actions/RegenerateScheduleDayForUserActionTest.php @@ -17,12 +17,6 @@ class RegenerateScheduleDayForUserActionTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_creates(): void { $date = now(); diff --git a/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php b/backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php similarity index 91% rename from tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php rename to backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php index 7635ec8..451b590 100644 --- a/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php +++ b/backend/tests/Unit/Schedule/Actions/DraftScheduleForDateActionTest.php @@ -16,12 +16,6 @@ class DraftScheduleForDateActionTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_draft_schedule(): void { $planner = $this->planner; diff --git a/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php b/backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php similarity index 91% rename from tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php rename to backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php index e30758e..52669ed 100644 --- a/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php +++ b/backend/tests/Unit/Schedule/Actions/DraftScheduleForPeriodActionTest.php @@ -18,12 +18,6 @@ class DraftScheduleForPeriodActionTest extends TestCase use RefreshDatabase; use DishesTestTrait; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_user_can_generate_schedule(): void { $planner = $this->planner; diff --git a/tests/Unit/Schedule/ScheduleGeneratorTest.php b/backend/tests/Unit/Schedule/ScheduleGeneratorTest.php similarity index 97% rename from tests/Unit/Schedule/ScheduleGeneratorTest.php rename to backend/tests/Unit/Schedule/ScheduleGeneratorTest.php index db24196..bce3589 100644 --- a/tests/Unit/Schedule/ScheduleGeneratorTest.php +++ b/backend/tests/Unit/Schedule/ScheduleGeneratorTest.php @@ -22,12 +22,6 @@ class ScheduleGeneratorTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_it_fills_up_the_next_2_weeks(): void { $planner = $this->planner; diff --git a/tests/Unit/ScheduleRepositoryTest.php b/backend/tests/Unit/ScheduleRepositoryTest.php similarity index 91% rename from tests/Unit/ScheduleRepositoryTest.php rename to backend/tests/Unit/ScheduleRepositoryTest.php index b0f93c5..600d2a5 100644 --- a/tests/Unit/ScheduleRepositoryTest.php +++ b/backend/tests/Unit/ScheduleRepositoryTest.php @@ -14,12 +14,6 @@ class ScheduleRepositoryTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_find_or_create_finds_existing_model(): void { $planner = $this->planner; diff --git a/tests/Unit/UpdateScheduledUserDishActionTest.php b/backend/tests/Unit/UpdateScheduledUserDishActionTest.php similarity index 100% rename from tests/Unit/UpdateScheduledUserDishActionTest.php rename to backend/tests/Unit/UpdateScheduledUserDishActionTest.php diff --git a/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php b/backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php similarity index 96% rename from tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php rename to backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php index 3e904e0..efe5982 100644 --- a/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php +++ b/backend/tests/Unit/UserDish/Repositories/UserDishRepositoryTest.php @@ -19,12 +19,6 @@ class UserDishRepositoryTest extends TestCase use HasPlanner; use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); - $this->setUpHasPlanner(); - } - public function test_find_interfering_dishes(): void { $planner = $this->planner; diff --git a/vite.config.js b/backend/vite.config.js similarity index 64% rename from vite.config.js rename to backend/vite.config.js index 0f996c9..421b569 100644 --- a/vite.config.js +++ b/backend/vite.config.js @@ -8,12 +8,4 @@ export default defineConfig({ refresh: true, }), ], - server: { - host: '0.0.0.0', - port: 5173, - hmr: { - host: 'localhost', - port: 5173, - }, - }, }); diff --git a/bin/build-push.sh b/bin/build-push.sh deleted file mode 100755 index 532b43f..0000000 --- a/bin/build-push.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Build and push production image to Codeberg - -set -e - -# Configuration -REGISTRY="codeberg.org" -NAMESPACE="lvl0" -IMAGE_NAME="dish-planner" -TAG="${1:-latest}" - -echo "๐Ÿ”จ Building production image..." -podman build -f Dockerfile -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} . - -echo "๐Ÿ“ค Pushing to Codeberg registry..." -echo "Please ensure you're logged in to Codeberg:" -echo " podman login codeberg.org" - -podman push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} - -echo "โœ… Done! Image pushed to ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}" -echo "" -echo "To deploy in production:" -echo "1. Copy docker-compose.prod.yml to your server" -echo "2. Set required environment variables:" -echo " - APP_KEY (generate with: openssl rand -base64 32)" -echo " - APP_URL" -echo " - DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_ROOT_PASSWORD" -echo "3. Run: docker-compose -f docker-compose.prod.yml up -d" \ No newline at end of file diff --git a/bin/start-dev b/bin/start-dev deleted file mode 100755 index 4d31519..0000000 --- a/bin/start-dev +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get script directory and project root -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Detect Docker or Podman -CONTAINER_CLI="" -# Check if docker is actually podman in disguise -if command -v docker &> /dev/null && docker --version 2>&1 | grep -q "podman"; then - CONTAINER_CLI="podman" - echo -e "${GREEN}Using Podman (via docker alias)${NC}" -elif command -v docker &> /dev/null && docker info &> /dev/null; then - CONTAINER_CLI="docker" - echo -e "${GREEN}Using Docker${NC}" -elif command -v podman &> /dev/null; then - CONTAINER_CLI="podman" - echo -e "${GREEN}Using Podman${NC}" -else - echo -e "${RED}โœ— Neither Docker nor Podman found. Please install one of them first.${NC}" - echo -e "${YELLOW}Install Docker: https://docs.docker.com/get-docker/${NC}" - echo -e "${YELLOW}Install Podman: https://podman.io/getting-started/installation${NC}" - exit 1 -fi - -echo -e "${GREEN}=== Dish Planner Development Setup ===${NC}\n" - -# Check if .env exists in backend -if [ ! -f "$PROJECT_ROOT/backend/.env" ]; then - echo -e "${YELLOW}Creating backend/.env from .env.example...${NC}" - cp "$PROJECT_ROOT/backend/.env.example" "$PROJECT_ROOT/backend/.env" - echo -e "${GREEN}โœ“ Created backend/.env${NC}\n" -fi - -# Ensure APP_PORT is set for Podman (can't use privileged port 80) -if [ "$CONTAINER_CLI" = "podman" ] && ! grep -q "^APP_PORT=" "$PROJECT_ROOT/backend/.env"; then - echo -e "${YELLOW}Setting APP_PORT=8000 for Podman (non-privileged port)...${NC}" - echo "APP_PORT=8000" >> "$PROJECT_ROOT/backend/.env" -fi - -# Backend setup -echo -e "${GREEN}=== Backend Setup ===${NC}" -cd "$PROJECT_ROOT/backend" - -# Install dependencies if vendor doesn't exist -if [ ! -d "vendor" ]; then - echo -e "${YELLOW}No vendor directory found. Installing dependencies with Docker...${NC}" - # Use a standalone PHP/Composer Docker image to install dependencies - # This is the recommended Laravel approach for first-time setup - # Configure for Docker or Podman - VOLUME_OPTS="$PROJECT_ROOT/backend:/var/www/html" - USER_OPTS="-u $(id -u):$(id -g)" - EXTRA_OPTS="" - - if [ "$CONTAINER_CLI" = "podman" ]; then - # Podman on SELinux systems needs :Z and --userns=keep-id to maintain user ID - VOLUME_OPTS="$VOLUME_OPTS:Z" - USER_OPTS="" - EXTRA_OPTS="--userns=keep-id" - fi - - $CONTAINER_CLI run --rm \ - $USER_OPTS \ - $EXTRA_OPTS \ - -v "$VOLUME_OPTS" \ - -w /var/www/html \ - docker.io/laravelsail/php84-composer:latest \ - composer install --ignore-platform-reqs - - echo -e "${GREEN}โœ“ Backend dependencies installed${NC}\n" -else - echo -e "${GREEN}โœ“ Backend dependencies already installed${NC}\n" -fi - -# Check if database volume exists - if not, we're doing fresh initialization -FRESH_DB=false -if ! $CONTAINER_CLI volume inspect sail-mysql &>/dev/null && ! $CONTAINER_CLI volume inspect backend_sail-mysql &>/dev/null; then - FRESH_DB=true - echo -e "${YELLOW}Fresh database initialization detected${NC}" -fi - -# Start containers using compose directly (docker-compose.override.yml is automatically used) -echo -e "${YELLOW}Starting backend containers...${NC}" -if $CONTAINER_CLI compose up -d 2>&1 | tee /tmp/compose-up.log; then - echo -e "${GREEN}โœ“ Containers started${NC}\n" -else - echo -e "${RED}โœ— Failed to start containers. Check /tmp/compose-up.log for details${NC}" - exit 1 -fi - -# Wait for database to be ready -echo -e "${YELLOW}Waiting for database to be ready...${NC}" -MAX_ATTEMPTS=30 -ATTEMPT=0 -while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - if $CONTAINER_CLI compose exec mysql mysqladmin ping -h localhost --silent 2>/dev/null; then - echo -e "${GREEN}โœ“ Database is ready${NC}" - break - fi - ATTEMPT=$((ATTEMPT + 1)) - echo -e "${YELLOW}Waiting for database... (attempt $ATTEMPT/$MAX_ATTEMPTS)${NC}" - sleep 2 -done - -if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then - echo -e "${RED}โœ— Database failed to become ready${NC}" - exit 1 -fi - -# Give MySQL extra time on fresh initialization to create users -if [ "$FRESH_DB" = true ]; then - echo -e "${YELLOW}Waiting for fresh database to complete initialization...${NC}" - sleep 10 -else - sleep 3 -fi - -# Run migrations -echo -e "${YELLOW}Running database migrations...${NC}" -$CONTAINER_CLI compose exec backend php artisan migrate --force - -# Check if database has data -echo -e "${YELLOW}Checking if database needs seeding...${NC}" -TABLE_COUNT=$($CONTAINER_CLI compose exec backend php artisan tinker --execute="echo \DB::table('users')->count();" 2>/dev/null | tail -1 | tr -d '[:space:]' || echo "0") -# Default to 0 if not a number -if ! [[ "$TABLE_COUNT" =~ ^[0-9]+$ ]]; then - TABLE_COUNT=0 -fi -if [ "$TABLE_COUNT" -eq "0" ]; then - echo -e "${YELLOW}Database is empty. Run seeders? (y/n)${NC}" - read -r response - if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then - $CONTAINER_CLI compose exec backend php artisan db:seed - echo -e "${GREEN}โœ“ Database seeded${NC}\n" - fi -fi - -echo -e "${GREEN}โœ“ Backend setup complete${NC}" -echo -e "${GREEN}Backend API running at: http://localhost:8000${NC}\n" - -# Display summary -echo -e "${GREEN}=== Development Environment Ready ===${NC}" -echo -e "${GREEN}Backend API:${NC} http://localhost:8000" -echo -e "${GREEN}Frontend:${NC} http://localhost:5173" -echo -e "${GREEN}Database:${NC} MySQL on localhost:3306" -echo -e "" -echo -e "${YELLOW}Note:${NC} Frontend container will install dependencies and start automatically." -echo -e "${YELLOW}To view frontend logs:${NC}" -echo -e " cd backend && $CONTAINER_CLI compose logs -f frontend" -echo -e "" -echo -e "${YELLOW}To stop all services:${NC}" -echo -e " cd backend && $CONTAINER_CLI compose down" diff --git a/bootstrap/app.php b/bootstrap/app.php deleted file mode 100644 index 7fb61b3..0000000 --- a/bootstrap/app.php +++ /dev/null @@ -1,80 +0,0 @@ -withRouting( - web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', - commands: __DIR__.'/../routes/console.php', - health: '/up', - then: function () { - Route::middleware('web') - ->group(base_path('routes/web/subscription.php')); - }, - ) - ->withMiddleware(function (Middleware $middleware) { - // Apply ForceJsonResponse only to API routes - $middleware->api(ForceJsonResponse::class); - - $middleware->alias([ - 'subscription' => RequireSubscription::class, - 'saas' => RequireSaasMode::class, - ]); - - // Exclude Stripe webhook from CSRF verification - $middleware->validateCsrfTokens(except: [ - 'stripe/webhook', - ]); - }) - ->withExceptions(function (Exceptions $exceptions) { - $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { - if ($request->is('api/*')) { - return true; - } - - return $request->expectsJson(); - }); - - /** @var OutputService $outputService */ - $outputService = resolve(OutputService::class); - - $exceptions->render(function (ValidationException $e, Request $request) use ($outputService) { - if ($request->is('api/*') || $request->expectsJson()) { - return response()->json( - $outputService->response(false, null, [$e->getMessage()]), - 404 - ); - } - }); - - $exceptions->render(function (NotFoundHttpException $e, Request $request) use ($outputService) { - if ($request->is('api/*') || $request->expectsJson()) { - return response()->json( - $outputService->response(false, null, ['MODEL_NOT_FOUND']), - 404 - ); - } - }); - - $exceptions->render(function (AccessDeniedHttpException $e, Request $request) use ($outputService) { - if ($request->is('api/*') || $request->expectsJson()) { - return response()->json( - $outputService->response(false, null, [$e->getMessage()]), - 403 - ); - } - }); - }) - ->create(); diff --git a/build-push.sh b/build-push.sh deleted file mode 100755 index 532b43f..0000000 --- a/build-push.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Build and push production image to Codeberg - -set -e - -# Configuration -REGISTRY="codeberg.org" -NAMESPACE="lvl0" -IMAGE_NAME="dish-planner" -TAG="${1:-latest}" - -echo "๐Ÿ”จ Building production image..." -podman build -f Dockerfile -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} . - -echo "๐Ÿ“ค Pushing to Codeberg registry..." -echo "Please ensure you're logged in to Codeberg:" -echo " podman login codeberg.org" - -podman push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} - -echo "โœ… Done! Image pushed to ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}" -echo "" -echo "To deploy in production:" -echo "1. Copy docker-compose.prod.yml to your server" -echo "2. Set required environment variables:" -echo " - APP_KEY (generate with: openssl rand -base64 32)" -echo " - APP_URL" -echo " - DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_ROOT_PASSWORD" -echo "3. Run: docker-compose -f docker-compose.prod.yml up -d" \ No newline at end of file diff --git a/build_and_push.sh b/build_and_push.sh deleted file mode 100755 index b3ce7d4..0000000 --- a/build_and_push.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t 192.168.178.152:50114/dishplanner-backend . -docker push 192.168.178.152:50114/dishplanner-backend diff --git a/config/livewire.php b/config/livewire.php deleted file mode 100644 index 294b7a4..0000000 --- a/config/livewire.php +++ /dev/null @@ -1,186 +0,0 @@ - 'App\\Livewire', - - /* - |--------------------------------------------------------------------------- - | View Path - |--------------------------------------------------------------------------- - | - | This value is used to specify where Livewire component Blade templates are - | stored when running file creation commands like `artisan make:livewire`. - | It is also used if you choose to omit a component's render() method. - | - */ - - 'view_path' => resource_path('views/livewire'), - - /* - |--------------------------------------------------------------------------- - | Layout - |--------------------------------------------------------------------------- - | The view that will be used as the layout when rendering a single component - | as an entire page via `Route::get('/post/create', CreatePost::class);`. - | In this case, the view returned by CreatePost will render into $slot. - | - */ - - 'layout' => 'components.layouts.app', - - /* - |--------------------------------------------------------------------------- - | Lazy Loading Placeholder - |--------------------------------------------------------------------------- - | Livewire allows you to lazy load components that would otherwise slow down - | the initial page load. Every component can have a custom placeholder or - | you can define the default placeholder view for all components below. - | - */ - - 'lazy_placeholder' => null, - - /* - |--------------------------------------------------------------------------- - | Temporary File Uploads - |--------------------------------------------------------------------------- - | - | Livewire handles file uploads by storing uploads in a temporary directory - | before the file is stored permanently. All file uploads are directed to - | a global endpoint for temporary storage. You may configure this below: - | - */ - - 'temporary_file_upload' => [ - 'disk' => null, // Example: 'local', 's3' | Default: 'default' - 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) - 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' - 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' - 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... - 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', - 'mov', 'avi', 'wmv', 'mp3', 'm4a', - 'jpg', 'jpeg', 'mpga', 'webp', 'wma', - ], - 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... - 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... - ], - - /* - |--------------------------------------------------------------------------- - | Render On Redirect - |--------------------------------------------------------------------------- - | - | This value determines if Livewire will run a component's `render()` method - | after a redirect has been triggered using something like `redirect(...)` - | Setting this to true will render the view once more before redirecting - | - */ - - 'render_on_redirect' => false, - - /* - |--------------------------------------------------------------------------- - | Eloquent Model Binding - |--------------------------------------------------------------------------- - | - | Previous versions of Livewire supported binding directly to eloquent model - | properties using wire:model by default. However, this behavior has been - | deemed too "magical" and has therefore been put under a feature flag. - | - */ - - 'legacy_model_binding' => false, - - /* - |--------------------------------------------------------------------------- - | Auto-inject Frontend Assets - |--------------------------------------------------------------------------- - | - | By default, Livewire automatically injects its JavaScript and CSS into the - | and of pages containing Livewire components. By disabling - | this behavior, you need to use @livewireStyles and @livewireScripts. - | - */ - - 'inject_assets' => true, - - /* - |--------------------------------------------------------------------------- - | Navigate (SPA mode) - |--------------------------------------------------------------------------- - | - | By adding `wire:navigate` to links in your Livewire application, Livewire - | will prevent the default link handling and instead request those pages - | via AJAX, creating an SPA-like effect. Configure this behavior here. - | - */ - - 'navigate' => [ - 'show_progress_bar' => true, - 'progress_bar_color' => '#2299dd', - ], - - /* - |--------------------------------------------------------------------------- - | HTML Morph Markers - |--------------------------------------------------------------------------- - | - | Livewire intelligently "morphs" existing HTML into the newly rendered HTML - | after each update. To make this process more reliable, Livewire injects - | "markers" into the rendered Blade surrounding @if, @class & @foreach. - | - */ - - 'inject_morph_markers' => true, - - /* - |--------------------------------------------------------------------------- - | Smart Wire Keys - |--------------------------------------------------------------------------- - | - | Livewire uses loops and keys used within loops to generate smart keys that - | are applied to nested components that don't have them. This makes using - | nested components more reliable by ensuring that they all have keys. - | - */ - - 'smart_wire_keys' => false, - - /* - |--------------------------------------------------------------------------- - | Pagination Theme - |--------------------------------------------------------------------------- - | - | When enabling Livewire's pagination feature by using the `WithPagination` - | trait, Livewire will use Tailwind templates to render pagination views - | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" - | - */ - - 'pagination_theme' => 'tailwind', - - /* - |--------------------------------------------------------------------------- - | Release Token - |--------------------------------------------------------------------------- - | - | This token is stored client-side and sent along with each request to check - | a users session to see if a new release has invalidated it. If there is - | a mismatch it will throw an error and prompt for a browser refresh. - | - */ - - 'release_token' => 'a', -]; diff --git a/database/migrations/2026_01_06_000525_create_customer_columns.php b/database/migrations/2026_01_06_000525_create_customer_columns.php deleted file mode 100644 index 131d232..0000000 --- a/database/migrations/2026_01_06_000525_create_customer_columns.php +++ /dev/null @@ -1,34 +0,0 @@ -string('stripe_id')->nullable()->index(); - $table->string('pm_type')->nullable(); - $table->string('pm_last_four', 4)->nullable(); - $table->timestamp('trial_ends_at')->nullable(); - }); - } - - public function down(): void - { - Schema::table('planners', function (Blueprint $table) { - $table->dropIndex([ - 'stripe_id', - ]); - - $table->dropColumn([ - 'stripe_id', - 'pm_type', - 'pm_last_four', - 'trial_ends_at', - ]); - }); - } -}; diff --git a/database/migrations/2026_01_06_000526_create_subscriptions_table.php b/database/migrations/2026_01_06_000526_create_subscriptions_table.php deleted file mode 100644 index 9043296..0000000 --- a/database/migrations/2026_01_06_000526_create_subscriptions_table.php +++ /dev/null @@ -1,37 +0,0 @@ -id(); - $table->foreignId('planner_id'); - $table->string('type'); - $table->string('stripe_id')->unique(); - $table->string('stripe_status'); - $table->string('stripe_price')->nullable(); - $table->integer('quantity')->nullable(); - $table->timestamp('trial_ends_at')->nullable(); - $table->timestamp('ends_at')->nullable(); - $table->timestamps(); - - $table->index(['planner_id', 'stripe_status']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('subscriptions'); - } -}; diff --git a/database/migrations/2026_01_06_000527_create_subscription_items_table.php b/database/migrations/2026_01_06_000527_create_subscription_items_table.php deleted file mode 100644 index 420e23f..0000000 --- a/database/migrations/2026_01_06_000527_create_subscription_items_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->foreignId('subscription_id'); - $table->string('stripe_id')->unique(); - $table->string('stripe_product'); - $table->string('stripe_price'); - $table->integer('quantity')->nullable(); - $table->timestamps(); - - $table->index(['subscription_id', 'stripe_price']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('subscription_items'); - } -}; diff --git a/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php b/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php deleted file mode 100644 index 033bb82..0000000 --- a/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('meter_id')->nullable()->after('stripe_price'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('subscription_items', function (Blueprint $table) { - $table->dropColumn('meter_id'); - }); - } -}; diff --git a/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php b/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php deleted file mode 100644 index b157b3a..0000000 --- a/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('meter_event_name')->nullable()->after('quantity'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('subscription_items', function (Blueprint $table) { - $table->dropColumn('meter_event_name'); - }); - } -}; diff --git a/database/seeders/DevelopmentSeeder.php b/database/seeders/DevelopmentSeeder.php deleted file mode 100644 index 0b470d8..0000000 --- a/database/seeders/DevelopmentSeeder.php +++ /dev/null @@ -1,76 +0,0 @@ -environment('testing')) { - return; - } - - // Create main planner - $planner = Planner::factory()->create([ - 'name' => 'Development Planner', - 'email' => 'myrmidex@myrmidex.net', - 'password' => Hash::make('Password'), - ]); - - // Create a few users (users don't have email/password - only planners do) - $users = collect([ - User::factory()->create([ - 'planner_id' => $planner->id, - 'name' => 'Alice Johnson', - ]), - User::factory()->create([ - 'planner_id' => $planner->id, - 'name' => 'Bob Smith', - ]), - User::factory()->create([ - 'planner_id' => $planner->id, - 'name' => 'Charlie Brown', - ]), - ]); - - // Create various dishes - $dishNames = [ - 'Spaghetti Bolognese', - 'Chicken Curry', - 'Caesar Salad', - 'Beef Stir Fry', - 'Vegetable Lasagne', - 'Fish Tacos', - 'Mushroom Risotto', - 'BBQ Ribs', - 'Greek Salad', - 'Pad Thai', - 'Margherita Pizza', - 'Beef Burger', - 'Chicken Fajitas', - 'Vegetable Soup', - 'Salmon Teriyaki', - ]; - - foreach ($dishNames as $dishName) { - $dish = Dish::factory()->create([ - 'planner_id' => $planner->id, - 'name' => $dishName, - ]); - - // Randomly assign dish to 1-3 users - $assignedUsers = $users->random(rand(1, 3)); - $dish->users()->attach($assignedUsers->pluck('id')); - } - - $this->command->info('Development data seeded successfully!'); - $this->command->info('Login credentials: myrmidex@myrmidex.net / Password'); - } -} \ No newline at end of file diff --git a/database/seeders/DishesSeeder.php b/database/seeders/DishesSeeder.php deleted file mode 100644 index 6503110..0000000 --- a/database/seeders/DishesSeeder.php +++ /dev/null @@ -1,44 +0,0 @@ -create(); - - // Get users belonging to this planner - $users = User::where('planner_id', $planner->id)->get(); - - if ($users->isEmpty()) { - $this->command->warn('No users found for planner. Skipping dishes seeder.'); - - return; - } - - $userIds = $users->pluck('id')->toArray(); - - // Build possible user combinations (individual users + all users together) - $userOptions = collect($userIds)->map(fn ($id) => [$id])->toArray(); - $userOptions[] = $userIds; // all users - - collect([ - 'Lasagne', 'Pizza', 'Burger', 'Fries', 'Salad', 'Sushi', 'Pancakes', 'Ice Cream', 'Spaghetti', 'Mac and Cheese', - 'Steak', 'Chicken', 'Beef', 'Pork', 'Fish', 'Chips', 'Cake', - ])->each(function (string $name) use ($planner, $userOptions) { - $dish = Dish::factory()->create([ - 'planner_id' => $planner->id, - 'name' => $name, - ]); - - $dish->users()->attach($userOptions[array_rand($userOptions)]); - }); - } -} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index f39bab7..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,99 +0,0 @@ -# Production Docker Compose -services: - app: - image: codeberg.org/lvl0/dish-planner:latest - container_name: dishplanner_app - restart: always - ports: - - "8000:8000" - environment: - # Required from user - APP_KEY: "${APP_KEY}" # Critical - must persist across deployments - APP_URL: "${APP_URL}" - DB_DATABASE: "${DB_DATABASE}" - DB_USERNAME: "${DB_USERNAME}" - DB_PASSWORD: "${DB_PASSWORD}" - - # Optional email configuration - MAIL_HOST: "${MAIL_HOST:-}" - MAIL_PORT: "${MAIL_PORT:-587}" - MAIL_USERNAME: "${MAIL_USERNAME:-}" - MAIL_PASSWORD: "${MAIL_PASSWORD:-}" - MAIL_FROM_ADDRESS: "${MAIL_FROM_ADDRESS:-noreply@example.com}" - volumes: - # Only persist storage in production - - app_storage:/app/storage - - app_logs:/app/storage/logs - depends_on: - - db - networks: - - dishplanner - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/up"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - db: - image: mariadb:11 - container_name: dishplanner_db - restart: always - environment: - MYSQL_DATABASE: "${DB_DATABASE}" - MYSQL_USER: "${DB_USERNAME}" - MYSQL_PASSWORD: "${DB_PASSWORD}" - MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" - volumes: - - db_data:/var/lib/mysql - networks: - - dishplanner - - # Optional: Redis for production caching/sessions - # redis: - # image: redis:7-alpine - # container_name: dishplanner_redis - # restart: always - # command: redis-server --requirepass ${REDIS_PASSWORD} - # volumes: - # - redis_data:/data - # networks: - # - dishplanner - # healthcheck: - # test: ["CMD", "redis-cli", "ping"] - # interval: 30s - # timeout: 10s - # retries: 3 - - # Optional: Backup service - # backup: - # image: mariadb:11 - # container_name: dishplanner_backup - # restart: always - # environment: - # MYSQL_HOST: db - # MYSQL_USER: root - # MYSQL_PASSWORD: "${DB_ROOT_PASSWORD}" - # volumes: - # - ./database/backups:/backups - # - ./scripts/backup.sh:/backup.sh:ro - # entrypoint: ["/bin/sh"] - # command: ["-c", "while true; do /backup.sh; sleep 86400; done"] - # depends_on: - # - db - # networks: - # - dishplanner - -networks: - dishplanner: - driver: bridge - -volumes: - db_data: - driver: local - app_storage: - driver: local - app_logs: - driver: local - # redis_data: - # driver: local diff --git a/docker-compose.yml b/docker-compose.yml index cd6cbf3..25e63c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,119 +1,55 @@ -# Local Development Docker Compose -version: '3.8' - services: - app: - build: - context: . - dockerfile: Dockerfile.dev - container_name: dishplanner_app + web: + image: nginx:alpine + container_name: dishplanner-nginx restart: unless-stopped - # Remove user directive to run as root in container - # The container will handle permissions internally + depends_on: + - backend + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + command: /bin/sh -c "until nslookup backend. ; do sleep 2; done && nginx -g 'daemon off;'" ports: - - "8000:8000" # Laravel app - - "5173:5173" # Vite dev server + - "3000:80" + + backend: + image: jochent/dishplanner-backend:v0.2 + container_name: dishplanner-backend + restart: unless-stopped environment: - # Laravel - APP_NAME: "${APP_NAME:-DishPlanner}" - APP_ENV: "${APP_ENV:-local}" - APP_KEY: "${APP_KEY:-base64:YOUR_KEY_HERE}" - APP_DEBUG: "${APP_DEBUG:-true}" - APP_URL: "${APP_URL:-http://localhost:8000}" - - # Database DB_CONNECTION: mysql DB_HOST: db DB_PORT: 3306 - DB_DATABASE: "${DB_DATABASE:-dishplanner}" - DB_USERNAME: "${DB_USERNAME:-dishplanner}" - DB_PASSWORD: "${DB_PASSWORD:-dishplanner}" - - # Session & Cache - SESSION_DRIVER: "${SESSION_DRIVER:-file}" - CACHE_DRIVER: "${CACHE_DRIVER:-file}" - QUEUE_CONNECTION: "${QUEUE_CONNECTION:-sync}" - - # Mail (for development) - MAIL_MAILER: "${MAIL_MAILER:-log}" - - # Vite - VITE_HOST: "0.0.0.0" - volumes: - # Mount entire project for hot reload with SELinux context - - .:/app:Z - # Named volumes for performance and permission isolation - - app_vendor:/app/vendor - - app_node_modules:/app/node_modules + DB_DATABASE: dishplanner + DB_USERNAME: dishuser + DB_PASSWORD: dishpass depends_on: - db - networks: - - dishplanner + ports: + - "8080:80" + + frontend: + image: jochent/dishplanner-frontend:v0.2 + container_name: dishplanner-frontend + restart: unless-stopped + depends_on: + - backend +# ports: +# - "3000:3000" db: - image: mariadb:11 - container_name: dishplanner_db + image: mysql:8.0 + container_name: dishplanner-db restart: unless-stopped - ports: - - "3306:3306" environment: - MYSQL_DATABASE: "${DB_DATABASE:-dishplanner}" - MYSQL_USER: "${DB_USERNAME:-dishplanner}" - MYSQL_PASSWORD: "${DB_PASSWORD:-dishplanner}" - MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root}" + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: dishplanner + MYSQL_USER: dishuser + MYSQL_PASSWORD: dishpass volumes: - db_data:/var/lib/mysql - # Initialize with SQL scripts - - ./docker/mysql-init:/docker-entrypoint-initdb.d - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 3 - networks: - - dishplanner - - # Optional: Mailhog for email testing - mailhog: - image: mailhog/mailhog - container_name: dishplanner_mailhog - restart: unless-stopped - ports: - - "1025:1025" # SMTP server - - "8025:8025" # Web UI - networks: - - dishplanner - - # Selenium for E2E testing with Dusk - selenium: - image: selenium/standalone-chrome:latest - container_name: dishplanner_selenium - restart: unless-stopped - ports: - - "4444:4444" # Selenium server - - "7900:7900" # VNC server for debugging - volumes: - - /dev/shm:/dev/shm - networks: - - dishplanner - environment: - - SE_VNC_PASSWORD=secret - - # Optional: Redis for caching/sessions - # redis: - # image: redis:alpine - # container_name: dishplanner_redis - # restart: unless-stopped - # ports: - # - "6379:6379" - # networks: - # - dishplanner - -networks: - dishplanner: - driver: bridge - volumes: db_data: - app_vendor: - app_node_modules: \ No newline at end of file + +networks: + default: + name: dishplanner-net diff --git a/docker/mysql-init/01-create-test-database.sql b/docker/mysql-init/01-create-test-database.sql deleted file mode 100644 index 9250306..0000000 --- a/docker/mysql-init/01-create-test-database.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Create test database for Dusk E2E tests -CREATE DATABASE IF NOT EXISTS dishplanner_test; - --- Grant all privileges on test database to the dishplanner user -GRANT ALL PRIVILEGES ON dishplanner_test.* TO 'dishplanner'@'%' IDENTIFIED BY 'dishplanner'; -GRANT ALL PRIVILEGES ON dishplanner_test.* TO 'dishplanner'@'localhost' IDENTIFIED BY 'dishplanner'; - -FLUSH PRIVILEGES; \ No newline at end of file diff --git a/frontend-old/app/components/features/OnboardingBanner.tsx b/frontend-old/app/components/features/OnboardingBanner.tsx deleted file mode 100644 index ee97858..0000000 --- a/frontend-old/app/components/features/OnboardingBanner.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Link } from "react-router" -import useRoutes from "@/hooks/useRoutes" -import { UserType } from "@/types/UserType" -import { DishType } from "@/types/DishType" - -interface Props { - dishes: DishType[], - users: UserType[] -} - -const OnboardingBanner = ({ dishes, users }: Props) => { - const routes = useRoutes(); - - const steps = [ - { - label: "Create a user", - href: routes.user.create(), - count: users.length - }, { - label: "Create a dish", - href: routes.dish.create(), - count: dishes.length - } - ] - - return ( -
-
Welcome to DishPlanner
-
To get you started, please follow these steps to set up your account. This will ensure a better - experience. -
- - { - steps.map((step, index) => ( -
- { - step.count === 0 - ? { step.label } - :
{ step.label }
- } -
- )) - } -
- ) -} - -export default OnboardingBanner; \ No newline at end of file diff --git a/frontend-old/app/components/features/auth/LoginForm.tsx b/frontend-old/app/components/features/auth/LoginForm.tsx deleted file mode 100644 index 3e693d8..0000000 --- a/frontend-old/app/components/features/auth/LoginForm.tsx +++ /dev/null @@ -1,98 +0,0 @@ - -import React, { useEffect, useState } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router'; -import { useAuth } from '@/context/AuthContext'; -import { login } from "@/utils/api/auth"; -import useRoutes from "@/hooks/useRoutes"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; -import Alert from "@/components/ui/Alert"; - -const LoginForm = () => { - const { login: authLogin } = useAuth(); - const routes = useRoutes(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [alertSuccess, setAlertSuccess] = useState([]) - const navigate = useNavigate(); - const location = useLocation(); - const searchParams = new URLSearchParams(location.search); - const isRegistered = searchParams.get('registered') === 'true'; - - useEffect(() => { - if (isRegistered) { - setAlertSuccess(['Registration successful!',' You can now log in.']); - - const timer = setTimeout(() => { - const params = new URLSearchParams(searchParams.toString()); - params.delete('registered'); - const newUrl = `${window.location.pathname}?${params.toString()}`; - - navigate(newUrl, { replace: true }); - }, 3000); - - return () => clearTimeout(timer) - } - }, [isRegistered, navigate, searchParams]) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - try { - await login(email, password); - authLogin(); - navigate('/', { replace: true }); - } catch (err) { - const errorMessage = - err instanceof Error - ? err.message - : 'Login failed'; - setError(errorMessage); - } - }; - - return ( -
-
- DISH PLANNER -
-
- { alertSuccess.length > 0 && - - {alertSuccess.map((msg, index) => ( - - {msg} -
-
- ))} -
- } -
- {error &&

{error}

} - setEmail(e.target.value)} - required - className="w-full p-2 mb-4 border rounded border-secondary bg-gray-600 text-secondary" - /> - setPassword(e.target.value)} - required - className="w-full p-2 mb-4 border rounded text-secondary border-secondary bg-gray-600" - /> - Login - - Create an account - -
-
-
- ) -} - -export default LoginForm \ No newline at end of file diff --git a/frontend-old/app/components/features/auth/RegisterForm.tsx b/frontend-old/app/components/features/auth/RegisterForm.tsx deleted file mode 100644 index 908d624..0000000 --- a/frontend-old/app/components/features/auth/RegisterForm.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState } from 'react'; -import { register } from "@/utils/api/auth"; -import useRoutes from "@/hooks/useRoutes"; -import SectionTitle from "@/components/ui/SectionTitle"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; -import { Link, useNavigate } from "react-router" - -const RegisterForm = () => { - const routes = useRoutes(); - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passwordAgain, setPasswordAgain] = useState(''); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [isRegistered, setIsRegistered] = useState(false); - const navigate = useNavigate(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (password !== passwordAgain) { - setError("Passwords do not match."); - return; - } - - try { - setIsLoading(true); - - await register(name, email, password, passwordAgain); - // navigate('/login?registered=true', { replace: true }); - } catch (err) { - const errorMessage = - err instanceof Error - ? err.message - : 'Registration\n failed'; - setError(errorMessage); - } finally { - setIsRegistered(true); - setIsLoading(false); - } - }; - - if (isRegistered) { - return
- Registration successful! - Please continue to the login page. -
- } - - return ( -
-
- DISH PLANNER -
-
-
-

Register

- { error &&

{ error }

} - setName(e.target.value) } - required - className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary" - /> - setEmail(e.target.value) } - required - className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary" - /> - setPassword(e.target.value) } - required - className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary" - /> - setPasswordAgain(e.target.value) } - required - className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary" - /> - - Create Account - - - Back to Login - -
-
-
- ) -} - -export default RegisterForm \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/AddUserToDishForm.tsx b/frontend-old/app/components/features/dishes/AddUserToDishForm.tsx deleted file mode 100644 index 974cd11..0000000 --- a/frontend-old/app/components/features/dishes/AddUserToDishForm.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useState } from "react"; -import { DishType } from "@/types/DishType"; -import { UserType } from "@/types/UserType"; -import { useFetchUsers } from "@/hooks/useFetchUsers"; -import Spinner from "@/components/Spinner"; -import {addUserToDish} from "@/utils/api/dishApi"; -import OutlineButton from "@/components/ui/Buttons/OutlineButton"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; - -interface Props { - dish: DishType; - reloadDish: () => void; -} - -const AddUserToDishForm = ({ dish, reloadDish }: Props) => { - const [showAdd, setShowAdd] = useState(false); - const [selectedUser, setSelectedUser] = useState("-1"); - const { users, isLoading: isUsersLoading } = useFetchUsers(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (selectedUser === "-1") { - alert("Please select a valid user."); - return; - } - - const userToAdd = users.find((user: UserType) => user.id === parseInt(selectedUser)); - - if (!userToAdd) { - alert("User not found."); - return; - } - - addUserToDish(dish.id, userToAdd.id) - .then(() => { - setShowAdd(false); - setSelectedUser("-1"); - reloadDish(); - }) - .catch(() => { - alert("Failed to add user, please try again."); - }); - }; - - if (isUsersLoading) { - return ; - } - - const remainingUsers = users.filter( - (user: UserType) => - !dish.users.find((dishUser: UserType) => dishUser.id === user.id) - ); - - return ( - <> - setShowAdd(!showAdd)} - disabled={remainingUsers.length === 0} - type="button" - > - Add User - - - { showAdd && ( -
-
-
-
- -
- -
- - Add User - -
-
-
-
- )} - - ); -}; - -export default AddUserToDishForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/CreateDishForm.tsx b/frontend-old/app/components/features/dishes/CreateDishForm.tsx deleted file mode 100644 index 69a139f..0000000 --- a/frontend-old/app/components/features/dishes/CreateDishForm.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router"; -import { createDish } from "~/utils/api/dishApi"; -import PageTitle from "~/components/ui/PageTitle"; -import Alert from "~/components/ui/Alert"; -import SolidButton from "~/components/ui/Buttons/SolidButton"; -import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton"; -import { ChevronLeftIcon } from "@heroicons/react/16/solid"; -import Hr from "~/components/ui/Hr" - -const CreateDishForm = () => { - const navigate = useNavigate() - const [name, setName] = useState(""); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - - const validateForm = () => { - if (!name.trim()) { - setError("Dish name cannot be empty."); - return false; - } - - return true; - }; - - const submitForm = async (e: React.FormEvent) => { - e.preventDefault() - - // Validate client-side input - if (!validateForm()) return; - - setError(""); - setLoading(true); - - try { - const result = await createDish(name); - if (result) { - navigate('/dishes') - } - } catch (error: unknown) { - setError(error instanceof Error ? error.message : "An unexpected error occurred."); - } finally { - setLoading(false); - } - } - - return ( -
-
- Create Dish -
- -
- { error && ( - { error } - ) } - -
- - setName(e.target.value) } // Update the name state on change - className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900" - placeholder="Enter dish name" - /> -
- - - { loading ? "Saving..." : "Save Changes" } - -
- -
- - } - > - Back to dishes - -
- ); - - -}; - -export default CreateDishForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/Dish.tsx b/frontend-old/app/components/features/dishes/Dish.tsx deleted file mode 100644 index 44128c7..0000000 --- a/frontend-old/app/components/features/dishes/Dish.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import {DishType} from "~/types/DishType"; - -import {PencilIcon, TrashIcon} from '@heroicons/react/24/solid' -import { Link } from "react-router"; -import useRoutes from "~/hooks/useRoutes"; -import {UserType} from "~/types/UserType"; -import Card from "~/components/layout/Card"; - -const Dish = ({ dish }: { dish: DishType}) => { - const routes = useRoutes(); - - return ( - -
-

{ dish.name }

- - { - dish.users.map((user: UserType) => ( -
{user.name.slice(0, 1)}
- )) - } -
-
- -
- -
- - -
- -
- -
-
- ) -} - -export default Dish \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/DishCard.tsx b/frontend-old/app/components/features/dishes/DishCard.tsx deleted file mode 100644 index d36a656..0000000 --- a/frontend-old/app/components/features/dishes/DishCard.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {UserType} from "@/types/UserType"; -import {DishType} from "@/types/DishType"; - -interface Props { - user: UserType, - dish: DishType, -} - -const DishCard = ({ user, dish }: Props) => { - return ( -
-
- { user.name.slice(0, 1) } -
-
- { dish ? dish.name : '-' } -
-
- ) -} - -export default DishCard \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/EditDishForm.tsx b/frontend-old/app/components/features/dishes/EditDishForm.tsx deleted file mode 100644 index a35002e..0000000 --- a/frontend-old/app/components/features/dishes/EditDishForm.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, {useState} from "react"; -import { useNavigate } from "react-router"; -import Alert from "~/components/ui/Alert"; -import {updateDish} from "~/utils/api/dishApi"; -import {DishType} from "~/types/DishType"; -import useRoutes from "~/hooks/useRoutes"; -import Spinner from "~/components/Spinner"; -import Button from "~/components/ui/Button" - -interface Props { - dish: DishType -} - -const EditDishForm = ({ dish }: Props) => { - const [name, setName] = useState(dish.name); - const [error, setError] = useState(""); - const navigate = useNavigate() - const [loading, setLoading] = useState(false); - const routes = useRoutes(); - - const validateForm = () => { - if (!name.trim()) { - setError("Dish name cannot be empty."); - return false; - } - - return true; - }; - - const submitForm = async (e: React.FormEvent) => { - e.preventDefault() - - if (!validateForm()) return; - - setError(""); - setLoading(true); - - try { - const result = await updateDish(dish.id, name); - if (result) { - navigate(routes.dish.index()) - } - } catch (error: unknown) { - setError(error instanceof Error ? error.message : "An unexpected error occurred"); - } finally { - setLoading(false); // Reset loading state - } - } - - if (loading) { - return ; - } - - return ( -
- { - error != '' && { error } - } - - {/* Dish name input */} -
- - setName(e.target.value)} // Update the name state on change - className="p-2 border rounded w-full bg-gray-500 border-secondary background-secondary" - /> -
- - {/* Save button */} - -
- ); -} - -export default EditDishForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/EditDishUserCardEditForm.tsx b/frontend-old/app/components/features/dishes/EditDishUserCardEditForm.tsx deleted file mode 100644 index 1ac08cd..0000000 --- a/frontend-old/app/components/features/dishes/EditDishUserCardEditForm.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from "react"; -import SectionTitle from "@/components/ui/SectionTitle"; -import {syncUserDishRecurrences} from "@/utils/api/usersApi"; -import Spinner from "@/components/Spinner"; -import {UserDishType} from "@/types/ScheduledUserDishType"; -import {RecurrenceType} from "@/types/RecurrenceType"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; - -interface Props { - userDish: UserDishType - onSubmit: () => void -} - -const EditDishUserCardEditForm = ({ userDish, onSubmit}: Props) => { - const weeklyRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\WeeklyRecurrence') - const minimumRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\MinimumRecurrence') - - const wv = weeklyRecurrence ? weeklyRecurrence.value : undefined - const mv = minimumRecurrence ? minimumRecurrence.value : undefined - - const [isWeeklyOn, setIsWeeklyOn] = React.useState(weeklyRecurrence !== undefined); - const [isMinimumOn, setIsMinimumOn] = React.useState(minimumRecurrence !== undefined); - const [weekday, setWeekday] = React.useState(wv ?? 0); - const [minimumValue, setMinimumValue] = React.useState(mv ?? 7); - const [loading, setLoading] = React.useState(false); - - const handleSubmit = () => { - const recurrences = [] - - if (isWeeklyOn) { - recurrences.push({ - type: 'App\\Models\\WeeklyRecurrence', - value: weekday, - }); - } - - if (isMinimumOn) { - recurrences.push({ - type: 'App\\Models\\MinimumRecurrence', - value: minimumValue, - }); - } - - setLoading(true) - syncUserDishRecurrences(userDish.dish.id, userDish.user.id, recurrences as RecurrenceType[]) - .then((data) => console.log('request data', data)) - .finally(() => { - setLoading(false) - onSubmit() - }) - } - - if (loading) { - return ; - } - - return ( -
- Recurrences - -
-
- setIsWeeklyOn(!isWeeklyOn)} - className="w-4 h-4 border border-gray-300 rounded-sm bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" - /> - -
- { - isWeeklyOn && ( -
- - -
- ) - } -
- -
-
- setIsMinimumOn(!isMinimumOn)} - className="w-4 h-4 border border-gray-300 rounded-sm bg-gray-500 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" - /> - -
- - { - isMinimumOn && ( -
- setMinimumValue(parseInt(e.currentTarget.value))} min="0" max="365" className="background-secondary border-secondary border-2 w-12 px-2" /> - -
- ) - } -
- - Save -
- ); -} - -export default EditDishUserCardEditForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/RecurrenceLabels.tsx b/frontend-old/app/components/features/dishes/RecurrenceLabels.tsx deleted file mode 100644 index deff7c3..0000000 --- a/frontend-old/app/components/features/dishes/RecurrenceLabels.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {RecurrenceType} from "@/types/RecurrenceType"; - -interface Props { - recurrences: RecurrenceType[]; -} - -const RecurrenceLabels = ({recurrences}: Props) => { - const weeklyRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\WeeklyRecurrence'); - const minimumRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\MinimumRecurrence'); - - const renderWeeklyRecurrence = () => { - if (weeklyRecurrences == undefined || weeklyRecurrences.length == 0) { - return ''; - } - - const weekdayString = (() => { - switch (weeklyRecurrences[0].value) { - case 0: return "Sunday" - case 1: return "Monday" - case 2: return "Tuesday"; - case 3: return "Wednesday"; - case 4: return "Thursday"; - case 5: return "Friday"; - case 6: return "Saturday"; - default: return "Invalid day"; - } - }) - - return ( -
- { weekdayString() } -
- ) - } - const renderMinimumRecurrence = () => { - if (minimumRecurrences == undefined || minimumRecurrences.length == 0) { - return ''; - } - - return ( -
- min: { minimumRecurrences[0].value } -
- ) - } - - return <> - { renderWeeklyRecurrence() } - { renderMinimumRecurrence() } - ; -}; - -export default RecurrenceLabels; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/SyncUsersForm.tsx b/frontend-old/app/components/features/dishes/SyncUsersForm.tsx deleted file mode 100644 index d1f7264..0000000 --- a/frontend-old/app/components/features/dishes/SyncUsersForm.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { DishType } from "@/types/DishType"; -import { UserType } from "@/types/UserType"; -import UserDishCard from "@/components/features/dishes/UserDishCard"; -import SectionTitle from "@/components/ui/SectionTitle"; -import AddUserToDishForm from "@/components/features/dishes/AddUserToDishForm"; - -interface Props { - dish: DishType; - reloadDish: () => void; -} - -const SyncUsersForm = ({ dish, reloadDish }: Props) => { - return ( -
- Users - - - - {dish.users.map((user: UserType) => ( - - ))} -
- ); -}; - -export default SyncUsersForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/dishes/UserDishCard.tsx b/frontend-old/app/components/features/dishes/UserDishCard.tsx deleted file mode 100644 index 5681434..0000000 --- a/frontend-old/app/components/features/dishes/UserDishCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, {useEffect} from "react"; -import {DishType} from "~/types/DishType"; -import {UserType} from "~/types/UserType"; -import { Link } from "react-router"; -import {PencilIcon, TrashIcon} from "@heroicons/react/24/solid"; -import {removeUserFromDish} from "~/utils/api/dishApi"; -import EditDishUserCardEditForm from "~/components/features/dishes/EditDishUserCardEditForm"; -import {getUserDishForUserAndDish} from "~/utils/api/usersApi"; -import Spinner from "~/components/Spinner"; -import RecurrenceLabels from "~/components/features/dishes/RecurrenceLabels"; -import {UserDishType} from "~/types/ScheduledUserDishType"; - -interface Props { - dish: DishType - user: UserType - reloadDish: () => void -} - -const UserDishCard = ({dish, user, reloadDish}: Props) => { - const [userDish, setUserDish] = React.useState(null); - const [userDishLoading, setUserDishLoading] = React.useState(true); - const [isEditMode, setIsEditMode] = React.useState(false); - - useEffect(() => { - getUserDishForUserAndDish(user.id, dish.id) - .then((userDish) => setUserDish(userDish)) - .finally(() => setUserDishLoading(false)) - }, [dish, user]); - - const handleRemove = () => { - removeUserFromDish(dish.id, user.id) - .then(() => reloadDish()) - .catch(() => { - alert("Failed to remove user, please try again."); - }); - }; - - if (userDishLoading || !userDish) { - return - } - - const onUserCardSubmit = () => { - setIsEditMode(false); - reloadDish() - } - - return ( -
-
-
- {user.name} -
- -
- -
- -
- setIsEditMode(!isEditMode)} to="#"> -
- -
- -
-
- -
- -
- -
-
- - {isEditMode && ( -
- -
- )} -
- ); -} - -export default UserDishCard; \ No newline at end of file diff --git a/frontend-old/app/components/features/navbar/MobileDropdownMenu.tsx b/frontend-old/app/components/features/navbar/MobileDropdownMenu.tsx deleted file mode 100644 index 46ccdf0..0000000 --- a/frontend-old/app/components/features/navbar/MobileDropdownMenu.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Link } from "react-router"; -import React from "react"; -import useRoutes from "~/hooks/useRoutes"; -import classNames from "classnames"; - -interface Props { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; - handleLogout: (e: React.MouseEvent) => void; -} - -const divStyles = classNames( - 'absolute', 'text-xxl', 'rounded-b', 'top-full mt-1', 'left-0', 'right-0', '', 'py-2', - 'bg-gray-600', 'border-secondary', 'shadow-md', 'flex', 'flex-col', 'space-y-3', - 'md:hidden' -) - -const linkStyles = classNames( - 'border-b-2', 'border-secondary', 'uppercase', - 'text-primary', 'hover:background-secondary', 'pb-2', 'pl-5', - 'space-grotesk', 'text-xl' -) - -const MobileDropdownMenu = ({ isOpen, setIsOpen, handleLogout }: Props) => { - const routes = useRoutes(); - - if (!isOpen) return null; - - return ( -
- setIsOpen(false)} - > - Home - - setIsOpen(false)} - > - Dishes - - setIsOpen(false)} - > - Users - - setIsOpen(false)} - > - History - - - Logout - -
- ) -} - -export default MobileDropdownMenu \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/ScheduleCalendar.tsx b/frontend-old/app/components/features/schedule/ScheduleCalendar.tsx deleted file mode 100644 index fce02c2..0000000 --- a/frontend-old/app/components/features/schedule/ScheduleCalendar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import ScheduleDayCard from "@/components/features/schedule/dayCard/ScheduleDayCard"; -import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; -import {useFetchUsers} from "@/hooks/useFetchUsers"; -import Spinner from "@/components/Spinner"; - -const generateDates = (startDate: string, days: number): string[] => { - const dates = []; - const start = new Date(startDate); - - for (let i = 0; i < days; i++) { - const currentDate = new Date(start); - currentDate.setDate(start.getDate() + i); - dates.push(currentDate.toISOString().split('T')[0]); - } - - return dates; -}; - - -const fillCalendar = (schedules: ScheduleType[]): FilledScheduleType[] => { - /* -Array(14) - 0: - date: "2025-05-05" - id: 2 - is_skipped: false - scheduled_user_dishes: [] - */ - - const dates = generateDates((new Date()).toISOString().split('T')[0], 31) - - return dates.map((date): FilledScheduleType => { - console.log(date) - - const schedule = schedules.find((schedule: ScheduleType) => schedule.date == date) - - if (schedule) { - return schedule - } - - return { - date, - scheduled_user_dishes: [] - } - }) -} - -interface Props { - schedule: ScheduleType[]; -} - -const ScheduleCalendar = ({ schedule }: Props) => { - const {users, isLoading: areUsersLoading} = useFetchUsers(); - - if (areUsersLoading) return - - const fullCalendar = fillCalendar(schedule) - - return ( -
- { fullCalendar.map((schedule, index) => ( - - ))} -
- ) -} - -export default ScheduleCalendar \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/ScheduleEditForm.tsx b/frontend-old/app/components/features/schedule/ScheduleEditForm.tsx deleted file mode 100644 index df5db7b..0000000 --- a/frontend-old/app/components/features/schedule/ScheduleEditForm.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { ScheduleType } from "@/types/ScheduleType"; -import Spinner from "@/components/Spinner"; -import PageTitle from "@/components/ui/PageTitle"; -import { getScheduleForDate, scheduleUserDish, updateScheduleForDate } from "@/utils/api/scheduleApi"; -import { UserDishType } from "@/types/ScheduledUserDishType"; -import Label from "@/components/ui/Label"; -import SectionTitle from "@/components/ui/SectionTitle"; -import { useFetchUsers } from "@/hooks/useFetchUsers"; -import { listUserDishes } from "@/utils/api/userDishApi"; -import scheduleBuilder from "@/utils/scheduleBuilder"; -import transformDate from "@/utils/dateBuilder"; -import { ChevronLeftIcon } from "@heroicons/react/16/solid"; -import Hr from "@/components/ui/Hr" -import Button from "@/components/ui/Button" - -interface Props { - date: string; -} - -const ScheduleEditForm = ({ date }: Props) => { - const [schedule, setSchedule] = useState() - const [userDishes, setUserDishes] = useState([]) - const [isScheduleLoading, setIsScheduleLoading] = useState(true); - const [areUserDishesLoading, setAreUserDishesLoading] = useState(true); - const { users } = useFetchUsers(); - - useEffect(() => { - getScheduleForDate(date) - .then((sched: ScheduleType) => setSchedule(sched)) - .finally(() => setIsScheduleLoading(false)) - }, [date]); - - - useEffect(() => { - listUserDishes() - .then((user_dishes: UserDishType[]) => setUserDishes(user_dishes)) - .finally(() => setAreUserDishesLoading(false)) - }, []); - - const handleSkipDay = () => { - updateScheduleForDate(date, true) - .then((schedule: ScheduleType) => { - setSchedule(schedule) - }) - } - - const handleUnskipDay = () => { - updateScheduleForDate(date, false) - .then((schedule: ScheduleType) => { - setSchedule(schedule) - }) - } - - const handleChange = (e: React.ChangeEvent, userId: number) => { - const userDishId = parseInt(e.currentTarget.value); - - if (userDishId === 0) { - scheduleUserDish(date, userId, null, true).then(() => window.location.reload()); - return; - } - - scheduleUserDish(date, userId, userDishId).then(() => window.location.reload()); - } - - if (isScheduleLoading || areUserDishesLoading || !schedule) { - return - } - - const scheduleData = scheduleBuilder(schedule, users, userDishes) - - return
-
-
- Edit Day -
-
- { transformDate(schedule.date) } -
-
- -
- - { - userDishes.length === 0 &&
-
No dishes found assigned to this user.
-
Go ahead and add some first, or choose to skip the day.
-
(dishes ={`>`} edit ={`>`} add user)
-
- } - - { schedule.is_skipped - ? - : ( - <> - { - scheduleData - .map((scheduleData) =>
-
{ scheduleData.user.name }
-
- -
-
) - } - - ) - } - -
Changes are saved automatically
- -
- -
- -
{ - schedule.is_skipped - ? - : - }
-
-
-} - -export default ScheduleEditForm \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/ScheduleRegenerateButton.tsx b/frontend-old/app/components/features/schedule/ScheduleRegenerateButton.tsx deleted file mode 100644 index 3a27044..0000000 --- a/frontend-old/app/components/features/schedule/ScheduleRegenerateButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState} from "react"; -import Modal from "@/components/ui/Modal"; -import ScheduleRegenerateForm from "@/components/features/schedule/ScheduleRegenerateForm"; -import {ArrowPathIcon} from "@heroicons/react/16/solid"; - -interface ScheduleRegenerateButtonProps { - onModalClose?: () => void; -} - -const ScheduleRegenerateButton = ({ onModalClose }: ScheduleRegenerateButtonProps) => { - const [open, setOpen] = useState(false); - - const handleCloseModal = () => { - setOpen(false) - if (onModalClose) { - onModalClose() - } - } - - const modalChildren = handleCloseModal()}/> - const buttonChild =
-
- - return -}; - -export default ScheduleRegenerateButton; \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/ScheduleRegenerateForm.tsx b/frontend-old/app/components/features/schedule/ScheduleRegenerateForm.tsx deleted file mode 100644 index 3a9a4bf..0000000 --- a/frontend-old/app/components/features/schedule/ScheduleRegenerateForm.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import {DialogTitle} from "@headlessui/react"; -import Toggle from "@/components/ui/Toggle"; -import {useEffect, useState} from "react"; -import {generateSchedule} from "@/utils/api/scheduleApi"; -import Alert from "@/components/ui/Alert"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; - -interface ScheduleRegenerateFormProps { - closeModal: () => void; -} - -const ScheduleRegenerateForm = ({closeModal}: ScheduleRegenerateFormProps) => { - const [overwrite, setOverwrite] = useState(false); - const [error, setError] = useState(""); - - useEffect(() => { - }, [overwrite]); - - const close = () => { - closeModal(); - } - - const handleToggle = () => { - setOverwrite(!overwrite) - } - - const handleSubmit = () => { - generateSchedule(overwrite) - .then(() => close()) - .catch((err) => setError(err)) - } - - return <> -
-
-
- - Regenerate Schedule - -
-
- { - error && { error } - } -
- -
-
- -
-
-
- - -
-
-
-
- handleSubmit()} - className="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-xs sm:ml-3 sm:w-auto" - > - Regenerate - - close()} - className="mt-3 inline-flex w-full justify-center rounded-md bg-gray-500 px-3 py-2 text-sm font-semibold text-gray-900 ring-1 shadow-xs border-secondary ring-inset sm:mt-0 sm:w-auto" - > - Cancel - -
- ; -}; - -export default ScheduleRegenerateForm; \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/UpcomingDishes.tsx b/frontend-old/app/components/features/schedule/UpcomingDishes.tsx deleted file mode 100644 index 99f69d4..0000000 --- a/frontend-old/app/components/features/schedule/UpcomingDishes.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { DateTime } from "luxon"; -import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar"; -import PageTitle from "@/components/ui/PageTitle"; -import Spinner from "@/components/Spinner"; -import { ScheduleType } from "@/types/ScheduleType"; -import { listSchedule } from "@/utils/api/scheduleApi"; -import OnboardingBanner from "@/components/features/OnboardingBanner" -import { useFetchUsers } from "@/hooks/useFetchUsers" -import { useFetchDishes } from "@/hooks/useFetchDishes" -import ScheduleRegenerateButton from "@/components/features/schedule/ScheduleRegenerateButton"; - -const UpcomingDishes = () => { - const [schedule, setSchedule] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - const today = DateTime.now().toFormat("yyyy-LL-dd"); - - const fetchSchedule = useCallback(() => { - setIsLoading(true); - listSchedule(today) - .then((dishes) => setSchedule(dishes)) - .finally(() => setIsLoading(false)); - }, [today]); - - useEffect(() => { - fetchSchedule(); - }, [fetchSchedule]); - - const { users, isLoading: areUsersLoading } = useFetchUsers(); - const { dishes, isLoading: areDishesLoading } = useFetchDishes(); - - if (isLoading || areUsersLoading || areDishesLoading) { - return ; - } - - if (users.length === 0 || dishes.length === 0) { - return - } - - return ( -
-
-
- Schedule -
-
- -
-
- { - !schedule || Object.keys(schedule).length === 0 - ?
No dishes scheduled
- : - } -
- ); -}; - -export default UpcomingDishes; \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/UserDishEditCard.tsx b/frontend-old/app/components/features/schedule/UserDishEditCard.tsx deleted file mode 100644 index 653efbd..0000000 --- a/frontend-old/app/components/features/schedule/UserDishEditCard.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import {FormEvent, useMemo, useState} from "react"; -import {DishType} from "@/types/DishType"; -import {ScheduledUserDishType} from "@/types/ScheduledUserDishType"; -import {updateScheduledUserDish} from "@/utils/api/scheduledUserDishesApi"; -import Alert from "@/components/ui/Alert"; -import classNames from "classnames"; - -interface Props { - scheduledUserDish: ScheduledUserDishType - allDishes: DishType[] -} - -const UserDishEditCard = ({ scheduledUserDish, allDishes }: Props) => { - const [selectedUserDishId, setSelectedUserDishId] = useState(scheduledUserDish.user_dish ? scheduledUserDish.user_dish.id : 0) - const [errorMessage, setErrorMessage] = useState("") - const [isSuccess, setIsSuccess] = useState(false); - - const selectStyle = classNames( - 'p-2', 'rounded', 'w-full', 'background-secondary', - 'focus:outline-none', - 'transition-[border-color] ease-out duration-1000', 'border-2', // Keep consistent base styles - { - 'border-green-500': isSuccess, // Green border when successful - 'border-red-500': !isSuccess && errorMessage !== "", // Red border when there's an error - 'border-secondary': !isSuccess && errorMessage === "", // Default border for neutral state - } - ) - - const handleOnChange = (e: FormEvent) => { - const userDishId = parseInt(e.currentTarget.value); - setSelectedUserDishId(userDishId); - - updateScheduledUserDish(scheduledUserDish.id, userDishId) - .then(() => { - setIsSuccess(false); - setTimeout(() => { - setIsSuccess(true); - setTimeout(() => setIsSuccess(false), 1000); - }, 0); - }) - .catch((error) => { - setErrorMessage(error); // Log API errors - }); - }; - - const filteredDishes = useMemo(() => - allDishes.filter((dish: DishType) => - dish.users.some((user) => user.id === scheduledUserDish.user_dish.user.id) - ), - [allDishes, scheduledUserDish.user_dish.user.id] - ) - - return ( -
-
{scheduledUserDish.user_dish.user.name}
- - { errorMessage !== "" && { errorMessage } } - - - -
- ); -}; - -export default UserDishEditCard; \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/dayCard/DateBadge.tsx b/frontend-old/app/components/features/schedule/dayCard/DateBadge.tsx deleted file mode 100644 index 1338f51..0000000 --- a/frontend-old/app/components/features/schedule/dayCard/DateBadge.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import {DateTime} from "luxon"; -import React from "react"; -import classNames from "classnames"; - -interface Props { - date: string - className?: string; -} - -const DateBadge = ({ className, date }: Props) => { - const isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd") - - const textStyle = classNames("inline font-bold", { - 'text-accent-blue': isToday, - 'text-secondary': !isToday, - }, className) - - return ( -
-
{DateTime.fromISO(date).toFormat("dd")}
-
-
{DateTime.fromISO(date).toFormat("LLL")}
-
- ) -} - -export default DateBadge \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCard.tsx b/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCard.tsx deleted file mode 100644 index 8c6b874..0000000 --- a/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCard.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import {UserType} from "~/types/UserType"; -import ScheduleDayCardUserDish from "~/components/features/schedule/dayCard/ScheduleDayCardUserDish"; -import { FilledScheduleType, ScheduleType } from "~/types/ScheduleType"; -import { Link } from "react-router"; -import {PencilSquareIcon} from "@heroicons/react/24/outline"; -import useRoutes from "~/hooks/useRoutes"; -import DateBadge from "~/components/features/schedule/dayCard/DateBadge"; -import { DateTime } from "luxon" -import classNames from "classnames" - -interface Props { - schedule: ScheduleType|FilledScheduleType; - users: UserType[]; -} - -const ScheduleDayCard = ({schedule, users}: Props) => { - const routes = useRoutes() - const isToday = DateTime.fromISO(schedule.date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd") - - const containerStyles = classNames( - 'w-full bg-gray-500 pt-5 pb-2 rounded-2xl text-xl', { - 'border-2 text-accent-blue border-accent-blue': isToday, - } - ) - - return ( -
- - -
- { - users.map((user) => ) - } - -
- - Edit - -
-
-
- ); -}; - -export default ScheduleDayCard; \ No newline at end of file diff --git a/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx b/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx deleted file mode 100644 index 35cf58d..0000000 --- a/frontend-old/app/components/features/schedule/dayCard/ScheduleDayCardUserDish.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { ScheduledUserDishType } from "@/types/ScheduledUserDishType"; -import { UserType } from "@/types/UserType"; -import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; - -interface Props { - schedule: ScheduleType|FilledScheduleType; - user: UserType; -} - -const ScheduleDayCardUserDish = ({ schedule, user }: Props) => { - const getDish = (user: UserType) => { - const scheduled_dishes = schedule.scheduled_user_dishes.filter((scheduled_user_dish: ScheduledUserDishType) => ( - scheduled_user_dish.user_dish?.user.id == user.id - )) - - if (scheduled_dishes.length > 0) { - return scheduled_dishes[0].user_dish.dish.name - } - - return '/' - } - - return ( -
-
{ user.name } :
-
{ getDish(user) }
-
- ); -}; - -export default ScheduleDayCardUserDish; \ No newline at end of file diff --git a/frontend-old/app/components/features/users/EditUserForm.tsx b/frontend-old/app/components/features/users/EditUserForm.tsx deleted file mode 100644 index e26cf58..0000000 --- a/frontend-old/app/components/features/users/EditUserForm.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, {useState} from "react"; -import { useNavigate } from "react-router"; -import useRoutes from "~/hooks/useRoutes"; -import {updateUser} from "~/utils/api/usersApi"; -import PageTitle from "~/components/ui/PageTitle"; -import { Link } from "react-router"; -import Alert from "~/components/ui/Alert"; -import {UserType} from "~/types/UserType"; -import SolidButton from "~/components/ui/Buttons/SolidButton"; - -interface Props { - user: UserType; -} - -const EditUserForm = ({ user }: Props) => { - - const [name, setName] = useState(user.name); - const [error, setError] = useState(''); - const navigate = useNavigate(); - const routes = useRoutes(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - // validateName - if (!name.trim()) { - setError('Name cannot be empty.'); - return; - } - - updateUser(user, name) - .then(() => { - navigate(routes.user.index()) - }) - } - - return ( -
- Create User - Back to users - -
- { - error != '' && { error } - } - - - setName(e.target.value)} - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" - /> - - Update -
-
- ); -} - -export default EditUserForm; \ No newline at end of file diff --git a/frontend-old/app/components/layout/AuthGuard.tsx b/frontend-old/app/components/layout/AuthGuard.tsx deleted file mode 100644 index 9f6cf29..0000000 --- a/frontend-old/app/components/layout/AuthGuard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAuth } from '@/context/AuthContext'; -import React, { useEffect } from 'react'; -import { useLocation, useNavigate } from "react-router" - -export default function AuthGuard({ children }: { children: React.ReactNode }) { - const { isAuthenticated } = useAuth(); - const navigate = useNavigate(); - const location = useLocation(); - - const publicRoutes = ['/login', '/register']; - const isPublic = publicRoutes.includes(location.pathname); - - useEffect(() => { - // Handle redirects based on auth state - if (isAuthenticated && isPublic) { - // Redirect authenticated users away from public pages - navigate('/', { replace: true }); - } else if (!isAuthenticated && !isPublic) { - // Redirect unauthenticated users trying to access protected pages - navigate('/login', { replace: true }); - } - }, [isAuthenticated, location.pathname, isPublic, navigate]); - - // Render children for all routes - redirects will happen via useEffect - return <>{children}; -} \ No newline at end of file diff --git a/frontend-old/app/components/layout/Card.tsx b/frontend-old/app/components/layout/Card.tsx deleted file mode 100644 index 4491cab..0000000 --- a/frontend-old/app/components/layout/Card.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -interface Props { - children: React.ReactNode; -} - -const Card = ({ children }: Props) => { - return ( -
- { children } -
- ) -} - -export default Card \ No newline at end of file diff --git a/frontend-old/app/components/layout/NavBar.tsx b/frontend-old/app/components/layout/NavBar.tsx deleted file mode 100644 index 05d71a9..0000000 --- a/frontend-old/app/components/layout/NavBar.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useState } from "react"; -import { Link, useNavigate } from "react-router"; -import useRoutes from "~/hooks/useRoutes"; -import MobileDropdownMenu from "~/components/features/navbar/MobileDropdownMenu"; -import { useAuth } from "~/context/AuthContext"; - -const NavBar = () => { - const [isOpen, setIsOpen] = useState(false); - const routes = useRoutes(); - const navigate = useNavigate(); - const {isAuthenticated, logout} = useAuth(); - - const handleLogout = (e: React.MouseEvent) => { - e.preventDefault(); - logout(); - navigate('/login', { replace: true }); - }; - - return ( - - ); -}; - -export default NavBar; \ No newline at end of file diff --git a/frontend-old/app/components/pages/PrivatePage.tsx b/frontend-old/app/components/pages/PrivatePage.tsx deleted file mode 100644 index 3b08c86..0000000 --- a/frontend-old/app/components/pages/PrivatePage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const PrivatePage = () => { - return ( -
- private -
- ) -} - -export default PrivatePage \ No newline at end of file diff --git a/frontend-old/app/components/pages/PublicPage.tsx b/frontend-old/app/components/pages/PublicPage.tsx deleted file mode 100644 index 8e0f5cb..0000000 --- a/frontend-old/app/components/pages/PublicPage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Outlet } from "react-router" - -const PublicPage = () => { - return ( -
- -
- ) -} - -export default PublicPage \ No newline at end of file diff --git a/frontend-old/app/components/ui/Alert.tsx b/frontend-old/app/components/ui/Alert.tsx deleted file mode 100644 index 9f08022..0000000 --- a/frontend-old/app/components/ui/Alert.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react" -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - type: 'error' | 'warning' | 'info' | 'success'; -} - -const Alert = ({ children, className, type }: Props) => { - let bgColor = 'bg-blue-200' - let fgColor = 'bg-blue-800' - - if (type == 'error') { - bgColor = 'bg-red-200' - fgColor = 'bg-red-800' - } else if (type == 'warning') { - bgColor = 'bg-orange-200' - fgColor = 'bg-orange-800' - } else if (type == 'success') { - bgColor = 'border-2 border-green-500' - fgColor = 'text-green-500' - } - - const styles = classNames(fgColor, bgColor, className, 'rounded') - - return ( -
- { children} -
- ) -} - -export default Alert \ No newline at end of file diff --git a/frontend-old/app/components/ui/Button.tsx b/frontend-old/app/components/ui/Button.tsx deleted file mode 100644 index 756e6b5..0000000 --- a/frontend-old/app/components/ui/Button.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Link } from "react-router"; -import React from "react"; -import classNames from "classnames"; - -interface ButtonProps { - appearance?: 'solid' | 'outline' | 'text'; - children: React.ReactNode; - className?: string; - href?: string; - icon?: React.ReactNode; - onClick?: () => void; - disabled?: boolean; - size?: 'small' | 'medium' | 'large'; - type?: 'button' | 'submit' | 'reset'; - variant?: 'primary' | 'secondary' | 'accent'; -} - -const Button = ({ appearance, children, className, disabled, href, icon, onClick, - size = 'medium', type, - variant = 'primary' -}: ButtonProps) => { - const styles = classNames( - "flex items-center space-x-1", - "justify-center font-size-18 py-2 px-4 rounded flex", - { - 'border-2 border-primary background-red text-white': variant === 'primary' && appearance === 'solid', - 'border-2 border-primary text-primary': variant === 'primary' && appearance === 'outline', - 'text-primary': variant === 'primary' && appearance === 'text', - 'border-2 border-secondary text-secondary': variant === 'secondary' && appearance === 'outline', - 'border-2 border-accent-blue text-accent-blue': variant === 'accent' && appearance === 'outline', - }, - className - ) - - const iconClassNames = classNames({ - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as React.ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - if (href !== undefined) { - return ( - - { icon && iconElement} - { children} - - ) - } - - return -} - -export default Button \ No newline at end of file diff --git a/frontend-old/app/components/ui/Buttons/OutlineButton.tsx b/frontend-old/app/components/ui/Buttons/OutlineButton.tsx deleted file mode 100644 index 5530482..0000000 --- a/frontend-old/app/components/ui/Buttons/OutlineButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - disabled?: boolean; - onClick?: () => void; - size?: "small" | "medium" | "large"; - type: 'submit' | 'button'; -} - -const OutlineButton = ({ children, className, disabled = false, onClick, size, type }: Props) => { - const style = classNames( - "justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex", - { 'text-xs': size === "small" }, - className - ) - - if (onClick === undefined) { - onClick = () => { - } - } - - return ( - - ) -} - -export default OutlineButton \ No newline at end of file diff --git a/frontend-old/app/components/ui/Buttons/OutlineLinkButton.tsx b/frontend-old/app/components/ui/Buttons/OutlineLinkButton.tsx deleted file mode 100644 index 6032bc3..0000000 --- a/frontend-old/app/components/ui/Buttons/OutlineLinkButton.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import classNames from "classnames"; -import { Link } from "react-router" - -interface Props { - children: React.ReactNode; - className?: string; - href: string; - icon?: React.ReactNode; - size?: "small" | "medium" | "large"; - variant?: "primary" | "secondary"; -} - -const OutlineLinkButton = ({ children, className, href, icon, size = "medium", variant }: Props) => { - const linkClassNames = classNames( - "underline font-default pt-3 pb-3 px-4 rounded mb-0 flex", - { - 'text-primary border-primary': variant === "primary", - 'text-secondary border-secondary': variant === "secondary", - 'text-accent-blue border-accent': !variant || !["primary", "secondary"].includes(variant), - }, { - 'text-size-14': size === "small", - 'font-size-18': !size || size === "medium", - 'text-2xl': size === "large", - }, - className, - ) - - const iconClassNames = classNames("mt-0.5", { - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", // Default size - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as React.ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - return ( - - {iconElement} - {children} - - ) -} - -export default OutlineLinkButton \ No newline at end of file diff --git a/frontend-old/app/components/ui/Buttons/SolidButton.tsx b/frontend-old/app/components/ui/Buttons/SolidButton.tsx deleted file mode 100644 index dffc021..0000000 --- a/frontend-old/app/components/ui/Buttons/SolidButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - disabled?: boolean; - onClick?: () => void; - size?: "small" | "medium" | "large"; - type: 'submit' | 'button'; -} - -const SolidButton = ({ children, className, disabled = false, onClick, size, type }: Props) => { - const style = classNames( - "py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0", - { - 'text-xs': size === "small", - 'font-size-18': !size || size === "medium", - }, - className - ) - - if (onClick === undefined) { - onClick = () => { - } - } - - return ( - - ) -} - -export default SolidButton \ No newline at end of file diff --git a/frontend-old/app/components/ui/Buttons/SolidLinkButton.tsx b/frontend-old/app/components/ui/Buttons/SolidLinkButton.tsx deleted file mode 100644 index ce4929c..0000000 --- a/frontend-old/app/components/ui/Buttons/SolidLinkButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import classNames from "classnames"; -import { Link } from "react-router" - -interface Props { - children: React.ReactNode; - className?: string; - href: string; - icon?: React.ReactNode; - size?: "small" | "medium" | "large"; - variant?: "primary" | "secondary"; -} - -const SolidLinkButton = ({ children, className, href, icon, size = "medium", variant }: Props) => { - const style = classNames( - "py-2 px-4 text-xl p-2 rounded hover:bg-secondary mb-0 text-center flex", - { - 'background-red text-white': variant === "primary", - 'background-secondary border-2 border-secondary': variant === "secondary", - }, - className - ) - - const iconClassNames = classNames("mt-1", { - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", // Default size - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as React.ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - return ( - -
- {iconElement} - {children} -
- - ) -} - -export default SolidLinkButton \ No newline at end of file diff --git a/frontend-old/app/components/ui/Hr.tsx b/frontend-old/app/components/ui/Hr.tsx deleted file mode 100644 index f1eee60..0000000 --- a/frontend-old/app/components/ui/Hr.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import classNames from "classnames" - -interface HrProps { - className?: string; -} - -const Hr = ({ className }: HrProps) => { - const styles = classNames("my-4 border-secondary", className) - - return
-} - -export default Hr \ No newline at end of file diff --git a/frontend-old/app/components/ui/Label.tsx b/frontend-old/app/components/ui/Label.tsx deleted file mode 100644 index a62d4e7..0000000 --- a/frontend-old/app/components/ui/Label.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -interface LabelProps { - href?: string; - children: React.ReactNode; - onClick?: () => void; -} - -const Label = ({ href, children, onClick }: LabelProps) => { - const styles = "items-center space-x-1 background-accent p-2 rounded" - - if (href !== undefined) { - return ( -
- { children} -
- ) - } - - return -} - -export default Label \ No newline at end of file diff --git a/frontend-old/app/components/ui/Modal.tsx b/frontend-old/app/components/ui/Modal.tsx deleted file mode 100644 index e60fdb5..0000000 --- a/frontend-old/app/components/ui/Modal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { type JSX } from "react"; -import classNames from "classnames"; -import Button from "@/components/ui/Button" - -interface ModalProps { - buttonChildren?: JSX.Element; - buttonClassName?: string; - buttonLabel?: string; - modalChildren: JSX.Element; - modalOpen?: boolean; - setModalOpen: (open: boolean) => void; -} - -const Modal = ({ - buttonLabel, - buttonClassName, - modalChildren, - modalOpen, - buttonChildren, - setModalOpen, -}: ModalProps) => { - const buttonStyles = classNames(buttonClassName, 'anta-regular'); - - const closeModal = () => { - setModalOpen(false) - } - - return ( - <> - - - {/**/} - {/* */} - {/*
*/} - {/* */} - {/* */} - {/* closeModal()}/>*/} - {/* {modalChildren}*/} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - - ) -} - -export default Modal; \ No newline at end of file diff --git a/frontend-old/app/components/ui/PageTitle.tsx b/frontend-old/app/components/ui/PageTitle.tsx deleted file mode 100644 index 569b2f3..0000000 --- a/frontend-old/app/components/ui/PageTitle.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import classNames from "classnames"; - -interface Props { - children: string, - className?: string, -} - -const PageTitle = ({ children, className }: Props) => { - const styles = classNames( - 'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold', - className, - ) - - return

{ children }

-} - -export default PageTitle \ No newline at end of file diff --git a/frontend-old/app/components/ui/RecurrenceInput.tsx b/frontend-old/app/components/ui/RecurrenceInput.tsx deleted file mode 100644 index 0d82560..0000000 --- a/frontend-old/app/components/ui/RecurrenceInput.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, {useState} from "react"; - -interface Props { - value: number; - setValue: (value: number) => void; -} - -const RecurrenceInput = ({ value, setValue}: Props) => { - const [openInput, setOpenInput] = useState<'category' | 'number'>([7, 365].includes(value) ? 'category' : 'number') - - const toggleInput = (e: React.MouseEvent) => { - e.preventDefault() - setOpenInput(openInput == 'category' ? 'number' : 'category') - } - - const toggleButton = () => { - return ( - - ) - } - - const prepareValue = (v: string) => { - setValue(parseInt(v)) - } - - return ( -
-
- - - { toggleButton() } -
- -
- - prepareValue(e.target.value)} - className="p-2 border rounded w-full bg-gray-500 border-secondary" - /> - { toggleButton() } -
-
- ) -} - -export default RecurrenceInput \ No newline at end of file diff --git a/frontend-old/app/components/ui/SectionTitle.tsx b/frontend-old/app/components/ui/SectionTitle.tsx deleted file mode 100644 index 93d13c4..0000000 --- a/frontend-old/app/components/ui/SectionTitle.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import classNames from "classnames"; - -interface Props { - children: string; - className?: string; -} - -const SectionTitle = ({ children, className }: Props) => { - const style = classNames("block font-size-18 uppercase w-full pl-2 text-accent-blue", - className - ) - - return

{ children }

-} - -export default SectionTitle \ No newline at end of file diff --git a/frontend-old/app/components/ui/Toggle.tsx b/frontend-old/app/components/ui/Toggle.tsx deleted file mode 100644 index 0d6b9e9..0000000 --- a/frontend-old/app/components/ui/Toggle.tsx +++ /dev/null @@ -1,41 +0,0 @@ - -interface ToggleProps { - checked: boolean; - onChange: (checked: boolean) => void; -} - -const Toggle = ({ checked, onChange }: ToggleProps) => { - const handleChange = () => { - onChange(checked); - } - - return ( - - ); -}; - -export default Toggle; \ No newline at end of file diff --git a/frontend-old/app/context/AuthContext.tsx b/frontend-old/app/context/AuthContext.tsx deleted file mode 100644 index 1cd3db1..0000000 --- a/frontend-old/app/context/AuthContext.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; - -interface AuthContextProps { - isAuthenticated: boolean; - login: () => void; - logout: () => void; -} - -const AuthContext = createContext({ - isAuthenticated: false, - login: () => {}, - logout: () => {}, -}); - -export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - // Start with false during SSR, will be updated on client - const [isAuthenticated, setIsAuthenticated] = useState(false); - - useEffect(() => { - // Check token on client after mount - const token = localStorage.getItem('token'); - setIsAuthenticated(!!token); - }, []); - - const login = () => { - setIsAuthenticated(true); - }; - - const logout = () => { - setIsAuthenticated(false); - localStorage.removeItem('token'); - }; - - return ( - - {children} - - ); -}; - -export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/frontend-old/app/hooks/useRoutes.ts b/frontend-old/app/hooks/useRoutes.ts deleted file mode 100644 index 255a545..0000000 --- a/frontend-old/app/hooks/useRoutes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { DishType } from "@/types/DishType"; -import type { UserType } from "@/types/UserType"; - -const useRoutes = () => { - return { - home: () => "/", - auth: { - login: () => "/login", - register: () => "/register", - }, - dish: { - index: () => "/dishes", - create: () => "/dishes/create", - edit: (dish: DishType) => `/dishes/${ dish.id }/edit`, - delete: (dish: DishType) => `/dishes/${ dish.id }/delete`, - }, - schedule: { - date: { - edit: (date: string) => `/schedule/${ date }/edit` - }, - history: () => "/scheduled-user-dishes/history", - }, - user: { - index: () => "/users", - create: () => `/users/create`, - edit: (user: UserType) => `/users/${ user.id }/edit`, - delete: (user: UserType) => `/users/${ user.id }/delete`, - } - }; -}; - -export default useRoutes; \ No newline at end of file diff --git a/frontend-old/app/root.tsx b/frontend-old/app/root.tsx deleted file mode 100644 index baa343e..0000000 --- a/frontend-old/app/root.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; - -import type { Route } from "./+types/root"; -import "./app.css"; -import React from "react" -import { AuthProvider } from "~/context/AuthContext" -import AuthGuard from "~/components/layout/AuthGuard" -import NavBar from "~/components/layout/NavBar" - -export const links: Route.LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - - - -
{ children }
-
-
- - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = "Oops!"; - let details = "An unexpected error occurred."; - let stack: string | undefined; - - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? "404" : "Error"; - details = - error.status === 404 - ? "The requested page could not be found." - : error.statusText || details; - } else if (import.meta.env.DEV && error && error instanceof Error) { - details = error.message; - stack = error.stack; - } - - return ( -
-

{ message }

-

{ details }

- { stack && ( -
-          { stack }
-        
- ) } -
- ); -} diff --git a/frontend-old/app/routes.ts b/frontend-old/app/routes.ts deleted file mode 100644 index 8a4afe8..0000000 --- a/frontend-old/app/routes.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type RouteConfig, index, route } from "@react-router/dev/routes"; - -export default [ - index("routes/home.tsx"), - route("login", "./components/features/auth/LoginForm.tsx"), - route("register", "./components/features/auth/RegisterForm.tsx"), - - // Dishes routes - route("dishes", "routes/dishes.tsx"), - route("dishes/create", "routes/dishes.create.tsx"), - route("dishes/:id/edit", "routes/dishes.$id.edit.tsx"), - - // Users routes - route("users", "routes/users.tsx"), - route("users/create", "routes/users.create.tsx"), - route("users/:id/edit", "routes/users.$id.edit.tsx"), - - // Schedule routes - route("schedule/:date/edit", "routes/schedule.$date.edit.tsx"), - - // History route - route("scheduled-user-dishes/history", "routes/scheduled-user-dishes.history.tsx"), -] satisfies RouteConfig; diff --git a/frontend-old/app/routes/dishes.$id.edit.tsx b/frontend-old/app/routes/dishes.$id.edit.tsx deleted file mode 100644 index a8a3fa0..0000000 --- a/frontend-old/app/routes/dishes.$id.edit.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Route } from "./+types/dishes.$id.edit"; -import { useCallback, useEffect, useState } from "react"; -import PageTitle from "~/components/ui/PageTitle"; -import EditDishForm from "~/components/features/dishes/EditDishForm"; -import { DishType } from "~/types/DishType"; -import Spinner from "~/components/Spinner"; -import { fetchDish } from "~/utils/api/dishApi"; -import SyncUsersForm from "~/components/features/dishes/SyncUsersForm"; -import { ChevronLeftIcon } from "@heroicons/react/16/solid"; -import useRoutes from "~/hooks/useRoutes"; -import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton"; -import Hr from "~/components/ui/Hr"; -import { useParams } from "react-router"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Edit Dish" }, - { name: "description", content: "Edit dish details" }, - ]; -} - -export default function EditDishPage() { - const params = useParams(); - const id = Number(params.id); - const [dish, setDish] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const routes = useRoutes(); - - const loadDish = useCallback(async () => { - try { - const fetchedDish = await fetchDish(id); - setDish(fetchedDish); - } catch (error) { - console.error("Error fetching dish:", error); - throw new Error("No token found in localStorage."); - } finally { - setIsLoading(false); - } - }, [id]); - - useEffect(() => { - loadDish(); - }, [loadDish]); - - if (isLoading || dish === null) { - return ; - } - - return ( -
-
- Edit Dish - - -

BACK

-
-
- - - -
- - -
- ); -} diff --git a/frontend-old/app/routes/dishes.create.tsx b/frontend-old/app/routes/dishes.create.tsx deleted file mode 100644 index a43e88d..0000000 --- a/frontend-old/app/routes/dishes.create.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Route } from "./+types/dishes.create"; -import CreateDishForm from "~/components/features/dishes/CreateDishForm"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Create Dish" }, - { name: "description", content: "Create a new dish" }, - ]; -} - -export default function CreateDishPage() { - return ; -} diff --git a/frontend-old/app/routes/dishes.tsx b/frontend-old/app/routes/dishes.tsx deleted file mode 100644 index 7bed7de..0000000 --- a/frontend-old/app/routes/dishes.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { Route } from "./+types/dishes"; -import PageTitle from "~/components/ui/PageTitle"; -import { DishType } from "~/types/DishType"; -import Dish from "~/components/features/dishes/Dish"; -import { PlusIcon } from "@heroicons/react/24/solid"; -import { useEffect, useState } from "react"; -import useRoutes from "~/hooks/useRoutes"; -import { listDishes } from "~/utils/api/dishApi"; -import Button from "~/components/ui/Button"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Dishes" }, - { name: "description", content: "Manage your dishes" }, - ]; -} - -export default function DishesIndexPage() { - const routes = useRoutes(); - - const [dishes, setDishes] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - listDishes() - .then((dishes: DishType[]) => setDishes(dishes)) - .finally(() => setLoading(false)); - }, []); - - if (loading) return

Loading...

; - - if (!dishes) { - return

Loading...

; - } - - return ( - <> -
-
- Dishes -
-
- -
-
- - {dishes.length === 0 ? ( -

No dishes found :(

- ) : ( - dishes.map((dish: DishType, index: number) => ( - - )) - )} - - ); -} diff --git a/frontend-old/app/routes/home.tsx b/frontend-old/app/routes/home.tsx deleted file mode 100644 index a1c6ee6..0000000 --- a/frontend-old/app/routes/home.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Route } from "./+types/home"; -import UpcomingDishes from "~/components/features/schedule/UpcomingDishes"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Schedule" }, - { name: "description", content: "View and manage your upcoming dish schedule" }, - ]; -} - -export default function Home() { - return ; -} diff --git a/frontend-old/app/routes/schedule.$date.edit.tsx b/frontend-old/app/routes/schedule.$date.edit.tsx deleted file mode 100644 index 4122f13..0000000 --- a/frontend-old/app/routes/schedule.$date.edit.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Route } from "./+types/schedule.$date.edit"; -import ScheduleEditForm from "~/components/features/schedule/ScheduleEditForm"; -import { useParams } from "react-router"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Edit Schedule" }, - { name: "description", content: "Edit schedule for a specific date" }, - ]; -} - -const ScheduleEditPage = () => { - const params = useParams(); - const date = params.date as string; - - return ; -}; - -export default ScheduleEditPage; diff --git a/frontend-old/app/routes/scheduled-user-dishes.history.tsx b/frontend-old/app/routes/scheduled-user-dishes.history.tsx deleted file mode 100644 index 591f015..0000000 --- a/frontend-old/app/routes/scheduled-user-dishes.history.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Route } from "./+types/scheduled-user-dishes.history"; -import HistoricalDishes from "~/components/features/schedule/HistoricalDishes"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - History" }, - { name: "description", content: "View historical dishes" }, - ]; -} - -export default function HistoryPage() { - return ; -} diff --git a/frontend-old/app/routes/users.$id.edit.tsx b/frontend-old/app/routes/users.$id.edit.tsx deleted file mode 100644 index 2b784ed..0000000 --- a/frontend-old/app/routes/users.$id.edit.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { Route } from "./+types/users.$id.edit"; -import { useEffect, useState } from "react"; -import { UserType } from "~/types/UserType"; -import { showUser } from "~/utils/api/usersApi"; -import Spinner from "~/components/Spinner"; -import EditUserForm from "~/components/features/users/EditUserForm"; -import { useParams } from "react-router"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Edit User" }, - { name: "description", content: "Edit user details" }, - ]; -} - -const UpdateUsersPage = () => { - const params = useParams(); - const id = Number(params.id); - const [user, setUser] = useState(null); - - useEffect(() => { - showUser(id).then((user: UserType) => setUser(user)); - }, [id]); - - if (!user) { - return ; - } - - return ; -}; - -export default UpdateUsersPage; diff --git a/frontend-old/app/routes/users.create.tsx b/frontend-old/app/routes/users.create.tsx deleted file mode 100644 index 34feb54..0000000 --- a/frontend-old/app/routes/users.create.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { Route } from "./+types/users.create"; -import PageTitle from "~/components/ui/PageTitle"; -import useRoutes from "~/hooks/useRoutes"; -import { useNavigate } from "react-router"; -import { useState } from "react"; -import Alert from "~/components/ui/Alert"; -import { createUser } from "~/utils/api/usersApi"; -import { Link } from "react-router"; -import SolidButton from "~/components/ui/Buttons/SolidButton"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Create User" }, - { name: "description", content: "Create a new user" }, - ]; -} - -const CreateUsersPage = () => { - const [name, setName] = useState(""); - const [error, setError] = useState(""); - const navigate = useNavigate(); - const routes = useRoutes(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!name.trim()) { - setError("Name cannot be empty."); - return; - } - - createUser(name).then(() => { - navigate(routes.user.index()); - }); - }; - - return ( -
- Create User - - Back to users - - -
- {error != "" && ( - - {error} - - )} - - - setName(e.target.value)} - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" - /> - - - Create - -
-
- ); -}; - -export default CreateUsersPage; diff --git a/frontend-old/app/routes/users.tsx b/frontend-old/app/routes/users.tsx deleted file mode 100644 index a8a6039..0000000 --- a/frontend-old/app/routes/users.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { Route } from "./+types/users"; -import PageTitle from "~/components/ui/PageTitle"; -import { useFetchUsers } from "~/hooks/useFetchUsers"; -import Spinner from "~/components/Spinner"; -import useRoutes from "~/hooks/useRoutes"; -import { Link } from "react-router"; -import { PencilIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid"; -import React from "react"; -import { deleteUser } from "~/utils/api/usersApi"; -import { UserType } from "~/types/UserType"; -import Card from "~/components/layout/Card"; -import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Dish Planner - Users" }, - { name: "description", content: "Manage your users" }, - ]; -} - -const UsersPage = () => { - const { users, isLoading } = useFetchUsers(); - const routes = useRoutes(); - - const handleDelete = (user: UserType) => { - deleteUser(user).then(() => window.location.reload()); - }; - - if (isLoading) { - return ; - } - - const usersList = () => { - return users.map((user) => ( - -
{user.name}
-
-
- -
- -
- -
-
- handleDelete(user)}> -
- -
- -
-
-
- )); - }; - - return ( -
-
-
- Users -
- -
- - -

Add User

-
-
-
- - {users && users.length > 0 ? usersList() :
No users
} -
- ); -}; - -export default UsersPage; diff --git a/frontend-old/app/styles/components/buttons.css b/frontend-old/app/styles/components/buttons.css deleted file mode 100644 index 07fc4de..0000000 --- a/frontend-old/app/styles/components/buttons.css +++ /dev/null @@ -1,42 +0,0 @@ -.button-primary-solid { - background-color: var(--color-primary); - color: var(--color-secondary-200); - border: 1px solid var(--color-primary); - text-transform: uppercase; - font-family: "Anta", serif; - font-style: normal; - font-size: 1.1rem; - font-weight: 600; - padding: 4px 16px 2px 16px; -} -.button-primary-outline { - background-color: var(--color-background); - color: var(--color-primary); - border: 1px solid var(--color-primary); - text-transform: uppercase; - font-family: "Anta", serif; - font-style: normal; - font-size: 1.1rem; - font-weight: 600; - padding: 4px 16px 2px 16px; -} - -.button-secondary-solid { - background-color: var(--color-secondary); - color: var(--color-primary); - border: 1px solid var(--color-secondary); -} - -.button-accent-solid { - background-color: var(--color-accent-blue); - color: var(--color-secondary-900); - border: 1px solid var(--color-accent-blue); -} -.button-accent-outline { - background-color: var(--color-background); - color: var(--color-accent-blue); - border: 1px solid var(--color-accent-blue); -} -.button-accent-outline:hover { - background-color: var(--color-background-400); -} \ No newline at end of file diff --git a/frontend-old/app/styles/main.css b/frontend-old/app/styles/main.css deleted file mode 100644 index 83d03cc..0000000 --- a/frontend-old/app/styles/main.css +++ /dev/null @@ -1,6 +0,0 @@ -@import "./theme/borders.css"; -@import "./theme/fonts.css"; -@import "./components/buttons.css"; - -@import "./base/globals.css"; -@import "./theme/colors.css"; diff --git a/frontend-old/app/styles/theme/colors.css b/frontend-old/app/styles/theme/colors.css deleted file mode 100644 index f283897..0000000 --- a/frontend-old/app/styles/theme/colors.css +++ /dev/null @@ -1,10 +0,0 @@ -@import './colors/root.css'; -@import './colors/background.css'; -@import './colors/border.css'; -@import './colors/text.css'; - -body { - color: var(--color-secondary) !important; - background: var(--color-gray-600) !important; -} - diff --git a/frontend-old/app/styles/theme/colors/text.css b/frontend-old/app/styles/theme/colors/text.css deleted file mode 100644 index ca38334..0000000 --- a/frontend-old/app/styles/theme/colors/text.css +++ /dev/null @@ -1,216 +0,0 @@ -.text-primary { - color: var(--color-primary); -} -.text-primary-100 { - color: var(--color-primary-100); -} -.text-primary-200 { - color: var(--color-primary-200); -} -.text-primary-300 { - color: var(--color-primary-300); -} -.text-primary-400 { - color: var(--color-primary-400); -} -.text-primary-500 { - color: var(--color-primary-500); -} -.text-primary-600 { - color: var(--color-primary-600); -} -.text-primary-700 { - color: var(--color-primary-700); -} -.text-primary-800 { - color: var(--color-primary-800); -} -.text-primary-900 { - color: var(--color-primary-900); -} - -.text-secondary { - color: var(--color-secondary) !important; -} -.text-secondary-100 { - color: var(--color-secondary-100); -} -.text-secondary-200 { - color: var(--color-secondary-200); -} -.text-secondary-300 { - color: var(--color-secondary-300); -} -.text-secondary-400 { - color: var(--color-secondary-400); -} -.text-secondary-500 { - color: var(--color-secondary-500); -} -.text-secondary-600 { - color: var(--color-secondary-600); -} -.text-secondary-700 { - color: var(--color-secondary-700); -} -.text-secondary-800 { - color: var(--color-secondary-800); -} -.text-secondary-900 { - color: var(--color-secondary-900); -} - -.text-accent-blue { - color: var(--color-accent-blue); -} -.text-accent-blue-100 { - color: var(--color-accent-blue-100); -} -.text-accent-blue-200 { - color: var(--color-accent-blue-200); -} -.text-accent-blue-300 { - color: var(--color-accent-blue-300); -} -.text-accent-blue-400 { - color: var(--color-accent-blue-400); -} -.text-accent-blue-500 { - color: var(--color-accent-blue-500); -} -.text-accent-blue-600 { - color: var(--color-accent-blue-600); -} -.text-accent-blue-700 { - color: var(--color-accent-blue-700); -} -.text-accent-blue-800 { - color: var(--color-accent-blue-800); -} -.text-accent-blue-900 { - color: var(--color-accent-blue-900); -} - -.text-accent-yellow { - color: var(--color-accent-yellow); -} -.text-accent-yellow-100 { - color: var(--color-accent-yellow-100); -} -.text-accent-yellow-200 { - color: var(--color-accent-yellow-200); -} -.text-accent-yellow-300 { - color: var(--color-accent-yellow-300); -} -.text-accent-yellow-400 { - color: var(--color-accent-yellow-400); -} -.text-accent-yellow-500 { - color: var(--color-accent-yellow-500); -} -.text-accent-yellow-600 { - color: var(--color-accent-yellow-600); -} -.text-accent-yellow-700 { - color: var(--color-accent-yellow-700); -} -.text-accent-yellow-800 { - color: var(--color-accent-yellow-800); -} -.text-accent-yellow-900 { - color: var(--color-accent-yellow-900); -} - -.text-danger { - color: var(--color-danger); -} -.text-danger-100 { - color: var(--color-danger-100); -} -.text-danger-200 { - color: var(--color-danger-200); -} -.text-danger-300 { - color: var(--color-danger-300); -} -.text-danger-400 { - color: var(--color-danger-400); -} -.text-danger-500 { - color: var(--color-danger-500); -} -.text-danger-600 { - color: var(--color-danger-600); -} -.text-danger-700 { - color: var(--color-danger-700); -} -.text-danger-800 { - color: var(--color-danger-800); -} -.text-danger-900 { - color: var(--color-danger-900); -} - -.text-warning { - color: var(--color-warning); -} -.text-warning-100 { - color: var(--color-warning-100); -} -.text-warning-200 { - color: var(--color-warning-200); -} -.text-warning-300 { - color: var(--color-warning-300); -} -.text-warning-400 { - color: var(--color-warning-400); -} -.text-warning-500 { - color: var(--color-warning-500); -} -.text-warning-600 { - color: var(--color-warning-600); -} -.text-warning-700 { - color: var(--color-warning-700); -} -.text-warning-800 { - color: var(--color-warning-800); -} -.text-warning-900 { - color: var(--color-warning-900); -} - -.text-success { - color: var(--color-success); -} -.text-success-100 { - color: var(--color-success-100); -} -.text-success-200 { - color: var(--color-success-200); -} -.text-success-300 { - color: var(--color-success-300); -} -.text-success-400 { - color: var(--color-success-400); -} -.text-success-500 { - color: var(--color-success-500); -} -.text-success-600 { - color: var(--color-success-600); -} -.text-success-700 { - color: var(--color-success-700); -} -.text-success-800 { - color: var(--color-success-800); -} -.text-success-900 { - color: var(--color-success-900); -} \ No newline at end of file diff --git a/frontend-old/app/styles/theme/fonts.css b/frontend-old/app/styles/theme/fonts.css deleted file mode 100644 index eed2248..0000000 --- a/frontend-old/app/styles/theme/fonts.css +++ /dev/null @@ -1,93 +0,0 @@ - -/* Global font settings */ - -/* Set Space Grotesk as the default font */ -body { - font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; - font-size: 14px; - line-height: 1.6; - color: #333; -} - -/* Use Anta for headings */ -h1, h2, h3 { - font-family: 'Syncopate', sans-serif; - color: #111; -} - -/* Use Space Grotesk for smaller text like paragraphs */ -p { - font-family: 'Space Grotesk', sans-serif; -} - - -.font-default { - font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; -} - -.font-syncopate { - font-family: "Syncopate", serif !important; -} - -.font-space-grotesk { - font-family: 'Space Grotesk', sans-serif; -} - -.font-weight-100 { - font-weight: 100; -} -.font-weight-200 { - font-weight: 200; -} -.font-weight-300 { - font-weight: 300; -} -.font-weight-400 { - font-weight: 400; -} -.font-weight-500 { - font-weight: 500; -} -.font-weight-600 { - font-weight: 600; -} -.font-weight-700 { - font-weight: 700; -} -.font-weight-800 { - font-weight: 800; -} -.font-weight-900 { - font-weight: 900; -} - -.font-size-12 { - font-size: 12px !important; -} - -.font-size-14 { - font-size: 14px !important; -} - -.font-size-16 { - font-size: 16px !important; -} - -.font-size-18 { - font-size: 18px !important; -} - -.font-size-20 { - font-size: 20px !important; -} - -.font-size-24 { - font-size: 24px !important; -} - -.font-size-32 { - font-size: 32px !important; -} -.font-size-48 { - font-size: 48px !important; -} \ No newline at end of file diff --git a/frontend-old/app/types/DishType.ts b/frontend-old/app/types/DishType.ts deleted file mode 100644 index 99bd7f2..0000000 --- a/frontend-old/app/types/DishType.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { UserType } from "@/types/UserType"; - -export type DishType = { - id: number - name: string, - recurrence: number, - users: UserType[], -} - -export type DishDateType = { - id: number; - date: string; - dish: DishType; - user: UserType; -} - -export type ScheduledDishesType = { - date: string; - dishes: { dish: DishType, user: UserType }[]; -} diff --git a/frontend-old/app/types/RecurrenceType.ts b/frontend-old/app/types/RecurrenceType.ts deleted file mode 100644 index 554c777..0000000 --- a/frontend-old/app/types/RecurrenceType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type RecurrenceType = { - type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence"; - value: number; -} diff --git a/frontend-old/app/types/ScheduleType.ts b/frontend-old/app/types/ScheduleType.ts deleted file mode 100644 index e9494af..0000000 --- a/frontend-old/app/types/ScheduleType.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType"; -import type { UserType } from "@/types/UserType"; - -export type ScheduleType = { - id: number; - date: string; - scheduled_user_dishes: ScheduledUserDishType[]; - is_skipped: boolean; -} - -export type FilledScheduleType = { - id?: number; - date: string; - is_skipped?: boolean; - scheduled_user_dishes: ScheduledUserDishType[]; -} - -export type ScheduleDataType = { - user: UserType; - scheduled_user_dish: UserDishType | null; - user_dishes: UserDishType[]; -} \ No newline at end of file diff --git a/frontend-old/app/types/ScheduledUserDishType.ts b/frontend-old/app/types/ScheduledUserDishType.ts deleted file mode 100644 index a853f3c..0000000 --- a/frontend-old/app/types/ScheduledUserDishType.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { UserType } from "@/types/UserType"; -import type { DishType } from "@/types/DishType"; -import type { RecurrenceType } from "@/types/RecurrenceType"; - -export type UserDishType = { - id: number; - dish: DishType; - user: UserType; - recurrences: RecurrenceType[]; -} - -export type ScheduledUserDishType = { - id: number; - user_dish: UserDishType; -} \ No newline at end of file diff --git a/frontend-old/app/types/UserDishType.ts b/frontend-old/app/types/UserDishType.ts deleted file mode 100644 index c4bd896..0000000 --- a/frontend-old/app/types/UserDishType.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {UserType} from "@/types/UserType"; -import {RecurrenceType} from "@/types/RecurrenceType"; - -export type DishType = { - user: UserType; - dish: DishType; - recurrences: RecurrenceType[]; -} \ No newline at end of file diff --git a/frontend-old/app/types/UserDishWithoutUserType.ts b/frontend-old/app/types/UserDishWithoutUserType.ts deleted file mode 100644 index d41a7d9..0000000 --- a/frontend-old/app/types/UserDishWithoutUserType.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DishType } from "@/types/DishType"; -import type { RecurrenceType } from "@/types/RecurrenceType"; - -export type UserDishWithoutUserType = { - id: number; - dish: DishType; - recurrences: RecurrenceType[]; -} diff --git a/frontend-old/app/types/UserType.ts b/frontend-old/app/types/UserType.ts deleted file mode 100644 index 334111a..0000000 --- a/frontend-old/app/types/UserType.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UserDishWithoutUserType } from "@/types/UserDishWithoutUserType"; - -export type UserType = { - id: number; - name: string; - user_dishes: UserDishWithoutUserType[]; -}; \ No newline at end of file diff --git a/frontend-old/app/utils/api/apiRequest.ts b/frontend-old/app/utils/api/apiRequest.ts deleted file mode 100644 index 01acc70..0000000 --- a/frontend-old/app/utils/api/apiRequest.ts +++ /dev/null @@ -1,107 +0,0 @@ -export const apiRequest = async (url: string, options: RequestInit = {}) => { - const token = localStorage.getItem('token'); - - const allowedRequests = [ - '/api/auth/login', - '/api/auth/register', - ] - - if (allowedRequests.includes(url)) { - return publicRequest(url, options) - } - - if (!token) { - throw new Error('No authentication token found.' + url); - } - - return privateRequest(url, token, options); -}; - -export const publicRequest = async (url: string, options: RequestInit = {}) => { - console.log('โ†’ Sending request', url, options.method); - - url = 'http://localhost' + url; - - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - ...(options.headers || {}), - }, - ...options, - }); - - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); - } - - return response.json(); -} - -export const privateRequest = async (fullUrl: string, token: string, options: RequestInit = {}) => { - const headers = { - ...(options.headers || {}), - Authorization: `Bearer ${token}`, - }; - - const url = `${process.env.NEXT_PUBLIC_API_URL}${fullUrl}`; - - const response = await fetch(url, { headers, ...options }); - - // Authentication failure - token invalid - redirect to login - if (response.status === 401) { - localStorage.removeItem('token'); - localStorage.removeItem('refreshToken'); - - window.location.href = '/login'; - - throw new Error('Unauthorized. Redirecting to login.'); - } - - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); - } - - return response.json(); -} - - -// Add shorthand HTTP methods -apiRequest.get = (url: string, options: RequestInit = {}) => { - return apiRequest(url, { ...options, method: 'GET' }); -}; - -apiRequest.post = | undefined>( - url: string, - body: TBody, - options: RequestInit = {}, -) => { - return apiRequest(url, { - ...options, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(options.headers || {}), - }, - body: body ? JSON.stringify(body) : undefined, - }); -}; - -apiRequest.put = | undefined>( - url: string, - body: TBody, - options: RequestInit = {} -) => { - return apiRequest(url, { - ...options, - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...(options.headers || {}), - }, - body: body ? JSON.stringify(body) : undefined, - }); -}; - -apiRequest.delete = (url: string, options: RequestInit = {}) => { - return apiRequest(url, { ...options, method: 'DELETE' }); -}; \ No newline at end of file diff --git a/frontend-old/app/utils/api/usersApi.ts b/frontend-old/app/utils/api/usersApi.ts deleted file mode 100644 index d07a9c6..0000000 --- a/frontend-old/app/utils/api/usersApi.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {RecurrenceType} from "@/types/RecurrenceType"; -import {apiRequest} from "@/utils/api/apiRequest"; -import {UserType} from "@/types/UserType"; - -export const listUsers = async () => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/users`, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .then((data) => { - if (data?.payload?.users) { - return data.payload.users; - } - throw new Error('Failed to fetch users'); - }) - .catch((err) => { - throw err; - }); -}; - -export const showUser = async (userId: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/users/${userId}`, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((data) => { - if (data?.payload?.user) { - return data.payload.user; - } - throw new Error('Failed to fetch users'); - }) - .catch((err) => { - throw err; - }); -}; - -export const createUser = async (name: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return await apiRequest.post('/api/users', {name}, { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); -}; - -export const updateUser = async (user: UserType, name: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return await apiRequest.put(`/api/users/${user.id}`, { name }, { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); -}; - -export const deleteUser = async (user: UserType) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return await apiRequest.delete(`/api/users/${user.id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); -}; - - -export const getUserDishForUserAndDish = async (userId: number, dishId: number) => { - const endpoint = `/api/users/${userId}/dishes/${dishId}`; - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(endpoint, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .then((data) => { - if (data?.payload?.user_dish) { - return data.payload.user_dish; - } - throw new Error('Failed to fetch user dish'); - }) - .catch((err) => { - throw err; - }); -}; - -export const syncUserDishRecurrences = async ( - dish_id: number, - user_id: number, - recurrenceData: RecurrenceType[] -) => { - const url = `/api/users/${user_id}/dishes/${dish_id}/recurrences`; - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.post(url, { recurrences: recurrenceData }, { - headers: { - Authorization: `Bearer ${token}`, - }, - }).catch((err) => { - throw err; - }); -}; - diff --git a/frontend-old/archive/src/components/Spinner.tsx b/frontend-old/archive/src/components/Spinner.tsx deleted file mode 100644 index f170616..0000000 --- a/frontend-old/archive/src/components/Spinner.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const Spinner = () => { - - return ( -
- - - - -
- ) - -} - -export default Spinner \ No newline at end of file diff --git a/frontend-old/archive/src/components/features/schedule/HistoricalDishes.tsx b/frontend-old/archive/src/components/features/schedule/HistoricalDishes.tsx deleted file mode 100644 index ad3718f..0000000 --- a/frontend-old/archive/src/components/features/schedule/HistoricalDishes.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client" - -import {useEffect, useState} from "react"; -import {DateTime} from "luxon"; -import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar"; -import PageTitle from "@/components/ui/PageTitle"; -import {ScheduleType} from "@/types/ScheduleType"; -import Spinner from "@/components/Spinner"; -import {listSchedule} from "@/utils/api/scheduleApi"; - -const HistoricalDishes = () => { - const [schedule, setSchedule] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - const yesterday = DateTime.now().minus({ days: 1 }).toFormat('yyyy-LL-dd'); - - useEffect(() => { - listSchedule(undefined, yesterday) - .then((dishes: ScheduleType[]) => dishes - .sort((a: ScheduleType, b: ScheduleType) => new Date(b.date).getTime() - new Date(a.date).getTime()) - ) - .then((dishes) => setSchedule(dishes)) - .finally(() => setIsLoading(false)) - }, [yesterday]); - - if (isLoading) { - return ; - } - - if (!schedule || Object.keys(schedule).length === 0) { - return ( -
- No dishes scheduled -
- ); - } - - return
- History - -
-} - -export default HistoricalDishes \ No newline at end of file diff --git a/frontend-old/archive/src/components/layout/Card.tsx b/frontend-old/archive/src/components/layout/Card.tsx deleted file mode 100644 index 6784526..0000000 --- a/frontend-old/archive/src/components/layout/Card.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, {FC} from "react"; - -interface Props { - children: React.ReactNode; -} - -const Card: FC = ({ children }) => { - return ( -
- { children } -
- ) -} - -export default Card \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Alert.tsx b/frontend-old/archive/src/components/ui/Alert.tsx deleted file mode 100644 index c32070c..0000000 --- a/frontend-old/archive/src/components/ui/Alert.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {FC} from "react"; -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - type: 'error' | 'warning' | 'info' | 'success'; -} - -const Alert: FC = ({ children, className, type } ) => { - let bgColor = 'bg-blue-200' - let fgColor = 'bg-blue-800' - - if (type == 'error') { - bgColor = 'bg-red-200' - fgColor = 'bg-red-800' - } else if (type == 'warning') { - bgColor = 'bg-orange-200' - fgColor = 'bg-orange-800' - } else if (type == 'success') { - bgColor = 'border-2 border-green-500' - fgColor = 'text-green-500' - } - - const styles = classNames(fgColor, bgColor, className, 'rounded') - - return ( -
- { children} -
- ) -} - -export default Alert \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Button.tsx b/frontend-old/archive/src/components/ui/Button.tsx deleted file mode 100644 index 4ad006c..0000000 --- a/frontend-old/archive/src/components/ui/Button.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import Link from "next/link"; -import React, { FC, ReactElement, ReactNode } from "react"; -import classNames from "classnames"; - -interface ButtonProps { - appearance?: 'solid' | 'outline' | 'text'; - children: ReactNode; - className?: string; - href?: string; - icon?: ReactNode; - onClick?: () => void; - disabled?: boolean; - size?: 'small' | 'medium' | 'large'; - type?: 'button' | 'submit' | 'reset'; - variant?: 'primary' | 'secondary' | 'accent'; -} - -const Button: FC = ({ appearance, children, className, disabled, href, icon, onClick, - size = 'medium', type, - variant = 'primary' -}) => { - const styles = classNames( - "flex items-center space-x-1", - "justify-center font-size-18 py-2 px-4 rounded flex", - { - 'border-2 border-primary background-red text-white': variant === 'primary' && appearance === 'solid', - 'border-2 border-primary text-primary': variant === 'primary' && appearance === 'outline', - 'text-primary': variant === 'primary' && appearance === 'text', - 'border-2 border-secondary text-secondary': variant === 'secondary' && appearance === 'outline', - 'border-2 border-accent-blue text-accent-blue': variant === 'accent' && appearance === 'outline', - }, - className - ) - - const iconClassNames = classNames({ - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - if (href !== undefined) { - return ( - - { icon && iconElement} - { children} - - ) - } - - return -} - -export default Button \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Buttons/OutlineButton.tsx b/frontend-old/archive/src/components/ui/Buttons/OutlineButton.tsx deleted file mode 100644 index b668b6b..0000000 --- a/frontend-old/archive/src/components/ui/Buttons/OutlineButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { FC } from "react"; -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - disabled?: boolean; - onClick?: () => void; - size?: "small" | "medium" | "large"; - type: 'submit' | 'button'; -} - -const OutlineButton: FC = ({ children, className, disabled = false, onClick, size, type }) => { - const style = classNames( - "justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex", - { 'text-xs': size === "small" }, - className - ) - - if (onClick === undefined) { - onClick = () => { - } - } - - return ( - - ) -} - -export default OutlineButton \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Buttons/OutlineLinkButton.tsx b/frontend-old/archive/src/components/ui/Buttons/OutlineLinkButton.tsx deleted file mode 100644 index ab0e9b1..0000000 --- a/frontend-old/archive/src/components/ui/Buttons/OutlineLinkButton.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { FC, ReactElement } from "react"; -import classNames from "classnames"; -import Link from "next/link"; - -interface Props { - children: React.ReactNode; - className?: string; - href: string; - icon?: React.ReactNode; - size?: "small" | "medium" | "large"; - variant?: "primary" | "secondary"; -} - -const OutlineLinkButton: FC = ({ children, className, href, icon, size = "medium", variant }) => { - const linkClassNames = classNames( - "underline font-default pt-3 pb-3 px-4 rounded mb-0 flex", - { - 'text-primary border-primary': variant === "primary", - 'text-secondary border-secondary': variant === "secondary", - 'text-accent-blue border-accent': !variant || !["primary", "secondary"].includes(variant), - }, { - 'text-size-14': size === "small", - 'font-size-18': !size || size === "medium", - 'text-2xl': size === "large", - }, - className, - ) - - const iconClassNames = classNames("mt-0.5", { - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", // Default size - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - return ( - - {iconElement} - {children} - - ) -} - -export default OutlineLinkButton \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Buttons/SolidButton.tsx b/frontend-old/archive/src/components/ui/Buttons/SolidButton.tsx deleted file mode 100644 index 62ac01d..0000000 --- a/frontend-old/archive/src/components/ui/Buttons/SolidButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {FC} from "react"; -import classNames from "classnames"; - -interface Props { - children: React.ReactNode; - className?: string; - disabled?: boolean; - onClick?: () => void; - size?: "small" | "medium" | "large"; - type: 'submit' | 'button'; -} - -const SolidButton: FC = ({ children, className, disabled = false, onClick, size, type }) => { - const style = classNames( - "py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0", - { - 'text-xs' : size === "small", - 'font-size-18' : !size || size === "medium", - }, - className - ) - - if (onClick === undefined) { - onClick = () => {} - } - - return ( - - ) -} - -export default SolidButton \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Buttons/SolidLinkButton.tsx b/frontend-old/archive/src/components/ui/Buttons/SolidLinkButton.tsx deleted file mode 100644 index 77a78dd..0000000 --- a/frontend-old/archive/src/components/ui/Buttons/SolidLinkButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { FC, ReactElement } from "react"; -import classNames from "classnames"; -import Link from "next/link"; - -interface Props { - children: React.ReactNode; - className?: string; - href: string; - icon?: React.ReactNode; - size?: "small" | "medium" | "large"; - variant?: "primary" | "secondary"; -} - -const SolidLinkButton: FC = ({ children, className, href, icon, size = "medium", variant }) => { - const style = classNames( - "py-2 px-4 text-xl p-2 rounded hover:bg-secondary mb-0 text-center flex", - { - 'background-red text-white': variant === "primary", - 'background-secondary border-2 border-secondary': variant === "secondary", - }, - className - ) - - const iconClassNames = classNames("mt-1", { - "h-4 w-4 mr-1": size === "small", - "h-5 w-5 mr-1": size === "medium", // Default size - "h-7 w-7 mr-2": size === "large", - }); - - const iconElement = - React.isValidElement(icon) && - React.cloneElement(icon as ReactElement<{ className?: string }>, { - className: iconClassNames, - }); - - return ( - -
- {iconElement} - {children} -
- - ) -} - -export default SolidLinkButton \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Description.tsx b/frontend-old/archive/src/components/ui/Description.tsx deleted file mode 100644 index 8b429c0..0000000 --- a/frontend-old/archive/src/components/ui/Description.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import classNames from "classnames"; -import React from "react"; - -interface Props { - children: React.ReactNode; - className?: string; -} - -const Description = ({ children, className }: Props) => { - const style = classNames("italic font-size-16", - className - ) - - return

{ children }

-} - -export default Description \ No newline at end of file diff --git a/frontend-old/archive/src/components/ui/Toggle.tsx b/frontend-old/archive/src/components/ui/Toggle.tsx deleted file mode 100644 index 786b093..0000000 --- a/frontend-old/archive/src/components/ui/Toggle.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {FC} from "react"; - -interface ToggleProps { - checked: boolean; - onChange: (checked: boolean) => void; -} - -const Toggle: FC = ({ checked, onChange }) => { - const handleChange = () => { - onChange(checked); - } - - return ( - - ); -}; - -export default Toggle; \ No newline at end of file diff --git a/frontend-old/archive/src/helpers/Date.ts b/frontend-old/archive/src/helpers/Date.ts deleted file mode 100644 index f183c86..0000000 --- a/frontend-old/archive/src/helpers/Date.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DateTime } from 'luxon'; - -// Validate if a given string matches the "yyyy-MM-dd" format and is a valid date -export const isValidDate = (date: string): boolean => { - const parsedDate = DateTime.fromFormat(date, 'yyyy-MM-dd'); - return parsedDate.isValid && parsedDate.toFormat('yyyy-MM-dd') === date; -}; - -// Format a date to a specific string format -export const formatDate = (date: Date | string, format: string = 'yyyy-MM-dd'): string => { - const parsedDate = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date); - return parsedDate.toFormat(format); -}; - -// Compare two dates to see if one is before the other -export const isBefore = (date1: string, date2: string): boolean => { - return DateTime.fromISO(date1) < DateTime.fromISO(date2); -}; diff --git a/frontend-old/archive/src/hooks/useFetchDishes.ts b/frontend-old/archive/src/hooks/useFetchDishes.ts deleted file mode 100644 index a7ab747..0000000 --- a/frontend-old/archive/src/hooks/useFetchDishes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useState, useEffect } from "react"; -import { listDishes } from "@/utils/api/dishApi" -import { DishType } from "@/types/DishType" - -export const useFetchDishes = () => { - const [dishes, setDishes] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchDishes = async () => { - listDishes() - .then((dishes: DishType[]) => setDishes(dishes)) - .catch((err) => setError((err as Error).message || "An error occurred.")) - .finally(() => setIsLoading(false)); - }; - - fetchDishes(); - }, []); - - return { dishes, isLoading, error }; -}; \ No newline at end of file diff --git a/frontend-old/archive/src/hooks/useFetchUsers.ts b/frontend-old/archive/src/hooks/useFetchUsers.ts deleted file mode 100644 index f6df05b..0000000 --- a/frontend-old/archive/src/hooks/useFetchUsers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useState, useEffect } from "react"; -import {UserType} from "@/types/UserType"; -import {listUsers} from "@/utils/api/usersApi"; - -export const useFetchUsers = () => { - const [users, setUsers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchUsers = async () => { - listUsers() - .then((users: UserType[]) => setUsers(users)) - .catch((err) => setError((err as Error).message || "An error occurred.")) - .finally(() => setIsLoading(false)); - }; - - fetchUsers(); - }, []); - - return { users, isLoading, error }; -}; \ No newline at end of file diff --git a/frontend-old/archive/src/styles/base/globals.css b/frontend-old/archive/src/styles/base/globals.css deleted file mode 100644 index 918cbfb..0000000 --- a/frontend-old/archive/src/styles/base/globals.css +++ /dev/null @@ -1,19 +0,0 @@ -html, body { - margin: 0; - padding: 0; - width: 100%; - overflow-x: hidden; -} - -body { - font-family: Arial, Helvetica, sans-serif; -} - - -.toggle-input:checked { - background-color: #22c55e; /* bg-green-500 */ -} - -.toggle-input:checked ~ span:last-child { - --tw-translate-x: 1.75rem; /* translate-x-7 */ -} \ No newline at end of file diff --git a/frontend-old/archive/src/styles/components/buttons.css b/frontend-old/archive/src/styles/components/buttons.css deleted file mode 100644 index 07fc4de..0000000 --- a/frontend-old/archive/src/styles/components/buttons.css +++ /dev/null @@ -1,42 +0,0 @@ -.button-primary-solid { - background-color: var(--color-primary); - color: var(--color-secondary-200); - border: 1px solid var(--color-primary); - text-transform: uppercase; - font-family: "Anta", serif; - font-style: normal; - font-size: 1.1rem; - font-weight: 600; - padding: 4px 16px 2px 16px; -} -.button-primary-outline { - background-color: var(--color-background); - color: var(--color-primary); - border: 1px solid var(--color-primary); - text-transform: uppercase; - font-family: "Anta", serif; - font-style: normal; - font-size: 1.1rem; - font-weight: 600; - padding: 4px 16px 2px 16px; -} - -.button-secondary-solid { - background-color: var(--color-secondary); - color: var(--color-primary); - border: 1px solid var(--color-secondary); -} - -.button-accent-solid { - background-color: var(--color-accent-blue); - color: var(--color-secondary-900); - border: 1px solid var(--color-accent-blue); -} -.button-accent-outline { - background-color: var(--color-background); - color: var(--color-accent-blue); - border: 1px solid var(--color-accent-blue); -} -.button-accent-outline:hover { - background-color: var(--color-background-400); -} \ No newline at end of file diff --git a/frontend-old/archive/src/styles/components/select.css b/frontend-old/archive/src/styles/components/select.css deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-old/archive/src/styles/theme/borders.css b/frontend-old/archive/src/styles/theme/borders.css deleted file mode 100644 index b4ed5db..0000000 --- a/frontend-old/archive/src/styles/theme/borders.css +++ /dev/null @@ -1,14 +0,0 @@ -.border-primary { - border-color: var(--color-primary); -} - -.border-secondary { - border-color: var(--color-secondary); -} - -.border-accent-blue { - border-color: var(--color-accent-blue); -} -.border-accent-800 { - border-color: var(--color-accent-blue-800); -} \ No newline at end of file diff --git a/frontend-old/archive/src/styles/theme/colors/background.css b/frontend-old/archive/src/styles/theme/colors/background.css deleted file mode 100644 index e8548f5..0000000 --- a/frontend-old/archive/src/styles/theme/colors/background.css +++ /dev/null @@ -1,226 +0,0 @@ -.bg-gray-100 { - background-color: var(--color-gray-100) !important; -} -.bg-gray-200 { - background-color: var(--color-gray-200) !important; -} -.bg-gray-300 { - background-color: var(--color-gray-300) !important; -} -.bg-gray-400 { - background-color: var(--color-gray-400) !important; -} -.bg-gray-500 { - background-color: var(--color-gray-500) !important; -} -.bg-gray-600 { - background-color: var(--color-gray-600) !important; -} -.bg-gray-700 { - background-color: var(--color-gray-700) !important; -} -.bg-gray-800 { - background-color: var(--color-gray-800) !important; -} -.bg-gray-900 { - background-color: var(--color-gray-900) !important; -} - - -.bg-primary { - background-color: var(--color-primary) !important; -} - - -.bg-accent-blue { - background-color: var(--color-accent-blue-500) !important; -} -.bg-accent-blue-100 { - background-color: var(--color-accent-blue-100) !important; -} -.bg-accent-blue-200 { - background-color: var(--color-accent-blue-200) !important; -} -.bg-accent-blue-300 { - background-color: var(--color-accent-blue-300) !important; -} -.bg-accent-blue-400 { - background-color: var(--color-accent-blue-400) !important; -} -.bg-accent-blue-500 { - background-color: var(--color-accent-blue-500) !important; -} -.bg-accent-blue-600 { - background-color: var(--color-accent-blue-600) !important; -} -.bg-accent-blue-700 { - background-color: var(--color-accent-blue-700) !important; -} -.bg-accent-blue-800 { - background-color: var(--color-accent-blue-800) !important; -} -.bg-accent-blue-900 { - background-color: var(--color-accent-blue-900) !important; -} - - -.bg-accent-yellow { - background-color: var(--color-accent-yellow) !important; -} - -.bg-accent-yellow-100 { - background-color: var(--color-accent-yellow-100) !important; -} - -.bg-accent-yellow-200 { - background-color: var(--color-accent-yellow-200) !important; -} - -.bg-accent-yellow-300 { - background-color: var(--color-accent-yellow-300) !important; -} - -.bg-accent-yellow-400 { - background-color: var(--color-accent-yellow-400) !important; -} - -.bg-accent-yellow-500 { - background-color: var(--color-accent-yellow-500) !important; -} - -.bg-accent-yellow-600 { - background-color: var(--color-accent-yellow-600) !important; -} - -.bg-accent-yellow-700 { - background-color: var(--color-accent-yellow-700) !important; -} - -.bg-accent-yellow-800 { - background-color: var(--color-accent-yellow-800) !important; -} - -.bg-accent-yellow-900 { - background-color: var(--color-accent-yellow-900) !important; -} - - -.bg-success { - background-color: var(--color-success) !important; -} - -.bg-success-100 { - background-color: var(--color-success-100) !important; -} - -.bg-success-200 { - background-color: var(--color-success-200) !important; -} - -.bg-success-300 { - background-color: var(--color-success-300) !important; -} - -.bg-success-400 { - background-color: var(--color-success-400) !important; -} - -.bg-success-500 { - background-color: var(--color-success-500) !important; -} - -.bg-success-600 { - background-color: var(--color-success-600) !important; -} - -.bg-success-700 { - background-color: var(--color-success-700) !important; -} - -.bg-success-800 { - background-color: var(--color-success-800) !important; -} - -.bg-success-900 { - background-color: var(--color-success-900) !important; -} - -.bg-warning { - background-color: var(--color-warning) !important; -} - -.bg-warning-100 { - background-color: var(--color-warning-100) !important; -} - -.bg-warning-200 { - background-color: var(--color-warning-200) !important; -} - -.bg-warning-300 { - background-color: var(--color-warning-300) !important; -} - -.bg-warning-400 { - background-color: var(--color-warning-400) !important; -} - -.bg-warning-500 { - background-color: var(--color-warning-500) !important; -} - -.bg-warning-600 { - background-color: var(--color-warning-600) !important; -} - -.bg-warning-700 { - background-color: var(--color-warning-700) !important; -} - -.bg-warning-800 { - background-color: var(--color-warning-800) !important; -} - -.bg-warning-900 { - background-color: var(--color-warning-900) !important; -} - -.bg-danger { - background-color: var(--color-danger) !important; -} - -.bg-danger-100 { - background-color: var(--color-danger-100) !important; -} - -.bg-danger-200 { - background-color: var(--color-danger-200) !important; -} - -.bg-danger-300 { - background-color: var(--color-danger-300) !important; -} - -.bg-danger-400 { - background-color: var(--color-danger-400) !important; -} - -.bg-danger-500 { - background-color: var(--color-danger-500) !important; -} - -.bg-danger-600 { - background-color: var(--color-danger-600) !important; -} - -.bg-danger-700 { - background-color: var(--color-danger-700) !important; -} - -.bg-danger-800 { - background-color: var(--color-danger-800) !important; -} - -.bg-danger-900 { - background-color: var(--color-danger-900) !important; -} \ No newline at end of file diff --git a/frontend-old/archive/src/styles/theme/colors/border.css b/frontend-old/archive/src/styles/theme/colors/border.css deleted file mode 100644 index 8975296..0000000 --- a/frontend-old/archive/src/styles/theme/colors/border.css +++ /dev/null @@ -1,286 +0,0 @@ -.border-primary { - border-color: var(--color-primary); -} - -.border-primary-100 { - border-color: var(--color-primary-100); -} - -.border-primary-200 { - border-color: var(--color-primary-200); -} - -.border-primary-300 { - border-color: var(--color-primary-300); -} - -.border-primary-400 { - border-color: var(--color-primary-400); -} - -.border-primary-500 { - border-color: var(--color-primary-500); -} - -.border-primary-600 { - border-color: var(--color-primary-600); -} - -.border-primary-700 { - border-color: var(--color-primary-700); -} - -.border-primary-800 { - border-color: var(--color-primary-800); -} - -.border-primary-900 { - border-color: var(--color-primary-900); -} - - -.border-secondary { - border-color: var(--color-secondary); -} - -.border-secondary-100 { - border-color: var(--color-secondary-100); -} - -.border-secondary-200 { - border-color: var(--color-secondary-200); -} - -.border-secondary-300 { - border-color: var(--color-secondary-300); -} - -.border-secondary-400 { - border-color: var(--color-secondary-400); -} - -.border-secondary-500 { - border-color: var(--color-secondary-500); -} - -.border-secondary-600 { - border-color: var(--color-secondary-600); -} - -.border-secondary-700 { - border-color: var(--color-secondary-700); -} - -.border-secondary-800 { - border-color: var(--color-secondary-800); -} - -.border-secondary-900 { - border-color: var(--color-secondary-900); -} - -.border-accent-blue { - border-color: var(--color-accent-blue); -} - -.border-accent-blue-100 { - border-color: var(--color-accent-blue-100); -} - -.border-accent-blue-200 { - border-color: var(--color-accent-blue-200); -} - -.border-accent-blue-300 { - border-color: var(--color-accent-blue-300); -} - -.border-accent-blue-400 { - border-color: var(--color-accent-blue-400); -} - -.border-accent-blue-500 { - border-color: var(--color-accent-blue-500); -} - -.border-accent-blue-600 { - border-color: var(--color-accent-blue-600); -} - -.border-accent-blue-700 { - border-color: var(--color-accent-blue-700); -} - -.border-accent-blue-800 { - border-color: var(--color-accent-blue-800); -} - -.border-accent-blue-900 { - border-color: var(--color-accent-blue-900); -} - - -.border-accent-yellow { - border-color: var(--color-accent-yellow); -} - -.border-accent-yellow-100 { - border-color: var(--color-accent-yellow-100); -} - -.border-accent-yellow-200 { - border-color: var(--color-accent-yellow-200); -} - -.border-accent-yellow-300 { - border-color: var(--color-accent-yellow-300); -} - -.border-accent-yellow-400 { - border-color: var(--color-accent-yellow-400); -} - -.border-accent-yellow-500 { - border-color: var(--color-accent-yellow-500); -} - -.border-accent-yellow-600 { - border-color: var(--color-accent-yellow-600); -} - -.border-accent-yellow-700 { - border-color: var(--color-accent-yellow-700); -} - -.border-accent-yellow-800 { - border-color: var(--color-accent-yellow-800); -} - -.border-accent-yellow-900 { - border-color: var(--color-accent-yellow-900); -} - - -.border-background { - border-color: var(--color-background) !important; -} - -.border-danger { - border-color: var(--color-danger); -} - -.border-danger-100 { - border-color: var(--color-danger-100); -} - -.border-danger-200 { - border-color: var(--color-danger-200); -} - -.border-danger-300 { - border-color: var(--color-danger-300); -} - -.border-danger-400 { - border-color: var(--color-danger-400); -} - -.border-danger-500 { - border-color: var(--color-danger-500); -} - -.border-danger-600 { - border-color: var(--color-danger-600); -} - -.border-danger-700 { - border-color: var(--color-danger-700); -} - -.border-danger-800 { - border-color: var(--color-danger-800); -} - -.border-danger-900 { - border-color: var(--color-danger-900); -} - -.border-success { - border-color: var(--color-success); -} - -.border-success-100 { - border-color: var(--color-success-100); -} - -.border-success-200 { - border-color: var(--color-success-200); -} - -.border-success-300 { - border-color: var(--color-success-300); -} - -.border-success-400 { - border-color: var(--color-success-400); -} - -.border-success-500 { - border-color: var(--color-success-500); -} - -.border-success-600 { - border-color: var(--color-success-600); -} - -.border-success-700 { - border-color: var(--color-success-700); -} - -.border-success-800 { - border-color: var(--color-success-800); -} - -.border-success-900 { - border-color: var(--color-success-900); -} - -.border-warning { - border-color: var(--color-warning); -} - -.border-warning-100 { - border-color: var(--color-warning-100); -} - -.border-warning-200 { - border-color: var(--color-warning-200); -} - -.border-warning-300 { - border-color: var(--color-warning-300); -} - -.border-warning-400 { - border-color: var(--color-warning-400); -} - -.border-warning-500 { - border-color: var(--color-warning-500); -} - -.border-warning-600 { - border-color: var(--color-warning-600); -} - -.border-warning-700 { - border-color: var(--color-warning-700); -} - -.border-warning-800 { - border-color: var(--color-warning-800); -} - -.border-warning-900 { - border-color: var(--color-warning-900); -} \ No newline at end of file diff --git a/frontend-old/archive/src/styles/theme/colors/root.css b/frontend-old/archive/src/styles/theme/colors/root.css deleted file mode 100644 index acffac0..0000000 --- a/frontend-old/archive/src/styles/theme/colors/root.css +++ /dev/null @@ -1,193 +0,0 @@ -:root { - --color-rose-50: #FFF5FC; - --color-rose-100: #FCE6F5; - --color-rose-200: #FAC3E7; - --color-rose-300: #F7A1D5; - --color-rose-400: #F25EAB; - --color-rose-500: #ED1F79; - --color-rose-600: #D61A68; - --color-rose-700: #B3124F; - --color-rose-800: #8F0B39; - --color-rose-900: #6B0626; - --color-rose-950: #450315; - - --color-deluge-50: #FAF7FC; - --color-deluge-100: #F2EDF7; - --color-deluge-200: #E2DAF0; - --color-deluge-300: #CEC3E6; - --color-deluge-400: #A49BD1; - --color-deluge-500: #7776BC; - --color-deluge-600: #6361AB; - --color-deluge-700: #43428C; - --color-deluge-800: #2C2B70; - --color-deluge-900: #191854; - --color-deluge-950: #0A0A36; - - --color-malibu-50: #FAFEFF; - --color-malibu-100: #F5FDFF; - --color-malibu-200: #E1F6FC; - --color-malibu-300: #CDEDFA; - --color-malibu-400: #ABDEF7; - --color-malibu-500: #85C7F2; - --color-malibu-600: #6EACDB; - --color-malibu-700: #4A81B5; - --color-malibu-800: #305F91; - --color-malibu-900: #1B3F6E; - --color-malibu-950: #0B2247; - - --color-gamboge-50: #FFFDF2; - --color-gamboge-100: #FCF7E3; - --color-gamboge-200: #FAECBB; - --color-gamboge-300: #F5DC93; - --color-gamboge-400: #EDBB47; - --color-gamboge-500: #E59500; - --color-gamboge-600: #CF7F00; - --color-gamboge-700: #AB6100; - --color-gamboge-800: #8A4700; - --color-gamboge-900: #663000; - --color-gamboge-950: #421C00; - - --color-ebony-clay-100: #9AA2B3; /* Soft slate */ - --color-ebony-clay-200: #7A8093; /* Balanced midtone */ - --color-ebony-clay-300: #5D637A; /* Former 400 */ - --color-ebony-clay-400: #444760; /* New shadowed steel */ - --color-ebony-clay-500: #2B2C41; - --color-ebony-clay-600: #24263C; /* Adjusted โ€” less jumpy */ - --color-ebony-clay-700: #1D1E36; /* Interpolated midpoint */ - --color-ebony-clay-800: #131427; /* Slightly lifted from old 800 */ - --color-ebony-clay-900: #0A0B1C; - --color-ebony-clay-950: #030412; - - --color-alizarin-crimson-50: #FFF5FA; - --color-alizarin-crimson-100: #FCE6F1; - --color-alizarin-crimson-200: #FAC3DC; - --color-alizarin-crimson-300: #F59FC0; - --color-alizarin-crimson-400: #F05D82; - --color-alizarin-crimson-500: #E71D36; - --color-alizarin-crimson-600: #D1192F; - --color-alizarin-crimson-700: #AD1121; - --color-alizarin-crimson-800: #8C0B18; - --color-alizarin-crimson-900: #69060E; - --color-alizarin-crimson-950: #420308; - - --color-spring-green-50: #F5FFFC; - --color-spring-green-100: #E8FFF9; - --color-spring-green-200: #C7FFEE; - --color-spring-green-300: #A4FCDF; - --color-spring-green-400: #62FCBC; - --color-spring-green-500: #21FA90; - --color-spring-green-600: #1BE07A; - --color-spring-green-700: #13BA5E; - --color-spring-green-800: #0C9646; - --color-spring-green-900: #07702D; - --color-spring-green-950: #03471A; - - --color-burning-orange-50: #FFFBF5; - --color-burning-orange-100: #FFF7EB; - --color-burning-orange-200: #FFE8CC; - --color-burning-orange-300: #FFD5AD; - --color-burning-orange-400: #FFA973; - --color-burning-orange-500: #FF6B35; - --color-burning-orange-600: #E65A2C; - --color-burning-orange-700: #BF441F; - --color-burning-orange-800: #993114; - --color-burning-orange-900: #731F0A; - --color-burning-orange-950: #4A1004; - - /* Standard naming */ - - --color-primary: var(--color-rose-500); - --color-primary-100: var(--color-rose-100); - --color-primary-200: var(--color-rose-200); - --color-primary-300: var(--color-rose-300); - --color-primary-400: var(--color-rose-400); - --color-primary-500: var(--color-rose-500); - --color-primary-600: var(--color-rose-600); - --color-primary-700: var(--color-rose-700); - --color-primary-800: var(--color-rose-800); - --color-primary-900: var(--color-rose-900); - - --color-secondary: var(--color-deluge-500); - --color-secondary-100: var(--color-deluge-100); - --color-secondary-200: var(--color-deluge-200); - --color-secondary-300: var(--color-deluge-300); - --color-secondary-400: var(--color-deluge-400); - --color-secondary-500: var(--color-deluge-500); - --color-secondary-600: var(--color-deluge-600); - --color-secondary-700: var(--color-deluge-700); - --color-secondary-800: var(--color-deluge-800); - --color-secondary-900: var(--color-deluge-900); - - --color-accent-blue: var(--color-malibu-500); - --color-accent-blue-100: var(--color-malibu-100); - --color-accent-blue-200: var(--color-malibu-200); - --color-accent-blue-300: var(--color-malibu-300); - --color-accent-blue-400: var(--color-malibu-400); - --color-accent-blue-500: var(--color-malibu-500); - --color-accent-blue-600: var(--color-malibu-600); - --color-accent-blue-700: var(--color-malibu-700); - --color-accent-blue-800: var(--color-malibu-800); - --color-accent-blue-900: var(--color-malibu-900); - - --color-accent-yellow: var(--color-gamboge-500); - --color-accent-yellow-50: var(--color-gamboge-50); - --color-accent-yellow-100: var(--color-gamboge-100); - --color-accent-yellow-200: var(--color-gamboge-200); - --color-accent-yellow-300: var(--color-gamboge-300); - --color-accent-yellow-400: var(--color-gamboge-400); - --color-accent-yellow-500: var(--color-gamboge-500); - --color-accent-yellow-600: var(--color-gamboge-600); - --color-accent-yellow-700: var(--color-gamboge-700); - --color-accent-yellow-800: var(--color-gamboge-800); - --color-accent-yellow-900: var(--color-gamboge-900); - --color-accent-yellow-950: var(--color-gamboge-950); - - --color-gray-100: var(--color-ebony-clay-100); - --color-gray-200: var(--color-ebony-clay-200); - --color-gray-300: var(--color-ebony-clay-300); - --color-gray-400: var(--color-ebony-clay-400); - --color-gray-500: var(--color-ebony-clay-500); - --color-gray-600: var(--color-ebony-clay-600); - --color-gray-700: var(--color-ebony-clay-700); - --color-gray-800: var(--color-ebony-clay-800); - --color-gray-900: var(--color-ebony-clay-900); - - --color-danger: var(--color-alizarin-crimson-500); - --color-danger-50: var(--color-alizarin-crimson-50); - --color-danger-100: var(--color-alizarin-crimson-100); - --color-danger-200: var(--color-alizarin-crimson-200); - --color-danger-300: var(--color-alizarin-crimson-300); - --color-danger-400: var(--color-alizarin-crimson-400); - --color-danger-500: var(--color-alizarin-crimson-500); - --color-danger-600: var(--color-alizarin-crimson-600); - --color-danger-700: var(--color-alizarin-crimson-700); - --color-danger-800: var(--color-alizarin-crimson-800); - --color-danger-900: var(--color-alizarin-crimson-900); - --color-danger-950: var(--color-alizarin-crimson-950); - - --color-success: var(--color-spring-green-500); - --color-success-50: var(--color-spring-green-50); - --color-success-100: var(--color-spring-green-100); - --color-success-200: var(--color-spring-green-200); - --color-success-300: var(--color-spring-green-300); - --color-success-400: var(--color-spring-green-400); - --color-success-500: var(--color-spring-green-500); - --color-success-600: var(--color-spring-green-600); - --color-success-700: var(--color-spring-green-700); - --color-success-800: var(--color-spring-green-800); - --color-success-900: var(--color-spring-green-900); - --color-success-950: var(--color-spring-green-950); - - --color-warning: var(--color-burning-orange-500); - --color-warning-50: var(--color-burning-orange-50); - --color-warning-100: var(--color-burning-orange-100); - --color-warning-200: var(--color-burning-orange-200); - --color-warning-300: var(--color-burning-orange-300); - --color-warning-400: var(--color-burning-orange-400); - --color-warning-500: var(--color-burning-orange-500); - --color-warning-600: var(--color-burning-orange-600); - --color-warning-700: var(--color-burning-orange-700); - --color-warning-800: var(--color-burning-orange-800); - --color-warning-900: var(--color-burning-orange-900); - --color-warning-950: var(--color-burning-orange-950); -} diff --git a/frontend-old/archive/src/utils/api/auth.ts b/frontend-old/archive/src/utils/api/auth.ts deleted file mode 100644 index f4511aa..0000000 --- a/frontend-old/archive/src/utils/api/auth.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { apiRequest } from '@/utils/api/apiRequest'; - -export const login = async (email: string, password: string) => { - const data = await apiRequest.post('/api/auth/login', { email, password }); - - if (!data.access_token) { - throw new Error('No access token returned from login.'); - } - - localStorage.setItem('token', data.access_token); - - return data; -}; - - -export const register = async (name: string, email: string, password: string, passwordConfirmation: string) => { - const data = await apiRequest.post('/api/auth/register', { - name, - email, - password, - password_confirmation: passwordConfirmation, // Match the backend's expected parameter - }); - - // Store the token (if returned by the backend) similarly to login - localStorage.setItem('token', data.access_token); - - return data; -}; diff --git a/frontend-old/archive/src/utils/api/dishApi.ts b/frontend-old/archive/src/utils/api/dishApi.ts deleted file mode 100644 index 8d38dba..0000000 --- a/frontend-old/archive/src/utils/api/dishApi.ts +++ /dev/null @@ -1,149 +0,0 @@ -import {DishType} from "@/types/DishType"; -import {apiRequest} from "@/utils/api/apiRequest"; - -export const listDishes = async (): Promise => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/dishes`, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .then((data) => { - if (data?.payload?.dishes) { - return data.payload.dishes as DishType[]; - } - throw new Error('SOMETHING WENT WRONG'); - }) - .catch((error) => { - throw error; - }); -}; - -export const fetchDish = async (id: number): Promise => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/dishes/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .then((data) => { - if (data?.payload?.dish) { - return data.payload.dish as DishType; - } - throw new Error('SOMETHING WENT WRONG'); - }) - .catch((error) => { - throw error; - }); -}; - -export const createDish = async ( - name: string, - // recurrence: number, - // userIds: number[] -) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.post(`/api/dishes`, { - name, - // recurrence, - // users: userIds, - }, { - headers: { - Authorization: `Bearer ${token}`, - }, - }).catch(() => { - throw new Error("Failed to create dish. Please try again later."); - }); -}; - -export const updateDish = async (dish_id: number, name: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.put(`/api/dishes/${dish_id}`, {name}, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .catch((error) => { - throw error; - }); -}; - -export const deleteDish = async (id: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.delete(`/api/dishes/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }}) - .then((data) => { - return data; - }) - .catch((error) => { - throw error; - }); -}; - -export const addUserToDish = async (dish_id: number, user_id: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.post(`/api/dishes/${dish_id}/users/add`, { - users: [user_id], - }, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((data) => { - return data; - }) - .catch((error) => { - throw error; - }); -}; - -export const removeUserFromDish = async (dish_id: number, user_id: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.post(`/api/dishes/${dish_id}/users/remove`, { - users: [user_id], - }, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((data) => { - return data; - }) - .catch((error) => { - throw error; - }); -}; diff --git a/frontend-old/archive/src/utils/api/scheduleApi.ts b/frontend-old/archive/src/utils/api/scheduleApi.ts deleted file mode 100644 index d7fcbf3..0000000 --- a/frontend-old/archive/src/utils/api/scheduleApi.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { apiRequest } from "@/utils/api/apiRequest"; -import { isValidDate } from "@/helpers/Date"; - -export const listSchedule = async (startDate?: string, endDate?: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - if (startDate && !isValidDate(startDate)) { - throw new Error('Invalid start date'); - } - if (endDate && !isValidDate(endDate)) { - throw new Error('Invalid end date'); - } - - const params = new URLSearchParams(); - if (startDate) params.append('start', startDate); - if (endDate) params.append('end', endDate); - - const endpoint = `/api/schedule${params.toString() ? `?${params.toString()}` : ''}`; - - return apiRequest.get(endpoint, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduleData) => scheduleData?.payload?.schedule); -}; - -export const getScheduleForDate = async (date: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - if (!isValidDate(date)) { - throw new Error('Invalid date'); - } - - const endpoint = `/api/schedule/${date}`; - - return apiRequest.get(endpoint, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduleData) => scheduleData?.payload?.schedule) - .catch((err) => { - throw err; - }); -}; - -// Update the schedule for a specific date (e.g., mark as skipped) -export const updateScheduleForDate = async (date: string, isSkipped: boolean) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - if (!isValidDate(date)) { - throw new Error('Invalid date'); - } - - const endpoint = `/api/schedule/${date}`; - - return apiRequest.put(endpoint, { is_skipped: isSkipped }, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduleData) => scheduleData?.payload?.schedule) - .catch((err) => { - throw err; - }); -}; - -// Generate a new schedule (optional: overwrite the existing one) -export const generateSchedule = async (overwrite: boolean) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - const endpoint = `/api/schedule/generate`; - - return apiRequest.post(endpoint, { overwrite }, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduleData) => scheduleData?.payload?.schedule) - .catch((err) => { - throw err; - }); -}; - -export const scheduleUserDish = async (date: string, user_id: number, user_dish_id: number|null, skipped: boolean = false) => { - const token = localStorage.getItem('token'); - - if (!token) throw new Error('No token found in localStorage.'); - - const endpoint = `/api/schedule/${date}/user-dishes`; - - return apiRequest.post(endpoint, { user_dish_id, user_id, skipped }, { headers: { Authorization: `Bearer ${token}`} }) - .then((scheduleData) => scheduleData?.payload?.schedule) - .catch((err) => { throw err }); -}; \ No newline at end of file diff --git a/frontend-old/archive/src/utils/api/scheduledUserDishesApi.ts b/frontend-old/archive/src/utils/api/scheduledUserDishesApi.ts deleted file mode 100644 index 8a9b66c..0000000 --- a/frontend-old/archive/src/utils/api/scheduledUserDishesApi.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { apiRequest } from "@/utils/api/apiRequest"; - -export const listScheduledUserDishesStartingFromDate = async (startDate: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/scheduled-user-dishes?start=${startDate}`, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduledDishesData) => scheduledDishesData?.payload?.schedule) - .catch((err) => { - throw err; - }); -}; - -export const listScheduledUserDishesEndingAtDate = async (endDate: string) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/scheduled-user-dishes?end=${endDate}`, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduledDishesData) => scheduledDishesData?.payload?.schedule) - .catch((err) => { - throw err; - }); -}; - -export const getScheduledUserDish = async (id: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - return apiRequest.get(`/api/scheduled-user-dishes/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduledDishesData) => scheduledDishesData?.payload?.scheduled_user_dish) - .catch((err) => { - throw err; - }); -}; - -export const updateScheduledUserDish = async (id: number, userDishId: number) => { - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('No token found in localStorage.'); - } - - const payload = userDishId > 0 - ? { user_dish_id: userDishId } - : { user_dish_id: null, is_skipped: true }; - - return apiRequest.put(`/api/scheduled-user-dishes/${id}`, payload, { - headers: { - Authorization: `Bearer ${token}`, - } - }) - .then((scheduledDishesData) => scheduledDishesData?.payload?.scheduled_user_dish) - .catch((err) => { - throw err; - }); -}; \ No newline at end of file diff --git a/frontend-old/archive/src/utils/api/userDishApi.ts b/frontend-old/archive/src/utils/api/userDishApi.ts deleted file mode 100644 index 39ba27f..0000000 --- a/frontend-old/archive/src/utils/api/userDishApi.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { apiRequest } from "@/utils/api/apiRequest"; -import { UserDishType } from "@/types/ScheduledUserDishType"; - -export const listUserDishes = async (): Promise => { - const token = localStorage.getItem('token'); - - if (!token) throw new Error('No token found in localStorage.'); - - return apiRequest.get(`/api/user-dishes`, { headers: { Authorization: `Bearer ${ token }` } }) - .then((data) => { - if (data?.payload?.user_dishes) return data.payload.user_dishes as UserDishType[]; - - throw new Error('SOMETHING WENT WRONG'); - }) - .catch((error) => { - throw error; - }); -}; diff --git a/frontend-old/archive/src/utils/dateBuilder.ts b/frontend-old/archive/src/utils/dateBuilder.ts deleted file mode 100644 index e41ce15..0000000 --- a/frontend-old/archive/src/utils/dateBuilder.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DateTime } from "luxon"; - -const transformDate = (inputDate: string, addSuffix = false): string => { - const date = DateTime.fromISO(inputDate) - const day = date.day - const suffix = addSuffix ? getDaySuffix(day) : '' - return date.toFormat("MMMM") + ` ${ day }${ suffix }, ` + date.toFormat("yyyy"); -} - -const getDaySuffix = (day: number): string => { - if (day >= 11 && day <= 13) return "th"; - switch (day % 10) { - case 1: - return "st"; - case 2: - return "nd"; - case 3: - return "rd"; - default: - return "th"; - } -}; - -export default transformDate; \ No newline at end of file diff --git a/frontend-old/archive/src/utils/scheduleBuilder.ts b/frontend-old/archive/src/utils/scheduleBuilder.ts deleted file mode 100644 index 8bbf15f..0000000 --- a/frontend-old/archive/src/utils/scheduleBuilder.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ScheduleDataType, ScheduleType } from "@/types/ScheduleType"; -import { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType"; -import { UserType } from "@/types/UserType"; - -const ScheduleBuilder = ( - schedule: ScheduleType, - users: UserType[], - userDishes: UserDishType[] -): ScheduleDataType[] => users.map(user => { - return { - user, - scheduled_user_dish: schedule.scheduled_user_dishes - .filter((scheduledUserDish: ScheduledUserDishType) => scheduledUserDish.user_dish?.user.id === user.id) - .shift()?.user_dish ?? null, - user_dishes: userDishes.filter((userDish: UserDishType) => userDish.user.id === user.id) - } -}) - -export default ScheduleBuilder \ No newline at end of file diff --git a/frontend-old/archive/tailwind.config.ts b/frontend-old/archive/tailwind.config.ts deleted file mode 100644 index 109807b..0000000 --- a/frontend-old/archive/tailwind.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/frontend-old/.dockerignore b/frontend-react-router-backup/.dockerignore similarity index 100% rename from frontend-old/.dockerignore rename to frontend-react-router-backup/.dockerignore diff --git a/frontend-old/.gitignore b/frontend-react-router-backup/.gitignore similarity index 100% rename from frontend-old/.gitignore rename to frontend-react-router-backup/.gitignore diff --git a/frontend-old/Dockerfile b/frontend-react-router-backup/Dockerfile similarity index 100% rename from frontend-old/Dockerfile rename to frontend-react-router-backup/Dockerfile diff --git a/frontend-react-router-backup/LICENSE b/frontend-react-router-backup/LICENSE new file mode 100644 index 0000000..577ff1d --- /dev/null +++ b/frontend-react-router-backup/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + DishPlanner Copyright (C) 2025 myrmidex + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/frontend-old/README.md b/frontend-react-router-backup/README.md similarity index 100% rename from frontend-old/README.md rename to frontend-react-router-backup/README.md diff --git a/frontend-old/app/app.css b/frontend-react-router-backup/app/app.css similarity index 65% rename from frontend-old/app/app.css rename to frontend-react-router-backup/app/app.css index 2187b39..99345d8 100644 --- a/frontend-old/app/app.css +++ b/frontend-react-router-backup/app/app.css @@ -1,8 +1,5 @@ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&family=Syncopate:wght@400;700&display=swap'); @import "tailwindcss"; -@import "./styles/main.css"; - @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; diff --git a/frontend-react-router-backup/app/root.tsx b/frontend-react-router-backup/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/frontend-react-router-backup/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/frontend-react-router-backup/app/routes.ts b/frontend-react-router-backup/app/routes.ts new file mode 100644 index 0000000..102b402 --- /dev/null +++ b/frontend-react-router-backup/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/frontend-react-router-backup/app/routes/home.tsx b/frontend-react-router-backup/app/routes/home.tsx new file mode 100644 index 0000000..398e47c --- /dev/null +++ b/frontend-react-router-backup/app/routes/home.tsx @@ -0,0 +1,13 @@ +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export default function Home() { + return ; +} diff --git a/frontend-old/app/welcome/logo-dark.svg b/frontend-react-router-backup/app/welcome/logo-dark.svg similarity index 100% rename from frontend-old/app/welcome/logo-dark.svg rename to frontend-react-router-backup/app/welcome/logo-dark.svg diff --git a/frontend-old/app/welcome/logo-light.svg b/frontend-react-router-backup/app/welcome/logo-light.svg similarity index 100% rename from frontend-old/app/welcome/logo-light.svg rename to frontend-react-router-backup/app/welcome/logo-light.svg diff --git a/frontend-old/app/welcome/welcome.tsx b/frontend-react-router-backup/app/welcome/welcome.tsx similarity index 100% rename from frontend-old/app/welcome/welcome.tsx rename to frontend-react-router-backup/app/welcome/welcome.tsx diff --git a/frontend-old/package-lock.json b/frontend-react-router-backup/package-lock.json similarity index 93% rename from frontend-old/package-lock.json rename to frontend-react-router-backup/package-lock.json index af085dc..ca4715b 100644 --- a/frontend-old/package-lock.json +++ b/frontend-react-router-backup/package-lock.json @@ -6,25 +6,20 @@ "": { "name": "my-react-router-app", "dependencies": { - "@headlessui/react": "^2.2.9", - "@heroicons/react": "^2.2.0", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", - "@types/luxon": "^3.7.1", - "classnames": "^2.5.1", "isbot": "^5.1.27", - "luxon": "^3.7.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router": "^7.5.3" }, "devDependencies": { "@react-router/dev": "^7.5.3", - "@tailwindcss/vite": "^4.1.6", + "@tailwindcss/vite": "^4.1.4", "@types/node": "^20", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", - "tailwindcss": "^4.1.6", + "tailwindcss": "^4.1.4", "typescript": "^5.8.3", "vite": "^6.3.3", "vite-tsconfig-paths": "^5.1.4" @@ -938,88 +933,6 @@ "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@headlessui/react": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", - "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.20.2", - "@react-aria/interactions": "^3.25.0", - "@tanstack/react-virtual": "^3.13.9", - "use-sync-external-store": "^1.5.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1183,73 +1096,6 @@ "node": ">=14" } }, - "node_modules/@react-aria/focus": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", - "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/interactions": "^3.25.6", - "@react-aria/utils": "^3.31.0", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.25.6", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", - "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.31.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", - "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.8", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-router/dev": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.6.0.tgz", @@ -1380,36 +1226,6 @@ "react-router": "7.6.0" } }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", - "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-types/shared": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", - "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", @@ -1690,15 +1506,6 @@ "win32" ] }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@tailwindcss/node": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", @@ -1976,33 +1783,6 @@ "vite": "^5.2.0 || ^6" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", - "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", - "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2010,12 +1790,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.17.46", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", @@ -2328,21 +2102,6 @@ "node": ">=18" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3507,15 +3266,6 @@ "yallist": "^3.0.2" } }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -4596,12 +4346,6 @@ "node": ">=8" } }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", @@ -4694,12 +4438,6 @@ } } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4793,15 +4531,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/frontend-old/package.json b/frontend-react-router-backup/package.json similarity index 76% rename from frontend-old/package.json rename to frontend-react-router-backup/package.json index cc47acc..1337f19 100644 --- a/frontend-old/package.json +++ b/frontend-react-router-backup/package.json @@ -9,27 +9,22 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { - "@headlessui/react": "^2.2.9", - "@heroicons/react": "^2.2.0", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", - "@types/luxon": "^3.7.1", - "classnames": "^2.5.1", "isbot": "^5.1.27", - "luxon": "^3.7.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router": "^7.5.3" }, "devDependencies": { "@react-router/dev": "^7.5.3", - "@tailwindcss/vite": "^4.1.6", + "@tailwindcss/vite": "^4.1.4", "@types/node": "^20", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", - "tailwindcss": "^4.1.6", + "tailwindcss": "^4.1.4", "typescript": "^5.8.3", "vite": "^6.3.3", "vite-tsconfig-paths": "^5.1.4" } -} +} \ No newline at end of file diff --git a/frontend-old/public/favicon.ico b/frontend-react-router-backup/public/favicon.ico similarity index 100% rename from frontend-old/public/favicon.ico rename to frontend-react-router-backup/public/favicon.ico diff --git a/frontend-old/react-router.config.ts b/frontend-react-router-backup/react-router.config.ts similarity index 100% rename from frontend-old/react-router.config.ts rename to frontend-react-router-backup/react-router.config.ts diff --git a/frontend-old/tsconfig.json b/frontend-react-router-backup/tsconfig.json similarity index 83% rename from frontend-old/tsconfig.json rename to frontend-react-router-backup/tsconfig.json index cfe4682..dc391a4 100644 --- a/frontend-old/tsconfig.json +++ b/frontend-react-router-backup/tsconfig.json @@ -15,8 +15,7 @@ "rootDirs": [".", "./.react-router/types"], "baseUrl": ".", "paths": { - "~/*": ["./app/*"], - "@/*": ["./app/*"] + "~/*": ["./app/*"] }, "esModuleInterop": true, "verbatimModuleSyntax": true, @@ -24,8 +23,5 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true - }, - "optimizeDeps": { - "include": ["react", "react-dom"] } } diff --git a/frontend-old/vite.config.ts b/frontend-react-router-backup/vite.config.ts similarity index 55% rename from frontend-old/vite.config.ts rename to frontend-react-router-backup/vite.config.ts index 93a885d..4a88d58 100644 --- a/frontend-old/vite.config.ts +++ b/frontend-react-router-backup/vite.config.ts @@ -5,16 +5,4 @@ import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], - server: { - proxy: { - "/api": { - target: "http://localhost:8000", - changeOrigin: true, - }, - "/sanctum": { - target: "http://localhost:8000", - changeOrigin: true, - }, - }, - }, }); diff --git a/frontend-old/archive/.dockerignore b/frontend/.dockerignore similarity index 100% rename from frontend-old/archive/.dockerignore rename to frontend/.dockerignore diff --git a/frontend-old/archive/.env.local.example b/frontend/.env.local.example similarity index 100% rename from frontend-old/archive/.env.local.example rename to frontend/.env.local.example diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..2fbb406 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,2 @@ +#NEXT_PUBLIC_API_URL=http://192.168.178.177:9000 +#NODE_ENV=local \ No newline at end of file diff --git a/frontend-old/archive/.gitignore b/frontend/.gitignore similarity index 100% rename from frontend-old/archive/.gitignore rename to frontend/.gitignore diff --git a/frontend/COMPONENT_GUIDE.md b/frontend/COMPONENT_GUIDE.md new file mode 100644 index 0000000..7027da9 --- /dev/null +++ b/frontend/COMPONENT_GUIDE.md @@ -0,0 +1,289 @@ +# Component Guide - Dish Planner + +This guide documents the standardized UI components in the Dish Planner application. All components use Tailwind CSS with our custom color variables from the design system. + +## Design System + +### Colors + +Our color palette is defined in `src/styles/theme/colors/root.css` and integrated into Tailwind via `tailwind.config.ts`: + +- **Primary (Rose)**: `bg-primary`, `text-primary`, `border-primary` with shades 50-950 +- **Secondary (Deluge)**: `bg-secondary`, `text-secondary`, `border-secondary` with shades 50-950 +- **Accent (Malibu Blue)**: `bg-accent`, `text-accent`, `border-accent` with shades 50-950 +- **Yellow (Gamboge)**: `bg-yellow`, `text-yellow`, `border-yellow` with shades 50-950 +- **Gray (Ebony Clay)**: `bg-gray`, `text-gray`, `border-gray` with shades 100-950 +- **Semantic Colors**: + - Danger (Alizarin Crimson): `bg-danger`, `text-danger`, `border-danger` + - Success (Spring Green): `bg-success`, `text-success`, `border-success` + - Warning (Burning Orange): `bg-warning`, `text-warning`, `border-warning` + +## Components + +### Button (`src/components/ui/Button.tsx`) + +Unified button component supporting multiple variants, appearances, and states. + +#### Props + +```typescript +interface ButtonProps { + appearance?: 'solid' | 'outline' | 'text'; // Default: 'solid' + variant?: 'primary' | 'secondary' | 'accent' | 'danger'; // Default: 'primary' + size?: 'small' | 'medium' | 'large'; // Default: 'medium' + type?: 'button' | 'submit' | 'reset'; // Default: 'button' + href?: string; // For link buttons + icon?: ReactNode; + disabled?: boolean; + onClick?: () => void; + className?: string; + children: ReactNode; +} +``` + +#### Examples + +```tsx +// Solid primary button + + +// Outline accent button with icon + + +// Text danger button + + +// Link button + +``` + +### Input (`src/components/ui/Input.tsx`) + +Standardized text input component with label, error, and helper text support. + +#### Props + +```typescript +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; + helperText?: string; + fullWidth?: boolean; // Default: true +} +``` + +#### Examples + +```tsx +// Basic input with label + setEmail(e.target.value)} + required +/> + +// Input with error + + +// Input with helper text + +``` + +### Select (`src/components/ui/Select.tsx`) + +Standardized select dropdown component. + +#### Props + +```typescript +interface SelectProps extends SelectHTMLAttributes { + label?: string; + error?: string; + helperText?: string; + fullWidth?: boolean; // Default: true + options: Array<{ value: string | number; label: string }>; +} +``` + +#### Example + +```tsx + setName(e.target.value)} - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + setName(e.target.value)} /> - Create + ); diff --git a/frontend-old/archive/src/app/users/page.tsx b/frontend/src/app/users/page.tsx similarity index 88% rename from frontend-old/archive/src/app/users/page.tsx rename to frontend/src/app/users/page.tsx index 0943dd8..f58b450 100644 --- a/frontend-old/archive/src/app/users/page.tsx +++ b/frontend/src/app/users/page.tsx @@ -10,7 +10,7 @@ import React from "react"; import {deleteUser} from "@/utils/api/usersApi"; import {UserType} from "@/types/UserType"; import Card from "@/components/layout/Card"; -import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton"; +import Button from "@/components/ui/Button"; const UsersPage = () => { const { users, isLoading } = useFetchUsers(); @@ -59,10 +59,9 @@ const UsersPage = () => {
- - -

Add User

-
+
diff --git a/frontend-old/app/components/Spinner.tsx b/frontend/src/components/Spinner.tsx similarity index 100% rename from frontend-old/app/components/Spinner.tsx rename to frontend/src/components/Spinner.tsx diff --git a/frontend-old/archive/src/components/features/OnboardingBanner.tsx b/frontend/src/components/features/OnboardingBanner.tsx similarity index 100% rename from frontend-old/archive/src/components/features/OnboardingBanner.tsx rename to frontend/src/components/features/OnboardingBanner.tsx diff --git a/frontend-old/archive/src/components/features/auth/LoginForm.tsx b/frontend/src/components/features/auth/LoginForm.tsx similarity index 88% rename from frontend-old/archive/src/components/features/auth/LoginForm.tsx rename to frontend/src/components/features/auth/LoginForm.tsx index 2189c36..c02be27 100644 --- a/frontend-old/archive/src/components/features/auth/LoginForm.tsx +++ b/frontend/src/components/features/auth/LoginForm.tsx @@ -6,9 +6,10 @@ import { login } from "@/utils/api/auth"; import { useRouter } from 'next/navigation'; import Link from "next/link"; import useRoutes from "@/hooks/useRoutes"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; +import Button from "@/components/ui/Button"; import { useSearchParams } from 'next/navigation'; import Alert from "@/components/ui/Alert"; +import Input from "@/components/ui/Input"; export default function LoginForm() { const { login: authLogin } = useAuth(); @@ -70,23 +71,23 @@ export default function LoginForm() { }
{error &&

{error}

} - setEmail(e.target.value)} required - className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + className="mb-4" /> - setPassword(e.target.value)} required - className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + className="mb-4" /> - Login + Create an account diff --git a/frontend-old/archive/src/components/features/auth/RegistrationForm.tsx b/frontend/src/components/features/auth/RegistrationForm.tsx similarity index 82% rename from frontend-old/archive/src/components/features/auth/RegistrationForm.tsx rename to frontend/src/components/features/auth/RegistrationForm.tsx index 0738381..ea9df3c 100644 --- a/frontend-old/archive/src/components/features/auth/RegistrationForm.tsx +++ b/frontend/src/components/features/auth/RegistrationForm.tsx @@ -6,7 +6,8 @@ import { useRouter } from 'next/navigation'; import useRoutes from "@/hooks/useRoutes"; import Link from "next/link"; import SectionTitle from "@/components/ui/SectionTitle"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; +import Button from "@/components/ui/Button"; +import Input from "@/components/ui/Input"; export default function LoginForm() { const router = useRouter(); @@ -57,44 +58,42 @@ export default function LoginForm() {

Register

{ error &&

{ error }

} - setName(e.target.value) } required - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" /> - setEmail(e.target.value) } required - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" /> - setPassword(e.target.value) } required - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" /> - setPasswordAgain(e.target.value) } required - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" /> - Create Account - + Back to Login diff --git a/frontend-old/archive/src/components/features/dishes/AddUserToDishForm.tsx b/frontend/src/components/features/dishes/AddUserToDishForm.tsx similarity index 89% rename from frontend-old/archive/src/components/features/dishes/AddUserToDishForm.tsx rename to frontend/src/components/features/dishes/AddUserToDishForm.tsx index 2e2d2c8..a5ed556 100644 --- a/frontend-old/archive/src/components/features/dishes/AddUserToDishForm.tsx +++ b/frontend/src/components/features/dishes/AddUserToDishForm.tsx @@ -4,8 +4,7 @@ import { UserType } from "@/types/UserType"; import { useFetchUsers } from "@/hooks/useFetchUsers"; import Spinner from "@/components/Spinner"; import {addUserToDish} from "@/utils/api/dishApi"; -import OutlineButton from "@/components/ui/Buttons/OutlineButton"; -import SolidButton from "@/components/ui/Buttons/SolidButton"; +import Button from "@/components/ui/Button"; interface Props { dish: DishType; @@ -54,14 +53,15 @@ const AddUserToDishForm: FC = ({ dish, reloadDish }) => { return ( <> - setShowAdd(!showAdd)} disabled={remainingUsers.length === 0} type="button" > Add User - + { showAdd && (
@@ -71,7 +71,7 @@ const AddUserToDishForm: FC = ({ dish, reloadDish }) => { setName(e.target.value) } // Update the name state on change - className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900" - placeholder="Enter dish name" - /> -
+ setName(e.target.value) } + placeholder="Enter dish name" + className="mb-4" + /> - { loading ? "Saving..." : "Save Changes" } - +
- } > Back to dishes - + ); diff --git a/frontend-old/archive/src/components/features/dishes/Dish.tsx b/frontend/src/components/features/dishes/Dish.tsx similarity index 100% rename from frontend-old/archive/src/components/features/dishes/Dish.tsx rename to frontend/src/components/features/dishes/Dish.tsx diff --git a/frontend-old/archive/src/components/features/dishes/DishCard.tsx b/frontend/src/components/features/dishes/DishCard.tsx similarity index 100% rename from frontend-old/archive/src/components/features/dishes/DishCard.tsx rename to frontend/src/components/features/dishes/DishCard.tsx diff --git a/frontend-old/archive/src/components/features/dishes/EditDishForm.tsx b/frontend/src/components/features/dishes/EditDishForm.tsx similarity index 75% rename from frontend-old/archive/src/components/features/dishes/EditDishForm.tsx rename to frontend/src/components/features/dishes/EditDishForm.tsx index cfffb2a..6409b16 100644 --- a/frontend-old/archive/src/components/features/dishes/EditDishForm.tsx +++ b/frontend/src/components/features/dishes/EditDishForm.tsx @@ -5,7 +5,8 @@ import {updateDish} from "@/utils/api/dishApi"; import {DishType} from "@/types/DishType"; import useRoutes from "@/hooks/useRoutes"; import Spinner from "@/components/Spinner"; -import Button from "@/components/ui/Button" +import Button from "@/components/ui/Button"; +import Input from "@/components/ui/Input"; interface Props { dish: DishType @@ -57,20 +58,15 @@ const EditDishForm: FC = ({ dish }) => { error != '' && { error } } - {/* Dish name input */} -
- - setName(e.target.value)} // Update the name state on change - className="p-2 border rounded w-full bg-gray-500 border-secondary background-secondary" - /> -
+ setName(e.target.value)} + /> - {/* Save button */} ); } diff --git a/frontend-old/archive/src/components/features/dishes/RecurrenceLabels.tsx b/frontend/src/components/features/dishes/RecurrenceLabels.tsx similarity index 100% rename from frontend-old/archive/src/components/features/dishes/RecurrenceLabels.tsx rename to frontend/src/components/features/dishes/RecurrenceLabels.tsx diff --git a/frontend-old/archive/src/components/features/dishes/SyncUsersForm.tsx b/frontend/src/components/features/dishes/SyncUsersForm.tsx similarity index 100% rename from frontend-old/archive/src/components/features/dishes/SyncUsersForm.tsx rename to frontend/src/components/features/dishes/SyncUsersForm.tsx diff --git a/frontend-old/archive/src/components/features/dishes/UserDishCard.tsx b/frontend/src/components/features/dishes/UserDishCard.tsx similarity index 100% rename from frontend-old/archive/src/components/features/dishes/UserDishCard.tsx rename to frontend/src/components/features/dishes/UserDishCard.tsx diff --git a/frontend-old/archive/src/components/features/navbar/MobileDropdownMenu.tsx b/frontend/src/components/features/navbar/MobileDropdownMenu.tsx similarity index 96% rename from frontend-old/archive/src/components/features/navbar/MobileDropdownMenu.tsx rename to frontend/src/components/features/navbar/MobileDropdownMenu.tsx index 3e8639a..2e74d52 100644 --- a/frontend-old/archive/src/components/features/navbar/MobileDropdownMenu.tsx +++ b/frontend/src/components/features/navbar/MobileDropdownMenu.tsx @@ -17,7 +17,7 @@ const divStyles = classNames( const linkStyles = classNames( 'border-b-2', 'border-secondary', 'uppercase', - 'text-primary', 'hover:background-secondary', 'pb-2', 'pl-5', + 'text-primary', 'hover:bg-secondary', 'pb-2', 'pl-5', 'space-grotesk', 'text-xl' ) diff --git a/frontend-old/app/components/features/schedule/HistoricalDishes.tsx b/frontend/src/components/features/schedule/HistoricalDishes.tsx similarity index 100% rename from frontend-old/app/components/features/schedule/HistoricalDishes.tsx rename to frontend/src/components/features/schedule/HistoricalDishes.tsx diff --git a/frontend-old/archive/src/components/features/schedule/ScheduleCalendar.tsx b/frontend/src/components/features/schedule/ScheduleCalendar.tsx similarity index 100% rename from frontend-old/archive/src/components/features/schedule/ScheduleCalendar.tsx rename to frontend/src/components/features/schedule/ScheduleCalendar.tsx diff --git a/frontend-old/archive/src/components/features/schedule/ScheduleEditForm.tsx b/frontend/src/components/features/schedule/ScheduleEditForm.tsx similarity index 95% rename from frontend-old/archive/src/components/features/schedule/ScheduleEditForm.tsx rename to frontend/src/components/features/schedule/ScheduleEditForm.tsx index 0f436a9..716f69a 100644 --- a/frontend-old/archive/src/components/features/schedule/ScheduleEditForm.tsx +++ b/frontend/src/components/features/schedule/ScheduleEditForm.tsx @@ -77,7 +77,7 @@ const ScheduleEditForm: FC = ({ date }) => { Edit Day
- { transformDate(schedule.date) } + { transformDate(schedule.date) }
@@ -98,7 +98,7 @@ const ScheduleEditForm: FC = ({ date }) => { { scheduleData .map((scheduleData) =>
-
{ scheduleData.user.name }
+
{ scheduleData.user.name }
setName(e.target.value)} - className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary" + setName(e.target.value)} /> - Update +
); diff --git a/frontend-old/archive/src/components/layout/AuthGuard.tsx b/frontend/src/components/layout/AuthGuard.tsx similarity index 100% rename from frontend-old/archive/src/components/layout/AuthGuard.tsx rename to frontend/src/components/layout/AuthGuard.tsx diff --git a/frontend/src/components/layout/Card.tsx b/frontend/src/components/layout/Card.tsx new file mode 100644 index 0000000..055a4b2 --- /dev/null +++ b/frontend/src/components/layout/Card.tsx @@ -0,0 +1,22 @@ +import React, {FC} from "react"; +import classNames from "classnames"; + +interface Props { + children: React.ReactNode; + className?: string; +} + +const Card: FC = ({ children, className }) => { + const styles = classNames( + "w-full border-2 border-secondary p-4 my-2 rounded-lg flex bg-gray-700", + className + ); + + return ( +
+ {children} +
+ ); +} + +export default Card; \ No newline at end of file diff --git a/frontend-old/archive/src/components/layout/NavBar.tsx b/frontend/src/components/layout/NavBar.tsx similarity index 93% rename from frontend-old/archive/src/components/layout/NavBar.tsx rename to frontend/src/components/layout/NavBar.tsx index 67cadb2..56a9cd9 100644 --- a/frontend-old/archive/src/components/layout/NavBar.tsx +++ b/frontend/src/components/layout/NavBar.tsx @@ -53,21 +53,21 @@ const NavBar = () => { {/* Desktop Menu */}
- + Home - + Dishes - + Users - + History + className="text-primary text-right hover:bg-secondary"> Logout
diff --git a/frontend/src/components/ui/Alert.tsx b/frontend/src/components/ui/Alert.tsx new file mode 100644 index 0000000..33bec26 --- /dev/null +++ b/frontend/src/components/ui/Alert.tsx @@ -0,0 +1,29 @@ +import React, {FC} from "react"; +import classNames from "classnames"; + +interface Props { + children: React.ReactNode; + className?: string; + type: 'error' | 'warning' | 'info' | 'success'; +} + +const Alert: FC = ({ children, className, type } ) => { + const styles = classNames( + "px-4 py-3 rounded-lg border-2", + { + 'bg-danger-50 border-danger text-danger-800': type === 'error', + 'bg-warning-50 border-warning text-warning-800': type === 'warning', + 'bg-accent-50 border-accent text-accent-800': type === 'info', + 'bg-success-50 border-success text-success-800': type === 'success', + }, + className + ); + + return ( +
+ {children} +
+ ); +} + +export default Alert; \ No newline at end of file diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..915756c --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,118 @@ +import Link from "next/link"; +import React, { FC, ReactElement, ReactNode } from "react"; +import classNames from "classnames"; + +interface ButtonProps { + appearance?: 'solid' | 'outline' | 'text'; + children: ReactNode; + className?: string; + href?: string; + icon?: ReactNode; + onClick?: () => void; + disabled?: boolean; + size?: 'small' | 'medium' | 'large'; + type?: 'button' | 'submit' | 'reset'; + variant?: 'primary' | 'secondary' | 'accent' | 'danger'; +} + +const Button: FC = ({ + appearance = 'solid', + children, + className, + disabled, + href, + icon, + onClick, + size = 'medium', + type = 'button', + variant = 'primary' +}) => { + const baseStyles = "inline-flex items-center justify-center gap-2 rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + + // Size styles + const sizeStyles = classNames({ + "text-sm py-1.5 px-3": size === "small", + "text-base py-2 px-4": size === "medium", + "text-lg py-3 px-6": size === "large", + }); + + // Variant + Appearance styles + const variantStyles = classNames({ + // Primary Solid + 'bg-primary text-white border-2 border-primary hover:bg-primary-600 focus:ring-primary-500': + variant === 'primary' && appearance === 'solid', + // Primary Outline + 'bg-transparent text-primary border-2 border-primary hover:bg-primary-50 focus:ring-primary-500': + variant === 'primary' && appearance === 'outline', + // Primary Text + 'bg-transparent text-primary border-2 border-transparent hover:bg-primary-50 focus:ring-primary-500': + variant === 'primary' && appearance === 'text', + + // Secondary Solid + 'bg-secondary text-white border-2 border-secondary hover:bg-secondary-600 focus:ring-secondary-500': + variant === 'secondary' && appearance === 'solid', + // Secondary Outline + 'bg-transparent text-secondary border-2 border-secondary hover:bg-secondary-50 focus:ring-secondary-500': + variant === 'secondary' && appearance === 'outline', + // Secondary Text + 'bg-transparent text-secondary border-2 border-transparent hover:bg-secondary-50 focus:ring-secondary-500': + variant === 'secondary' && appearance === 'text', + + // Accent Solid + 'bg-accent text-white border-2 border-accent hover:bg-accent-600 focus:ring-accent-500': + variant === 'accent' && appearance === 'solid', + // Accent Outline + 'bg-transparent text-accent border-2 border-accent hover:bg-accent-50 focus:ring-accent-500': + variant === 'accent' && appearance === 'outline', + // Accent Text + 'bg-transparent text-accent border-2 border-transparent hover:bg-accent-50 focus:ring-accent-500': + variant === 'accent' && appearance === 'text', + + // Danger Solid + 'bg-danger text-white border-2 border-danger hover:bg-danger-600 focus:ring-danger-500': + variant === 'danger' && appearance === 'solid', + // Danger Outline + 'bg-transparent text-danger border-2 border-danger hover:bg-danger-50 focus:ring-danger-500': + variant === 'danger' && appearance === 'outline', + // Danger Text + 'bg-transparent text-danger border-2 border-transparent hover:bg-danger-50 focus:ring-danger-500': + variant === 'danger' && appearance === 'text', + }); + + const styles = classNames(baseStyles, sizeStyles, variantStyles, className); + + const iconClassNames = classNames({ + "h-4 w-4": size === "small", + "h-5 w-5": size === "medium", + "h-6 w-6": size === "large", + }); + + const iconElement = icon && React.isValidElement(icon) + ? React.cloneElement(icon as ReactElement<{ className?: string }>, { + className: iconClassNames, + }) + : null; + + if (href !== undefined) { + return ( + + {iconElement} + {children} + + ); + } + + return ( + + ); +} + +export default Button; \ No newline at end of file diff --git a/frontend/src/components/ui/Checkbox.tsx b/frontend/src/components/ui/Checkbox.tsx new file mode 100644 index 0000000..6db787c --- /dev/null +++ b/frontend/src/components/ui/Checkbox.tsx @@ -0,0 +1,64 @@ +import React, { FC, InputHTMLAttributes } from "react"; +import classNames from "classnames"; + +interface CheckboxProps extends Omit, 'type'> { + label?: string; + error?: string; + helperText?: string; +} + +const Checkbox: FC = ({ + label, + error, + helperText, + className, + id, + ...props +}) => { + const checkboxId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + const checkboxStyles = classNames( + "h-5 w-5 rounded border-2 transition-colors cursor-pointer", + "focus:outline-none focus:ring-2 focus:ring-offset-1", + { + "border-secondary text-primary focus:ring-primary-500": !error, + "border-danger text-danger focus:ring-danger-500": error, + "opacity-50 cursor-not-allowed": props.disabled, + }, + className + ); + + const labelStyles = classNames( + "ml-2 text-sm font-medium cursor-pointer", + { + "text-secondary": !error, + "text-danger": error, + } + ); + + return ( +
+
+ + {label && ( + + )} +
+ {error && ( +

{error}

+ )} + {helperText && !error && ( +

{helperText}

+ )} +
+ ); +}; + +export default Checkbox; diff --git a/frontend-old/app/components/ui/Description.tsx b/frontend/src/components/ui/Description.tsx similarity index 85% rename from frontend-old/app/components/ui/Description.tsx rename to frontend/src/components/ui/Description.tsx index 8b429c0..dea94f2 100644 --- a/frontend-old/app/components/ui/Description.tsx +++ b/frontend/src/components/ui/Description.tsx @@ -7,7 +7,7 @@ interface Props { } const Description = ({ children, className }: Props) => { - const style = classNames("italic font-size-16", + const style = classNames("italic text-base", className ) diff --git a/frontend-old/archive/src/components/ui/Hr.tsx b/frontend/src/components/ui/Hr.tsx similarity index 100% rename from frontend-old/archive/src/components/ui/Hr.tsx rename to frontend/src/components/ui/Hr.tsx diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx new file mode 100644 index 0000000..f23ee4d --- /dev/null +++ b/frontend/src/components/ui/Input.tsx @@ -0,0 +1,68 @@ +import React, { FC, InputHTMLAttributes } from "react"; +import classNames from "classnames"; + +interface InputProps extends Omit, 'size'> { + label?: string; + error?: string; + helperText?: string; + fullWidth?: boolean; +} + +const Input: FC = ({ + label, + error, + helperText, + fullWidth = true, + className, + id, + required, + ...props +}) => { + const inputId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + const inputStyles = classNames( + "px-3 py-2 rounded border-2 transition-colors", + "bg-gray-600 text-secondary", + "focus:outline-none focus:ring-2 focus:ring-offset-1", + { + "border-secondary focus:border-primary focus:ring-primary-500": !error, + "border-danger focus:border-danger focus:ring-danger-500": error, + "w-full": fullWidth, + "opacity-50 cursor-not-allowed": props.disabled, + }, + className + ); + + const labelStyles = classNames( + "block text-sm font-medium mb-1", + { + "text-secondary": !error, + "text-danger": error, + } + ); + + return ( +
+ {label && ( + + )} + + {error && ( +

{error}

+ )} + {helperText && !error && ( +

{helperText}

+ )} +
+ ); +}; + +export default Input; diff --git a/frontend-old/archive/src/components/ui/Label.tsx b/frontend/src/components/ui/Label.tsx similarity index 100% rename from frontend-old/archive/src/components/ui/Label.tsx rename to frontend/src/components/ui/Label.tsx diff --git a/frontend-old/archive/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx similarity index 100% rename from frontend-old/archive/src/components/ui/Modal.tsx rename to frontend/src/components/ui/Modal.tsx diff --git a/frontend-old/archive/src/components/ui/PageTitle.tsx b/frontend/src/components/ui/PageTitle.tsx similarity index 95% rename from frontend-old/archive/src/components/ui/PageTitle.tsx rename to frontend/src/components/ui/PageTitle.tsx index ca65f18..abeafa1 100644 --- a/frontend-old/archive/src/components/ui/PageTitle.tsx +++ b/frontend/src/components/ui/PageTitle.tsx @@ -8,7 +8,7 @@ interface Props { const PageTitle: FC = ({ children, className }) => { const styles = classNames( - 'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold', + 'ml-4 text-2xl font-default uppercase w-full text-accent font-bold', className, ) diff --git a/frontend-old/archive/src/components/ui/RecurrenceInput.tsx b/frontend/src/components/ui/RecurrenceInput.tsx similarity index 100% rename from frontend-old/archive/src/components/ui/RecurrenceInput.tsx rename to frontend/src/components/ui/RecurrenceInput.tsx diff --git a/frontend-old/archive/src/components/ui/SectionTitle.tsx b/frontend/src/components/ui/SectionTitle.tsx similarity index 75% rename from frontend-old/archive/src/components/ui/SectionTitle.tsx rename to frontend/src/components/ui/SectionTitle.tsx index 93d13c4..5940039 100644 --- a/frontend-old/archive/src/components/ui/SectionTitle.tsx +++ b/frontend/src/components/ui/SectionTitle.tsx @@ -6,7 +6,7 @@ interface Props { } const SectionTitle = ({ children, className }: Props) => { - const style = classNames("block font-size-18 uppercase w-full pl-2 text-accent-blue", + const style = classNames("block text-lg uppercase w-full pl-2 text-accent", className ) diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx new file mode 100644 index 0000000..3b04546 --- /dev/null +++ b/frontend/src/components/ui/Select.tsx @@ -0,0 +1,82 @@ +import React, { FC, SelectHTMLAttributes } from "react"; +import classNames from "classnames"; + +interface SelectOption { + value: string | number; + label: string; +} + +interface SelectProps extends Omit, 'size'> { + label?: string; + error?: string; + helperText?: string; + fullWidth?: boolean; + options: SelectOption[]; +} + +const Select: FC = ({ + label, + error, + helperText, + fullWidth = true, + options, + className, + id, + required, + ...props +}) => { + const selectId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + const selectStyles = classNames( + "px-3 py-2 rounded border-2 transition-colors", + "bg-gray-600 text-secondary", + "focus:outline-none focus:ring-2 focus:ring-offset-1", + "cursor-pointer", + { + "border-secondary focus:border-primary focus:ring-primary-500": !error, + "border-danger focus:border-danger focus:ring-danger-500": error, + "w-full": fullWidth, + "opacity-50 cursor-not-allowed": props.disabled, + }, + className + ); + + const labelStyles = classNames( + "block text-sm font-medium mb-1", + { + "text-secondary": !error, + "text-danger": error, + } + ); + + return ( +
+ {label && ( + + )} + + {error && ( +

{error}

+ )} + {helperText && !error && ( +

{helperText}

+ )} +
+ ); +}; + +export default Select; diff --git a/frontend/src/components/ui/Toggle.tsx b/frontend/src/components/ui/Toggle.tsx new file mode 100644 index 0000000..7b36a7a --- /dev/null +++ b/frontend/src/components/ui/Toggle.tsx @@ -0,0 +1,75 @@ +import {FC} from "react"; +import classNames from "classnames"; + +interface ToggleProps { + checked: boolean; + onChange: (checked: boolean) => void; + label?: string; + disabled?: boolean; + helperText?: string; +} + +const Toggle: FC = ({ checked, onChange, label, disabled = false, helperText }) => { + const handleChange = () => { + if (!disabled) { + onChange(!checked); + } + }; + + const containerStyles = classNames( + "flex items-center gap-3 cursor-pointer select-none", + { + "opacity-50 cursor-not-allowed": disabled, + } + ); + + const toggleStyles = classNames( + "relative transition-colors duration-200 w-14 h-7 rounded-full", + { + "bg-success": checked && !disabled, + "bg-gray-400": !checked && !disabled, + "bg-gray-500": disabled, + } + ); + + const sliderStyles = classNames( + "absolute top-0.5 left-0.5 w-6 h-6 rounded-full transform transition-transform duration-200 bg-white shadow-md", + { + "translate-x-7": checked, + "translate-x-0": !checked, + } + ); + + return ( +
+ + {helperText && ( +

{helperText}

+ )} +
+ ); +}; + +export default Toggle; \ No newline at end of file diff --git a/frontend-old/archive/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx similarity index 100% rename from frontend-old/archive/src/context/AuthContext.tsx rename to frontend/src/context/AuthContext.tsx diff --git a/frontend-old/app/helpers/Date.ts b/frontend/src/helpers/Date.ts similarity index 100% rename from frontend-old/app/helpers/Date.ts rename to frontend/src/helpers/Date.ts diff --git a/frontend-old/app/hooks/useFetchDishes.ts b/frontend/src/hooks/useFetchDishes.ts similarity index 100% rename from frontend-old/app/hooks/useFetchDishes.ts rename to frontend/src/hooks/useFetchDishes.ts diff --git a/frontend-old/app/hooks/useFetchUsers.ts b/frontend/src/hooks/useFetchUsers.ts similarity index 100% rename from frontend-old/app/hooks/useFetchUsers.ts rename to frontend/src/hooks/useFetchUsers.ts diff --git a/frontend-old/archive/src/hooks/useRoutes.ts b/frontend/src/hooks/useRoutes.ts similarity index 100% rename from frontend-old/archive/src/hooks/useRoutes.ts rename to frontend/src/hooks/useRoutes.ts diff --git a/frontend-old/app/styles/base/globals.css b/frontend/src/styles/base/globals.css similarity index 100% rename from frontend-old/app/styles/base/globals.css rename to frontend/src/styles/base/globals.css diff --git a/frontend-old/app/styles/components/select.css b/frontend/src/styles/components/select.css similarity index 100% rename from frontend-old/app/styles/components/select.css rename to frontend/src/styles/components/select.css diff --git a/frontend-old/archive/src/styles/main.css b/frontend/src/styles/main.css similarity index 100% rename from frontend-old/archive/src/styles/main.css rename to frontend/src/styles/main.css diff --git a/frontend-old/app/styles/theme/borders.css b/frontend/src/styles/theme/borders.css similarity index 100% rename from frontend-old/app/styles/theme/borders.css rename to frontend/src/styles/theme/borders.css diff --git a/frontend-old/archive/src/styles/theme/colors.css b/frontend/src/styles/theme/colors.css similarity index 100% rename from frontend-old/archive/src/styles/theme/colors.css rename to frontend/src/styles/theme/colors.css diff --git a/frontend-old/app/styles/theme/colors/background.css b/frontend/src/styles/theme/colors/background.css similarity index 100% rename from frontend-old/app/styles/theme/colors/background.css rename to frontend/src/styles/theme/colors/background.css diff --git a/frontend-old/app/styles/theme/colors/border.css b/frontend/src/styles/theme/colors/border.css similarity index 100% rename from frontend-old/app/styles/theme/colors/border.css rename to frontend/src/styles/theme/colors/border.css diff --git a/frontend-old/app/styles/theme/colors/root.css b/frontend/src/styles/theme/colors/root.css similarity index 100% rename from frontend-old/app/styles/theme/colors/root.css rename to frontend/src/styles/theme/colors/root.css diff --git a/frontend-old/archive/src/styles/theme/colors/text.css b/frontend/src/styles/theme/colors/text.css similarity index 100% rename from frontend-old/archive/src/styles/theme/colors/text.css rename to frontend/src/styles/theme/colors/text.css diff --git a/frontend-old/archive/src/styles/theme/fonts.css b/frontend/src/styles/theme/fonts.css similarity index 100% rename from frontend-old/archive/src/styles/theme/fonts.css rename to frontend/src/styles/theme/fonts.css diff --git a/frontend-old/archive/src/types/DishType.ts b/frontend/src/types/DishType.ts similarity index 100% rename from frontend-old/archive/src/types/DishType.ts rename to frontend/src/types/DishType.ts diff --git a/frontend-old/archive/src/types/ScheduleType.ts b/frontend/src/types/ScheduleType.ts similarity index 100% rename from frontend-old/archive/src/types/ScheduleType.ts rename to frontend/src/types/ScheduleType.ts diff --git a/frontend-old/archive/src/types/ScheduledUserDishType.ts b/frontend/src/types/ScheduledUserDishType.ts similarity index 100% rename from frontend-old/archive/src/types/ScheduledUserDishType.ts rename to frontend/src/types/ScheduledUserDishType.ts diff --git a/frontend-old/archive/src/types/UserDishType.ts b/frontend/src/types/UserDishType.ts similarity index 100% rename from frontend-old/archive/src/types/UserDishType.ts rename to frontend/src/types/UserDishType.ts diff --git a/frontend-old/archive/src/types/UserType.ts b/frontend/src/types/UserType.ts similarity index 100% rename from frontend-old/archive/src/types/UserType.ts rename to frontend/src/types/UserType.ts diff --git a/frontend-old/archive/src/utils/api/apiRequest.ts b/frontend/src/utils/api/apiRequest.ts similarity index 100% rename from frontend-old/archive/src/utils/api/apiRequest.ts rename to frontend/src/utils/api/apiRequest.ts diff --git a/frontend-old/app/utils/api/auth.ts b/frontend/src/utils/api/auth.ts similarity index 100% rename from frontend-old/app/utils/api/auth.ts rename to frontend/src/utils/api/auth.ts diff --git a/frontend-old/app/utils/api/dishApi.ts b/frontend/src/utils/api/dishApi.ts similarity index 100% rename from frontend-old/app/utils/api/dishApi.ts rename to frontend/src/utils/api/dishApi.ts diff --git a/frontend-old/app/utils/api/scheduleApi.ts b/frontend/src/utils/api/scheduleApi.ts similarity index 100% rename from frontend-old/app/utils/api/scheduleApi.ts rename to frontend/src/utils/api/scheduleApi.ts diff --git a/frontend-old/app/utils/api/scheduledUserDishesApi.ts b/frontend/src/utils/api/scheduledUserDishesApi.ts similarity index 100% rename from frontend-old/app/utils/api/scheduledUserDishesApi.ts rename to frontend/src/utils/api/scheduledUserDishesApi.ts diff --git a/frontend-old/app/utils/api/userDishApi.ts b/frontend/src/utils/api/userDishApi.ts similarity index 100% rename from frontend-old/app/utils/api/userDishApi.ts rename to frontend/src/utils/api/userDishApi.ts diff --git a/frontend-old/archive/src/utils/api/usersApi.ts b/frontend/src/utils/api/usersApi.ts similarity index 100% rename from frontend-old/archive/src/utils/api/usersApi.ts rename to frontend/src/utils/api/usersApi.ts diff --git a/frontend-old/app/utils/dateBuilder.ts b/frontend/src/utils/dateBuilder.ts similarity index 100% rename from frontend-old/app/utils/dateBuilder.ts rename to frontend/src/utils/dateBuilder.ts diff --git a/frontend-old/app/utils/scheduleBuilder.ts b/frontend/src/utils/scheduleBuilder.ts similarity index 100% rename from frontend-old/app/utils/scheduleBuilder.ts rename to frontend/src/utils/scheduleBuilder.ts diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..9ee2147 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,148 @@ +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + // Primary colors (Rose) + primary: { + DEFAULT: "var(--color-primary)", + 50: "var(--color-rose-50)", + 100: "var(--color-primary-100)", + 200: "var(--color-primary-200)", + 300: "var(--color-primary-300)", + 400: "var(--color-primary-400)", + 500: "var(--color-primary-500)", + 600: "var(--color-primary-600)", + 700: "var(--color-primary-700)", + 800: "var(--color-primary-800)", + 900: "var(--color-primary-900)", + 950: "var(--color-rose-950)", + }, + // Secondary colors (Deluge) + secondary: { + DEFAULT: "var(--color-secondary)", + 50: "var(--color-deluge-50)", + 100: "var(--color-secondary-100)", + 200: "var(--color-secondary-200)", + 300: "var(--color-secondary-300)", + 400: "var(--color-secondary-400)", + 500: "var(--color-secondary-500)", + 600: "var(--color-secondary-600)", + 700: "var(--color-secondary-700)", + 800: "var(--color-secondary-800)", + 900: "var(--color-secondary-900)", + 950: "var(--color-deluge-950)", + }, + // Accent Blue (Malibu) + accent: { + DEFAULT: "var(--color-accent-blue)", + 50: "var(--color-malibu-50)", + 100: "var(--color-accent-blue-100)", + 200: "var(--color-accent-blue-200)", + 300: "var(--color-accent-blue-300)", + 400: "var(--color-accent-blue-400)", + 500: "var(--color-accent-blue-500)", + 600: "var(--color-accent-blue-600)", + 700: "var(--color-accent-blue-700)", + 800: "var(--color-accent-blue-800)", + 900: "var(--color-accent-blue-900)", + 950: "var(--color-malibu-950)", + }, + // Accent Yellow (Gamboge) + yellow: { + DEFAULT: "var(--color-accent-yellow)", + 50: "var(--color-accent-yellow-50)", + 100: "var(--color-accent-yellow-100)", + 200: "var(--color-accent-yellow-200)", + 300: "var(--color-accent-yellow-300)", + 400: "var(--color-accent-yellow-400)", + 500: "var(--color-accent-yellow-500)", + 600: "var(--color-accent-yellow-600)", + 700: "var(--color-accent-yellow-700)", + 800: "var(--color-accent-yellow-800)", + 900: "var(--color-accent-yellow-900)", + 950: "var(--color-accent-yellow-950)", + }, + // Grays (Ebony Clay) + gray: { + DEFAULT: "var(--color-gray-500)", + 100: "var(--color-gray-100)", + 200: "var(--color-gray-200)", + 300: "var(--color-gray-300)", + 400: "var(--color-gray-400)", + 500: "var(--color-gray-500)", + 600: "var(--color-gray-600)", + 700: "var(--color-gray-700)", + 800: "var(--color-gray-800)", + 900: "var(--color-gray-900)", + 950: "var(--color-ebony-clay-950)", + }, + // Semantic colors + danger: { + DEFAULT: "var(--color-danger)", + 50: "var(--color-danger-50)", + 100: "var(--color-danger-100)", + 200: "var(--color-danger-200)", + 300: "var(--color-danger-300)", + 400: "var(--color-danger-400)", + 500: "var(--color-danger-500)", + 600: "var(--color-danger-600)", + 700: "var(--color-danger-700)", + 800: "var(--color-danger-800)", + 900: "var(--color-danger-900)", + 950: "var(--color-danger-950)", + }, + success: { + DEFAULT: "var(--color-success)", + 50: "var(--color-success-50)", + 100: "var(--color-success-100)", + 200: "var(--color-success-200)", + 300: "var(--color-success-300)", + 400: "var(--color-success-400)", + 500: "var(--color-success-500)", + 600: "var(--color-success-600)", + 700: "var(--color-success-700)", + 800: "var(--color-success-800)", + 900: "var(--color-success-900)", + 950: "var(--color-success-950)", + }, + warning: { + DEFAULT: "var(--color-warning)", + 50: "var(--color-warning-50)", + 100: "var(--color-warning-100)", + 200: "var(--color-warning-200)", + 300: "var(--color-warning-300)", + 400: "var(--color-warning-400)", + 500: "var(--color-warning-500)", + 600: "var(--color-warning-600)", + 700: "var(--color-warning-700)", + 800: "var(--color-warning-800)", + 900: "var(--color-warning-900)", + 950: "var(--color-warning-950)", + }, + // Legacy support + background: "var(--color-background)", + foreground: "var(--color-foreground)", + }, + borderRadius: { + DEFAULT: "4px", + sm: "2px", + md: "4px", + lg: "8px", + xl: "12px", + "2xl": "16px", + }, + fontFamily: { + default: ['"Anta"', 'serif'], + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/frontend-old/archive/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from frontend-old/archive/tsconfig.json rename to frontend/tsconfig.json diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..4eb4d39 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,53 @@ +server { + listen 80; + server_name localhost; + + # === FRONTEND === + location / { + proxy_pass http://frontend:3000; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (optional but good for Next.js dev tools etc.) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # === BACKEND API === + location /api/ { + proxy_pass http://backend:80; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # === Sanctum (Laravel token auth) === + location /sanctum/ { + proxy_pass http://backend:80; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # === Client-side routing fallback (Next.js or SPA mode) === + error_page 404 /index.html; + location = /index.html { + proxy_pass http://frontend:3000/index.html; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 09088a2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3040 +0,0 @@ -{ - "name": "app", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "alpinejs": "^3.15.3" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^6.0.3", - "autoprefixer": "^10.4.23", - "axios": "^1.7.4", - "concurrently": "^9.0.1", - "laravel-vite-plugin": "^1.3.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.19", - "vite": "^6.4.1" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", - "vue": "^3.2.25" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", - "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", - "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", - "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.26", - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.6", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", - "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", - "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", - "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", - "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", - "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" - }, - "peerDependencies": { - "vue": "3.5.26" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", - "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/alpinejs": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.3.tgz", - "integrity": "sha512-fSI6F5213FdpMC4IWaup92KhuH3jBX0VVqajRJ6cOTCy1cL6888KyXdGO+seAAkn+g6fnrxBqQEx6gRpQ5EZoQ==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "~3.1.1" - } - }, - "node_modules/alpinejs/node_modules/@vue/reactivity": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", - "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.1.5" - } - }, - "node_modules/alpinejs/node_modules/@vue/shared": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", - "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/laravel-vite-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", - "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "vite-plugin-full-reload": "^1.1.0" - }, - "bin": { - "clean-orphaned-assets": "bin/clean.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-full-reload": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", - "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "picomatch": "^2.3.1" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vue": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", - "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-sfc": "3.5.26", - "@vue/runtime-dom": "3.5.26", - "@vue/server-renderer": "3.5.26", - "@vue/shared": "3.5.26" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7b6cec3..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "private": true, - "type": "module", - "scripts": { - "build": "vite build", - "dev": "vite" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^6.0.3", - "autoprefixer": "^10.4.23", - "axios": "^1.7.4", - "concurrently": "^9.0.1", - "laravel-vite-plugin": "^1.3.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.19", - "vite": "^6.4.1" - }, - "dependencies": { - "alpinejs": "^3.15.3" - } -} diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml deleted file mode 100644 index 24fbe45..0000000 --- a/phpunit.dusk.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - ./tests/Browser - - - diff --git a/public/images/logo-with-text.png b/public/images/logo-with-text.png deleted file mode 100644 index 20025fd..0000000 Binary files a/public/images/logo-with-text.png and /dev/null differ diff --git a/public/images/logo-without-text.png b/public/images/logo-without-text.png deleted file mode 100644 index 287e3c1..0000000 Binary files a/public/images/logo-without-text.png and /dev/null differ diff --git a/resources/css/app.css b/resources/css/app.css deleted file mode 100644 index 3473b92..0000000 --- a/resources/css/app.css +++ /dev/null @@ -1,131 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Syncopate:wght@400;700&family=Space+Grotesk:wght@300..700&display=swap'); - -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - /* Primary Colors */ - --color-primary: #ED1F79; - --color-rose-500: #ED1F79; - - /* Secondary Colors */ - --color-secondary: #7776BC; - --color-deluge-500: #7776BC; - - /* Accent Colors */ - --color-accent-blue: #85C7F2; - --color-malibu-500: #85C7F2; - - /* Gray Scale */ - --color-gray-100: #9AA2B3; - --color-gray-200: #7A8093; - --color-gray-300: #5D637A; - --color-gray-400: #444760; - --color-gray-500: #2B2C41; - --color-gray-600: #24263C; - --color-gray-700: #1D1E36; - --color-gray-800: #131427; - --color-gray-900: #0A0B1C; - - /* Semantic Colors */ - --color-success: #21FA90; - --color-warning: #FF6B35; - --color-danger: #E71D36; -} - -body { - background-color: var(--color-gray-600); - color: var(--color-gray-100); -} - -/* Custom Font Size Classes */ -.font-size-12 { font-size: 12px; } -.font-size-14 { font-size: 14px; } -.font-size-16 { font-size: 16px; } -.font-size-18 { font-size: 18px; } -.font-size-20 { font-size: 20px; } -.font-size-24 { font-size: 24px; } -.font-size-32 { font-size: 32px; } -.font-size-48 { font-size: 48px; } - -/* Font Weight Classes */ -.font-weight-100 { font-weight: 100; } -.font-weight-200 { font-weight: 200; } -.font-weight-300 { font-weight: 300; } -.font-weight-400 { font-weight: 400; } -.font-weight-500 { font-weight: 500; } -.font-weight-600 { font-weight: 600; } -.font-weight-700 { font-weight: 700; } -.font-weight-800 { font-weight: 800; } -.font-weight-900 { font-weight: 900; } - -/* Button Styles */ -.button-primary-solid { - background-color: var(--color-primary); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.25rem; -} - -.button-primary-solid:hover { - background-color: var(--color-secondary); -} - -.button-primary-outline { - background-color: transparent; - color: var(--color-primary); - border: 2px solid var(--color-primary); - padding: 0.5rem 1rem; - border-radius: 0.25rem; -} - -.button-accent-solid { - background-color: var(--color-accent-blue); - color: var(--color-gray-900); - padding: 0.5rem 1rem; - border-radius: 0.25rem; -} - -.button-accent-outline { - background-color: transparent; - color: var(--color-accent-blue); - border: 2px solid var(--color-accent-blue); - padding: 0.5rem 1rem; - border-radius: 0.25rem; -} - -/* Checkbox Styles */ -input[type="checkbox"] { - appearance: none; - -webkit-appearance: none; - width: 1rem; - height: 1rem; - background-color: var(--color-gray-600); - border: 1px solid var(--color-secondary); - border-radius: 0.25rem; - cursor: pointer; - position: relative; -} - -input[type="checkbox"]:checked { - background-color: var(--color-primary); - border-color: var(--color-primary); -} - -input[type="checkbox"]:checked::after { - content: ''; - position: absolute; - left: 4px; - top: 1px; - width: 5px; - height: 9px; - border: solid white; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -input[type="checkbox"]:focus { - outline: none; - box-shadow: 0 0 0 2px var(--color-accent-blue); -} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php deleted file mode 100644 index 15fb4e9..0000000 --- a/resources/views/auth/login.blade.php +++ /dev/null @@ -1,37 +0,0 @@ -@extends('components.layouts.guest') - -@section('content') -

Sign in to your account

- -
- @csrf - - - - - - - - - Sign In - - -
- Don't have an account? - Register -
- -@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php deleted file mode 100644 index 823dbaf..0000000 --- a/resources/views/auth/register.blade.php +++ /dev/null @@ -1,51 +0,0 @@ -@extends('components.layouts.guest') - -@section('content') -

Create an account

- -
- @csrf - - - - - - - - - - - Register - - -
- Already have an account? - Login -
- -@endsection diff --git a/resources/views/billing/index.blade.php b/resources/views/billing/index.blade.php deleted file mode 100644 index 00c8900..0000000 --- a/resources/views/billing/index.blade.php +++ /dev/null @@ -1,106 +0,0 @@ - -
-
-

BILLING

- - @if (session('success')) -
-

{{ session('success') }}

-
- @endif - - @if (session('error')) -
-

{{ session('error') }}

-
- @endif - -
-

Subscription Details

- -
-
- Plan - {{ $planType }} -
- -
- Status - - {{ ucfirst($subscription->stripe_status) }} - -
- - @if($nextBillingDate) -
- Next billing date - {{ $nextBillingDate->format('F j, Y') }} -
- @endif - - @if($subscription->ends_at) -
- Access until - {{ $subscription->ends_at->format('F j, Y') }} -
- @endif - - @if($planner->pm_last_four) -
- Payment method - - {{ ucfirst($planner->pm_type ?? 'Card') }} - โ€ขโ€ขโ€ขโ€ข {{ $planner->pm_last_four }} - Update - -
- @endif -
- - @if(!$subscription->ends_at) -
- -
- @endif -
-
- - -
-
-

Cancel Subscription?

-

- Are you sure you want to cancel your subscription? You will retain access until the end of your current billing period. -

-
- -
- @csrf - -
-
-
-
-
-
diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php deleted file mode 100644 index c49e33c..0000000 --- a/resources/views/components/button.blade.php +++ /dev/null @@ -1,20 +0,0 @@ -@props([ - 'variant' => 'primary', - 'type' => 'button', -]) - -@php - $baseClasses = 'px-4 py-2 rounded transition-colors duration-200 disabled:opacity-50'; - - $variantClasses = match($variant) { - 'primary' => 'bg-primary text-white hover:bg-secondary', - 'outline' => 'border-2 border-secondary text-gray-100 hover:bg-gray-700', - 'danger' => 'bg-danger text-white hover:bg-red-700', - 'danger-outline' => 'border-2 border-danger text-danger hover:bg-danger hover:text-white', - default => 'bg-secondary text-white hover:bg-secondary', - }; -@endphp - - diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php deleted file mode 100644 index 6114e7f..0000000 --- a/resources/views/components/card.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -@props([ - 'padding' => true, -]) - -
merge(['class' => 'border-2 border-secondary rounded-lg bg-gray-650' . ($padding ? ' p-6' : '')]) }}> - {{ $slot }} -
diff --git a/resources/views/components/checkbox.blade.php b/resources/views/components/checkbox.blade.php deleted file mode 100644 index 985731b..0000000 --- a/resources/views/components/checkbox.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@props([ - 'name', - 'label' => null, - 'checked' => false, -]) - -
- -
diff --git a/resources/views/components/input.blade.php b/resources/views/components/input.blade.php deleted file mode 100644 index 576a4f3..0000000 --- a/resources/views/components/input.blade.php +++ /dev/null @@ -1,28 +0,0 @@ -@props([ - 'type' => 'text', - 'name', - 'label' => null, - 'placeholder' => '', - 'value' => null, - 'required' => false, - 'autofocus' => false, -]) - -
- @if($label) - - @endif - - merge(['class' => 'w-full p-2 border rounded bg-gray-700 border-secondary text-gray-100 focus:bg-gray-900 focus:outline-none focus:border-accent-blue' . ($errors->has($name) ? ' border-red-500' : '')]) }}> - - @error($name) - {{ $message }} - @enderror -
diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php deleted file mode 100644 index d893086..0000000 --- a/resources/views/components/layouts/app.blade.php +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - {{ config('app.name', 'Dish Planner') }} - - - @vite(['resources/css/app.css', 'resources/js/app.js']) - @livewireStyles - - -
- - - - -
- - -
- -
- - -
- @auth - - -
-
{{ Auth::user()->name }}
- @if(is_mode_saas()) - - Billing - - @endif -
- @csrf - -
-
- @else -
- Login - @if (Route::has('register')) - Register - @endif -
- @endauth -
-
- - -
- {{ $slot }} -
-
- - @livewireScripts - - {{-- CSRF Token Auto-Refresh for Livewire --}} - - - - - diff --git a/resources/views/components/layouts/guest.blade.php b/resources/views/components/layouts/guest.blade.php deleted file mode 100644 index 7b9107e..0000000 --- a/resources/views/components/layouts/guest.blade.php +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - {{ config('app.name', 'Dish Planner') }} - - - @vite(['resources/css/app.css', 'resources/js/app.js']) - @livewireStyles - - -
- -
- Dish Planner -
- - -
- - @yield('content') - -
-
- - @livewireScripts - - {{-- CSRF Token Auto-Refresh for Livewire --}} - - - \ No newline at end of file diff --git a/resources/views/components/select.blade.php b/resources/views/components/select.blade.php deleted file mode 100644 index 604ab84..0000000 --- a/resources/views/components/select.blade.php +++ /dev/null @@ -1,26 +0,0 @@ -@props([ - 'name', - 'label' => null, - 'options' => [], - 'selected' => null, - 'required' => false, -]) - -
- @if($label) - - @endif - - - - @error($name) - {{ $message }} - @enderror -
diff --git a/resources/views/components/user-multi-select.blade.php b/resources/views/components/user-multi-select.blade.php deleted file mode 100644 index 0429e73..0000000 --- a/resources/views/components/user-multi-select.blade.php +++ /dev/null @@ -1,36 +0,0 @@ -@props([ - 'users', - 'selectedIds' => [], - 'wireModel' => null, - 'toggleAllMethod' => null, - 'label' => 'Users', -]) - -
- -
- @if($toggleAllMethod) - - -
- @endif - - @forelse($users as $user) - - @empty -

No users available.

- @endforelse -
-
\ No newline at end of file diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php deleted file mode 100644 index e77b7b0..0000000 --- a/resources/views/dashboard.blade.php +++ /dev/null @@ -1,31 +0,0 @@ - -
-
- @if (session('success')) -
-

Welcome to Dish Planner!

-

Your subscription is now active. Start planning your dishes!

-
- @endif - -

DASHBOARD

- - -
-
-
\ No newline at end of file diff --git a/resources/views/dishes/index.blade.php b/resources/views/dishes/index.blade.php deleted file mode 100644 index 4cdb214..0000000 --- a/resources/views/dishes/index.blade.php +++ /dev/null @@ -1,7 +0,0 @@ - -
-
- -
-
-
\ No newline at end of file diff --git a/resources/views/livewire/dishes/dishes-list.blade.php b/resources/views/livewire/dishes/dishes-list.blade.php deleted file mode 100644 index 0157fbb..0000000 --- a/resources/views/livewire/dishes/dishes-list.blade.php +++ /dev/null @@ -1,186 +0,0 @@ -
-
-

MANAGE DISHES

- -
- - - @if (session()->has('success')) -
- {{ session('success') }} -
- @endif - - @if (session()->has('error')) -
- {{ session('error') }} -
- @endif - - -
- @forelse ($dishes as $dish) -
-
-
-

{{ $dish->name }}

- - -
- @forelse($dish->users as $user) - -
- {{ strtoupper(substr($user->name, 0, 1)) }} -
- {{ $user->name }} -
- @empty - No users assigned - @endforelse -
-
- -
- - -
-
-
- @empty -
-

No dishes found.

-
- @endforelse -
- - -
- {{ $dishes->links() }} -
- - - @if($showCreateModal) -
-
-

Add New Dish

- -
-
- - - @error('name') {{ $message }} @enderror -
- - @if($users->count() > 0) - - @error('selectedUsers') {{ $message }} @enderror - @else -
-

No users available to assign. Add users to assign them to dishes.

-
- @endif - -
- - -
- -
-
- @endif - - - @if($showEditModal && $editingDish) -
-
-

Edit Dish

- -
-
- - - @error('name') {{ $message }} @enderror -
- - @if($users->count() > 0) - - @error('selectedUsers') {{ $message }} @enderror - @else -
-

No users available to assign. Add users to assign them to dishes.

-
- @endif - -
- - -
- -
-
- @endif - - - @if($showDeleteModal && $deletingDish) -
-
-

Delete Dish

-

- Are you sure you want to delete {{ $deletingDish->name }}? This action cannot be undone. -

- -
- - -
-
-
- @endif -
\ No newline at end of file diff --git a/resources/views/livewire/schedule/schedule-calendar.blade.php b/resources/views/livewire/schedule/schedule-calendar.blade.php deleted file mode 100644 index 83fad1f..0000000 --- a/resources/views/livewire/schedule/schedule-calendar.blade.php +++ /dev/null @@ -1,315 +0,0 @@ -
-

SCHEDULE

- - - - - - @if (session()->has('success')) -
- {{ session('success') }} -
- @endif - - @if (session()->has('error')) -
- {{ session('error') }} -
- @endif - - -
- - -

{{ $this->monthName }}

- - -
- - - - - -
- @foreach($calendarDays as $dayData) - @if($dayData['day']) -
- - -
-
- {{ $dayData['date']->format('D, M j') }} - @if($dayData['isToday']) - (Today) - @endif -
- -
- - - @if($dayData['scheduledDishes']->isNotEmpty()) -
- @foreach($dayData['scheduledDishes'] as $scheduled) -
-
-
- {{ strtoupper(substr($scheduled->user->name, 0, 1)) }} -
-
-
{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}
-
{{ $scheduled->user->name }}
-
-
- - -
- - -
- - - - -
-
-
- @endforeach -
- @else -
No dishes scheduled
- @endif -
- @endif - @endforeach -
- - - - @if($showRegenerateModal) -
-
-

Regenerate Day

-

- This will clear the selected day and allow for regeneration. Continue? -

- -
- - -
-
-
- @endif - - - @if($showEditDishModal) -
-
-

Edit Dish

-

- Choose a dish for {{ \App\Models\User::find($editUserId)?->name }} on {{ \Carbon\Carbon::parse($editDate)->format('M j, Y') }} -

- - @if(count($availableDishes) > 0) -
- - -
- @else -
-

- No dishes available for this user. - Add dishes first. -

-
- @endif - -
- - @if(count($availableDishes) > 0) - - @endif -
-
-
- @endif - - - @if($showAddDishModal) -
-
-

Add Dish

-

- Add a dish for {{ \Carbon\Carbon::parse($addDate)->format('M j, Y') }} -

- - - - @if(count($addUserIds) > 0) -
- - @if(count($addAvailableDishes) > 0) - - @else -

- No dishes in common for selected users. - Add dishes first. -

- @endif -
- @endif - -
- - @if(count($addAvailableUsers) > 0 && count($addAvailableDishes) > 0) - - @endif -
-
-
- @endif - - -
\ No newline at end of file diff --git a/resources/views/livewire/schedule/schedule-generator.blade.php b/resources/views/livewire/schedule/schedule-generator.blade.php deleted file mode 100644 index 6f0c7c6..0000000 --- a/resources/views/livewire/schedule/schedule-generator.blade.php +++ /dev/null @@ -1,110 +0,0 @@ -
- - -
-
- -
- - -
- - -
- - -
- - -
- -
-
- - -
- -
- @foreach($users as $user) - - @endforeach -
- @error('selectedUsers') {{ $message }} @enderror -
- - -
- -
- - @if($showAdvancedOptions) -
-

Advanced Options

-

- Future features: Recurrence patterns, dish rotation rules, dietary restrictions, etc. -

-
- These options will be available in future updates. -
-
- @endif - - -
- - - -
- - -
-
- - - - - Generating schedule... -
-
-
-
\ No newline at end of file diff --git a/resources/views/livewire/users/users-list.blade.php b/resources/views/livewire/users/users-list.blade.php deleted file mode 100644 index 81c593d..0000000 --- a/resources/views/livewire/users/users-list.blade.php +++ /dev/null @@ -1,148 +0,0 @@ -
-
-

MANAGE USERS

- -
- - - @if (session()->has('success')) -
- {{ session('success') }} -
- @endif - - @if (session()->has('error')) -
- {{ session('error') }} -
- @endif - - -
- @forelse ($users as $user) -
-
-
- {{ strtoupper(substr($user->name, 0, 1)) }} -
-
-

{{ $user->name }}

-
-
- -
- - -
-
- @empty -
-

No users found.

-
- @endforelse -
- - -
- {{ $users->links() }} -
- - - @if($showCreateModal) -
-
-

Add New User

- -
-
- - - @error('name') {{ $message }} @enderror -
- -
- - -
-
-
-
- @endif - - - @if($showEditModal && $editingUser) -
-
-

Edit User

- -
-
- - - @error('name') {{ $message }} @enderror -
- -
- - -
-
-
-
- @endif - - - @if($showDeleteModal && $deletingUser) -
-
-

Delete User

-

- Are you sure you want to delete {{ $deletingUser->name }}? This action cannot be undone. -

- -
- - -
-
-
- @endif -
\ No newline at end of file diff --git a/resources/views/schedule/index.blade.php b/resources/views/schedule/index.blade.php deleted file mode 100644 index 5b8cc0b..0000000 --- a/resources/views/schedule/index.blade.php +++ /dev/null @@ -1,7 +0,0 @@ - -
-
- -
-
-
\ No newline at end of file diff --git a/resources/views/subscription/index.blade.php b/resources/views/subscription/index.blade.php deleted file mode 100644 index a44e7e9..0000000 --- a/resources/views/subscription/index.blade.php +++ /dev/null @@ -1,51 +0,0 @@ - -
-
-

SUBSCRIPTION

- - @if(auth()->user()->subscribed()) -
-

Active Subscription

-

You have an active subscription.

- -
- @csrf - -
-
- @else -
-

Subscribe to Dish Planner

- -
-
- @csrf - -
-

Monthly

-

Billed monthly

- -
-
- -
- @csrf - -
-

Yearly

-

Billed annually

- -
-
-
-
- @endif -
-
-
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php deleted file mode 100644 index c156967..0000000 --- a/resources/views/users/index.blade.php +++ /dev/null @@ -1,7 +0,0 @@ - -
-
- -
-
-
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php deleted file mode 100644 index f23fbc5..0000000 --- a/routes/web.php +++ /dev/null @@ -1,50 +0,0 @@ -route('dashboard'); -}); - -// Guest routes -Route::middleware('guest')->group(function () { - Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login'); - Route::post('/login', [LoginController::class, 'login']); - Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register'); - Route::post('/register', [RegisterController::class, 'register']); -}); - -// CSRF refresh route (available to both guest and authenticated users) -Route::get('/refresh-csrf', function () { - return response()->json(['token' => csrf_token()]); -})->name('refresh-csrf'); - -// Authenticated routes -Route::middleware('auth')->group(function () { - Route::post('/logout', [LoginController::class, 'logout'])->name('logout'); - - // Routes requiring active subscription in SaaS mode - Route::middleware('subscription')->group(function () { - Route::get('/dashboard', function () { - return view('dashboard'); - })->name('dashboard'); - - Route::get('/dishes', function () { - return view('dishes.index'); - })->name('dishes.index'); - - Route::get('/schedule', function () { - return view('schedule.index'); - })->name('schedule.index'); - - Route::get('/users', function () { - return view('users.index'); - })->name('users.index'); - - Route::get('/billing', [SubscriptionController::class, 'billing'])->name('billing')->middleware('saas'); - Route::get('/billing/portal', [SubscriptionController::class, 'billingPortal'])->name('billing.portal')->middleware('saas'); - }); -}); diff --git a/routes/web/subscription.php b/routes/web/subscription.php deleted file mode 100644 index d3258b9..0000000 --- a/routes/web/subscription.php +++ /dev/null @@ -1,18 +0,0 @@ -name('cashier.webhook'); - -Route::middleware('auth')->group(function () { - Route::get('/subscription', function () { - return view('subscription.index'); - })->name('subscription.index'); - - Route::post('/subscription/checkout', [SubscriptionController::class, 'checkout'])->name('subscription.checkout'); - Route::get('/subscription/success', [SubscriptionController::class, 'success'])->name('subscription.success'); - Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel'])->name('subscription.cancel'); -}); diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 4ff3195..0000000 --- a/shell.nix +++ /dev/null @@ -1,185 +0,0 @@ -{ pkgs ? import {} }: - -pkgs.mkShell { - buildInputs = with pkgs; [ - # PHP and tools - php83 - php83Packages.composer - - # Node.js and npm - nodejs_20 - - # Container tools - podman - podman-compose - - # Database client (optional, for direct DB access) - mariadb.client - - # Utilities - git - curl - gnumake - ]; - - shellHook = '' - # Export user/group IDs for Docker permission matching - export USER_ID=$(id -u) - export GROUP_ID=$(id -g) - # Use keep-id for proper permission mapping in rootless podman - export PODMAN_USERNS=keep-id - - # Define helper functions - dev-rebuild() { - echo "๐Ÿ”จ Rebuilding development environment..." - PODMAN_USERNS=keep-id podman-compose down -v - PODMAN_USERNS=keep-id podman-compose build --no-cache app - PODMAN_USERNS=keep-id podman-compose up -d - echo "โœ… Rebuild complete! Check logs with: dev-logs" - } - - dev-rebuild-quick() { - echo "โšก Quick rebuild (keeping volumes)..." - PODMAN_USERNS=keep-id podman-compose down - PODMAN_USERNS=keep-id podman-compose build app - PODMAN_USERNS=keep-id podman-compose up -d - echo "โœ… Quick rebuild complete!" - } - - dev-up() { - echo "๐Ÿš€ Starting development environment..." - PODMAN_USERNS=keep-id podman-compose up -d - echo "โœ… Dev environment started!" - } - - dev-down() { - echo "๐Ÿ›‘ Stopping development environment..." - podman-compose down - echo "โœ… Dev environment stopped!" - } - - dev-restart() { - echo "๐Ÿ”„ Restarting development environment..." - podman-compose restart - echo "โœ… Dev environment restarted!" - } - - dev-logs() { - podman-compose logs -f "$@" - } - - dev-shell() { - podman-compose exec app sh - } - - dev-artisan() { - podman-compose exec app php artisan "$@" - } - - dev-fix-permissions() { - echo "๐Ÿ”ง Fixing file permissions..." - echo "This will require sudo to fix Docker-created files" - sudo chown -R $(id -u):$(id -g) storage/ bootstrap/cache/ vendor/ node_modules/ 2>/dev/null || true - echo "โœ… Permissions fixed!" - } - - prod-build() { - local TAG="''${1:-latest}" - local REGISTRY="codeberg.org" - local NAMESPACE="lvl0" - local IMAGE_NAME="dish-planner" - - echo "๐Ÿ”จ Building production image..." - podman build -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} . - - echo "โœ… Build complete: ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}" - echo "Run 'prod-push' to push to Codeberg" - } - - prod-push() { - local TAG="''${1:-latest}" - local REGISTRY="codeberg.org" - local NAMESPACE="lvl0" - local IMAGE_NAME="dish-planner" - - echo "๐Ÿ“ค Pushing to Codeberg registry..." - if podman push ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}; then - echo "โœ… Image pushed to ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}" - else - echo "โŒ Failed to push image. Did you run 'prod-build' first?" - echo " Also make sure you're logged in with 'prod-login'" - return 1 - fi - } - - prod-build-nc() { - local TAG="''${1:-latest}" - local REGISTRY="codeberg.org" - local NAMESPACE="lvl0" - local IMAGE_NAME="dish-planner" - - echo "๐Ÿ”จ Building production image (no cache)..." - podman build --no-cache -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} . - - echo "โœ… Build complete: ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}" - echo "Run 'prod-push' to push to Codeberg" - } - - prod-build-push() { - local TAG="''${1:-latest}" - prod-build "$TAG" && prod-push "$TAG" - } - - prod-login() { - echo "๐Ÿ“ Logging into Codeberg registry..." - podman login codeberg.org - } - - echo "๐Ÿš€ Dish Planner Development Environment" - echo "=======================================" - echo "PHP: $(php --version | head -n1)" - echo "Node: $(node --version)" - echo "Podman: $(podman --version)" - echo "Podman-compose: $(podman-compose --version 2>/dev/null || echo 'checking...')" - echo "" - echo "Development commands:" - echo " dev-up - Start development environment" - echo " dev-down - Stop development environment" - echo " dev-restart - Restart containers" - echo " dev-rebuild - Full rebuild (removes volumes)" - echo " dev-rebuild-quick - Quick rebuild (keeps volumes)" - echo " dev-logs [svc] - Follow logs (default: all)" - echo " dev-shell - Enter app container" - echo " dev-artisan - Run artisan commands" - echo " dev-fix-permissions - Fix Docker-created file permissions" - echo "" - echo "Production commands:" - echo " prod-login - Login to Codeberg registry" - echo " prod-build [tag] - Build production image (default: latest)" - echo " prod-push [tag] - Push image to Codeberg" - echo " prod-build-push - Build and push in one command" - echo "" - - # Auto-start prompt - if [ -f "docker-compose.yml" ]; then - read -p "Start development containers? (y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "Starting containers..." - podman-compose up -d - - # Wait a moment for containers to start - sleep 3 - - echo "" - echo "โœ… Services should be available at:" - echo " App: http://localhost:8000" - echo " Vite: http://localhost:5173" - echo " Mailhog: http://localhost:8025" - echo " MariaDB: localhost:3306" - echo "" - echo "Run 'podman-compose logs -f app' to follow logs" - fi - fi - ''; -} \ No newline at end of file diff --git a/src/DishPlanner/Schedule/Actions/ClearScheduleForMonthAction.php b/src/DishPlanner/Schedule/Actions/ClearScheduleForMonthAction.php deleted file mode 100644 index f203d30..0000000 --- a/src/DishPlanner/Schedule/Actions/ClearScheduleForMonthAction.php +++ /dev/null @@ -1,26 +0,0 @@ -startOfDay(); - $endDate = $startDate->copy()->endOfMonth()->endOfDay(); - - $scheduleIds = Schedule::withoutGlobalScopes() - ->where('planner_id', $planner->id) - ->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]) - ->pluck('id'); - - ScheduledUserDish::whereIn('schedule_id', $scheduleIds) - ->whereIn('user_id', $userIds) - ->delete(); - } -} diff --git a/src/DishPlanner/Schedule/Actions/GenerateScheduleForMonthAction.php b/src/DishPlanner/Schedule/Actions/GenerateScheduleForMonthAction.php deleted file mode 100644 index 1f5937d..0000000 --- a/src/DishPlanner/Schedule/Actions/GenerateScheduleForMonthAction.php +++ /dev/null @@ -1,102 +0,0 @@ -copy()->endOfMonth(); - - if ($clearExisting) { - $this->clearExistingSchedules($planner, $startDate, $endDate, $userIds); - } - - $userDishesMap = $this->loadUserDishes($planner, $userIds); - - $this->generateSchedulesForPeriod($planner, $startDate, $endDate, $userIds, $userDishesMap); - }); - } - - private function clearExistingSchedules( - Planner $planner, - Carbon $startDate, - Carbon $endDate, - array $userIds - ): void { - $scheduleIds = Schedule::withoutGlobalScopes() - ->where('planner_id', $planner->id) - ->whereBetween('date', [$startDate, $endDate]) - ->pluck('id'); - - ScheduledUserDish::whereIn('schedule_id', $scheduleIds) - ->whereIn('user_id', $userIds) - ->delete(); - } - - private function loadUserDishes(Planner $planner, array $userIds): array - { - $users = User::query() - ->with('userDishes.dish') - ->whereIn('id', $userIds) - ->where('planner_id', $planner->id) - ->get() - ->keyBy('id'); - - $userDishesMap = []; - foreach ($users as $userId => $user) { - if ($user->userDishes->isNotEmpty()) { - $userDishesMap[$userId] = $user->userDishes; - } - } - - return $userDishesMap; - } - - private function generateSchedulesForPeriod( - Planner $planner, - Carbon $startDate, - Carbon $endDate, - array $userIds, - array $userDishesMap - ): void { - $currentDate = $startDate->copy(); - - while ($currentDate <= $endDate) { - $schedule = Schedule::firstOrCreate( - ['planner_id' => $planner->id, 'date' => $currentDate->format('Y-m-d')], - ['is_skipped' => false] - ); - - foreach ($userIds as $userId) { - if (!isset($userDishesMap[$userId]) || $userDishesMap[$userId]->isEmpty()) { - continue; - } - - $randomUserDish = $userDishesMap[$userId]->random(); - - ScheduledUserDish::firstOrCreate( - ['schedule_id' => $schedule->id, 'user_id' => $userId], - ['user_dish_id' => $randomUserDish->id, 'is_skipped' => false] - ); - } - - $currentDate->addDay(); - } - } -} diff --git a/src/DishPlanner/Schedule/Actions/RegenerateScheduleForDateForUsersAction.php b/src/DishPlanner/Schedule/Actions/RegenerateScheduleForDateForUsersAction.php deleted file mode 100644 index 8609b09..0000000 --- a/src/DishPlanner/Schedule/Actions/RegenerateScheduleForDateForUsersAction.php +++ /dev/null @@ -1,45 +0,0 @@ - $planner->id, 'date' => $date->format('Y-m-d')], - ['is_skipped' => false] - ); - - ScheduledUserDish::where('schedule_id', $schedule->id) - ->whereIn('user_id', $userIds) - ->delete(); - - $users = User::with('userDishes.dish') - ->whereIn('id', $userIds) - ->where('planner_id', $planner->id) - ->get(); - - foreach ($users as $user) { - if ($user->userDishes->isNotEmpty()) { - $randomUserDish = $user->userDishes->random(); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $randomUserDish->id, - 'is_skipped' => false, - ]); - } - } - }); - } -} diff --git a/src/DishPlanner/Schedule/Services/ScheduleCalendarService.php b/src/DishPlanner/Schedule/Services/ScheduleCalendarService.php deleted file mode 100644 index 8b64d3d..0000000 --- a/src/DishPlanner/Schedule/Services/ScheduleCalendarService.php +++ /dev/null @@ -1,66 +0,0 @@ -copy()->endOfMonth(); - $daysInMonth = $firstDay->daysInMonth; - - $schedules = $this->loadSchedulesForMonth($planner, $firstDay, $lastDay); - - return $this->buildCalendarDays($year, $month, $daysInMonth, $schedules); - } - - private function loadSchedulesForMonth(Planner $planner, Carbon $startDate, Carbon $endDate): Collection - { - return Schedule::with(['scheduledUserDishes.user', 'scheduledUserDishes.userDish.dish']) - ->where('planner_id', $planner->id) - ->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]) - ->get() - ->keyBy(fn ($schedule) => $schedule->date->day); - } - - private function buildCalendarDays(int $year, int $month, int $daysInMonth, Collection $schedules): array - { - $calendarDays = []; - - for ($day = 1; $day <= 31; $day++) { - if ($day <= $daysInMonth) { - $date = Carbon::createFromDate($year, $month, $day); - $scheduledDishes = $schedules->get($day)?->scheduledUserDishes ?? collect(); - - $calendarDays[] = [ - 'day' => $day, - 'date' => $date, - 'isToday' => $date->isToday(), - 'scheduledDishes' => $scheduledDishes, - 'isEmpty' => $scheduledDishes->isEmpty() - ]; - } else { - $calendarDays[] = [ - 'day' => null, - 'date' => null, - 'isToday' => false, - 'scheduledDishes' => collect(), - 'isEmpty' => true - ]; - } - } - - return $calendarDays; - } - - public function getMonthName(int $month, int $year): string - { - return Carbon::createFromDate($year, $month, 1)->format('F Y'); - } -} diff --git a/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateAction.php b/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateAction.php deleted file mode 100644 index b724d8a..0000000 --- a/src/DishPlanner/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateAction.php +++ /dev/null @@ -1,28 +0,0 @@ -where('planner_id', $planner->id) - ->whereDate('date', $date) - ->first(); - - if (! $schedule) { - return false; - } - - return ScheduledUserDish::query() - ->where('schedule_id', $schedule->id) - ->where('user_id', $userId) - ->delete() > 0; - } -} diff --git a/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php b/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php deleted file mode 100644 index 441093c..0000000 --- a/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php +++ /dev/null @@ -1,39 +0,0 @@ -where('planner_id', $planner->id) - ->whereDate('date', $date) - ->first(); - - if (! $schedule) { - return false; - } - - $scheduledUserDish = ScheduledUserDish::query() - ->where('schedule_id', $schedule->id) - ->where('user_id', $userId) - ->first(); - - if (! $scheduledUserDish) { - return false; - } - - $scheduledUserDish->update([ - 'is_skipped' => true, - 'user_dish_id' => null, - ]); - - return true; - } -} diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index d4cfcca..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,45 +0,0 @@ -import defaultTheme from 'tailwindcss/defaultTheme'; - -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', - './vendor/livewire/livewire/resources/views/*.blade.php', - './storage/framework/views/*.php', - './resources/**/*.blade.php', - './resources/**/*.js', - './resources/**/*.vue', - './app/Livewire/**/*.php', - ], - theme: { - extend: { - fontFamily: { - 'sans': ['Space Grotesk', ...defaultTheme.fontFamily.sans], - 'syncopate': ['Syncopate', 'sans-serif'], - 'space-grotesk': ['Space Grotesk', 'sans-serif'], - 'default': [...defaultTheme.fontFamily.sans], - }, - colors: { - 'primary': '#ED1F79', - 'secondary': '#7776BC', - 'accent-blue': '#85C7F2', - 'gray': { - 100: '#9AA2B3', - 200: '#7A8093', - 300: '#5D637A', - 400: '#444760', - 500: '#2B2C41', - 600: '#24263C', - 650: '#202239', - 700: '#1D1E36', - 800: '#131427', - 900: '#0A0B1C', - }, - 'success': '#21FA90', - 'warning': '#FF6B35', - 'danger': '#E71D36', - }, - }, - }, - plugins: [], -}; diff --git a/tests/Browser/Auth/LoginTest.php b/tests/Browser/Auth/LoginTest.php deleted file mode 100644 index 8eaa75b..0000000 --- a/tests/Browser/Auth/LoginTest.php +++ /dev/null @@ -1,89 +0,0 @@ -unique()->safeEmail(); - - self::$testPlanner = Planner::factory()->create([ - 'email' => self::$testEmail, - 'password' => Hash::make(self::$testPassword), - ]); - } - } - - public function testSuccessfulLogin(): void - { - $this->ensureTestPlannerExists(); - - $this->browse(function (Browser $browser) { - $browser->driver->manage()->deleteAllCookies(); - $browser->visit('http://dishplanner_app:8000/login') - ->waitFor('input[id="email"]', self::TIMEOUT_SHORT) - ->clear('input[id="email"]') - ->type('input[id="email"]', self::$testEmail) - ->clear('input[id="password"]') - ->type('input[id="password"]', self::$testPassword) - ->press('Login') - ->waitForLocation('/dashboard', self::TIMEOUT_MEDIUM) - ->assertPathIs('/dashboard'); - }); - } - - public function testLoginWithWrongCredentials(): void - { - $this->ensureTestPlannerExists(); - - $this->browse(function (Browser $browser) { - $browser->driver->manage()->deleteAllCookies(); - $browser->visit('http://dishplanner_app:8000/login') - ->waitFor('input[id="email"]', self::TIMEOUT_SHORT) - ->clear('input[id="email"]') - ->type('input[id="email"]', self::$testEmail) - ->clear('input[id="password"]') - ->type('input[id="password"]', 'wrongpassword') - ->press('Login') - ->pause(self::PAUSE_MEDIUM) - ->assertPathIs('/login') - ->assertSee('These credentials do not match our records'); - }); - } - - public function testLoginFormRequiredFields(): void - { - $this->browse(function (Browser $browser) { - $browser->driver->manage()->deleteAllCookies(); - $browser->visit('http://dishplanner_app:8000/login') - ->waitFor('input[id="email"]', self::TIMEOUT_SHORT); - - // Check that both fields have the required attribute - $browser->assertAttribute('input[id="email"]', 'required', 'true'); - $browser->assertAttribute('input[id="password"]', 'required', 'true'); - - // Verify email field is type email - $browser->assertAttribute('input[id="email"]', 'type', 'email'); - - // Verify password field is type password - $browser->assertAttribute('input[id="password"]', 'type', 'password'); - - // Test that we stay on login page if we try to submit with empty fields - $browser->press('Login') - ->pause(self::PAUSE_SHORT) - ->assertPathIs('/login'); - }); - } -} diff --git a/tests/Browser/Components/DishModal.php b/tests/Browser/Components/DishModal.php deleted file mode 100644 index 7baff18..0000000 --- a/tests/Browser/Components/DishModal.php +++ /dev/null @@ -1,105 +0,0 @@ -mode = $mode; - } - - /** - * Get the root selector for the component. - */ - public function selector(): string - { - // Livewire modals typically have a specific structure - return '[role="dialog"], .fixed.inset-0'; - } - - /** - * Assert that the browser page contains the component. - */ - public function assert(Browser $browser): void - { - $browser->assertVisible($this->selector()); - - if ($this->mode === 'create') { - $browser->assertSee('Add New Dish'); - } else { - $browser->assertSee('Edit Dish'); - } - } - - /** - * Get the element shortcuts for the component. - * - * @return array - */ - public function elements(): array - { - return [ - '@name-input' => 'input[wire\\:model="name"]', - '@description-input' => 'textarea[wire\\:model="description"]', - '@users-section' => 'div:contains("Assign to Users")', - '@submit-button' => $this->mode === 'create' ? 'button:contains("Create Dish")' : 'button:contains("Update Dish")', - '@cancel-button' => 'button:contains("Cancel")', - '@validation-error' => '.text-red-500', - ]; - } - - /** - * Fill the dish form. - */ - public function fillForm(Browser $browser, string $name, ?string $description = null): void - { - $browser->waitFor('@name-input') - ->clear('@name-input') - ->type('@name-input', $name); - - if ($description !== null && $browser->element('@description-input')) { - $browser->clear('@description-input') - ->type('@description-input', $description); - } - } - - /** - * Select users to assign the dish to. - */ - public function selectUsers(Browser $browser, array $userIds): void - { - foreach ($userIds as $userId) { - $browser->check("input[type='checkbox'][value='{$userId}']"); - } - } - - /** - * Submit the form. - */ - public function submit(Browser $browser): void - { - $browser->press($this->mode === 'create' ? 'Create Dish' : 'Update Dish'); - } - - /** - * Cancel the modal. - */ - public function cancel(Browser $browser): void - { - $browser->press('Cancel'); - } - - /** - * Assert validation error is shown. - */ - public function assertValidationError(Browser $browser, string $message = 'required'): void - { - $browser->assertSee($message); - } -} \ No newline at end of file diff --git a/tests/Browser/Components/LoginForm.php b/tests/Browser/Components/LoginForm.php deleted file mode 100644 index f526ff3..0000000 --- a/tests/Browser/Components/LoginForm.php +++ /dev/null @@ -1,89 +0,0 @@ -assertVisible($this->selector()) - ->assertVisible('@email') - ->assertVisible('@password') - ->assertVisible('@submit'); - } - - /** - * Get the element shortcuts for the component. - * - * @return array - */ - public function elements(): array - { - return [ - '@email' => 'input[id="email"]', - '@password' => 'input[id="password"]', - '@submit' => 'button[type="submit"]', - '@remember' => 'input[name="remember"]', - '@error' => '.text-red-500', - ]; - } - - /** - * Fill in the login form. - */ - public function fillForm(Browser $browser, string $email, string $password): void - { - $browser->type('@email', $email) - ->type('@password', $password); - } - - /** - * Submit the login form. - */ - public function submit(Browser $browser): void - { - $browser->press('@submit'); - } - - /** - * Login with the given credentials. - */ - public function loginWith(Browser $browser, string $email, string $password): void - { - $this->fillForm($browser, $email, $password); - $this->submit($browser); - } - - /** - * Assert that the form fields are required. - */ - public function assertFieldsRequired(Browser $browser): void - { - $browser->assertAttribute('@email', 'required', 'true') - ->assertAttribute('@password', 'required', 'true') - ->assertAttribute('@email', 'type', 'email') - ->assertAttribute('@password', 'type', 'password'); - } - - /** - * Assert that the form has validation errors. - */ - public function assertHasErrors(Browser $browser): void - { - $browser->assertPresent('@error'); - } -} \ No newline at end of file diff --git a/tests/Browser/Dishes/CreateDishFormValidationTest.php b/tests/Browser/Dishes/CreateDishFormValidationTest.php deleted file mode 100644 index a59c89c..0000000 --- a/tests/Browser/Dishes/CreateDishFormValidationTest.php +++ /dev/null @@ -1,49 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser); - - $browser->on(new DishesPage) - ->openCreateModal() - ->within(new DishModal('create'), function ($browser) { - $browser->fillForm('', null) - ->submit() - ->pause(2000) - ->assertValidationError('required'); - }); - }); - } -} \ No newline at end of file diff --git a/tests/Browser/Dishes/CreateDishSuccessTest.php b/tests/Browser/Dishes/CreateDishSuccessTest.php deleted file mode 100644 index 822dc77..0000000 --- a/tests/Browser/Dishes/CreateDishSuccessTest.php +++ /dev/null @@ -1,52 +0,0 @@ -browse(function (Browser $browser) { - $dishName = 'Test Dish ' . uniqid(); - - $this->loginAndGoToDishes($browser); - - $browser->on(new DishesPage) - ->openCreateModal() - ->within(new DishModal('create'), function ($browser) use ($dishName) { - $browser->fillForm($dishName) - ->submit(); - }) - ->pause(3000) - ->assertDishVisible($dishName) - ->assertSee('Dish created successfully'); - }); - } -} \ No newline at end of file diff --git a/tests/Browser/Dishes/CreateDishTest.php b/tests/Browser/Dishes/CreateDishTest.php deleted file mode 100644 index 9a333e1..0000000 --- a/tests/Browser/Dishes/CreateDishTest.php +++ /dev/null @@ -1,47 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser); - - $browser->on(new DishesPage) - ->assertSee('MANAGE DISHES') - ->assertSee('Add Dish'); - }); - } - - // TODO: Moved to separate single-method test files to avoid static planner issues - // See: OpenCreateDishModalTest, CreateDishFormValidationTest, CancelDishCreationTest, CreateDishSuccessTest -} \ No newline at end of file diff --git a/tests/Browser/Dishes/DeleteDishTest.php b/tests/Browser/Dishes/DeleteDishTest.php deleted file mode 100644 index 51083f1..0000000 --- a/tests/Browser/Dishes/DeleteDishTest.php +++ /dev/null @@ -1,76 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser) - ->assertPathIs('/dishes') - ->assertSee('MANAGE DISHES'); - - // Verify that delete functionality is available by looking for the text in the page source - $pageSource = $browser->driver->getPageSource(); - $this->assertStringContainsString('Delete', $pageSource); - }); - } - - // TODO: Fix static planner issue causing login failures in suite runs - // These tests pass in isolation but fail when run in full suite - /* - public function testDeleteModalComponents(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser) - ->assertSee('MANAGE DISHES') - ->assertSee('Add Dish'); - }); - } - - public function testDeletionSafetyFeatures(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser); - - // Check that Livewire component includes all CRUD features - $pageSource = $browser->driver->getPageSource(); - $this->assertStringContainsString('MANAGE DISHES', $pageSource); - $this->assertStringContainsString('Add Dish', $pageSource); - // Either we have dishes with Delete button OR "No dishes found" message - if (str_contains($pageSource, 'No dishes found')) { - $this->assertStringContainsString('No dishes found', $pageSource); - } else { - $this->assertStringContainsString('Delete', $pageSource); - } - }); - } - */ -} \ No newline at end of file diff --git a/tests/Browser/Dishes/DishDeletionSafetyTest.php b/tests/Browser/Dishes/DishDeletionSafetyTest.php deleted file mode 100644 index ab5b1d2..0000000 --- a/tests/Browser/Dishes/DishDeletionSafetyTest.php +++ /dev/null @@ -1,49 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser); - - // Check that Livewire component includes all CRUD features - $pageSource = $browser->driver->getPageSource(); - $this->assertStringContainsString('MANAGE DISHES', $pageSource); - $this->assertStringContainsString('Add Dish', $pageSource); - // Either we have dishes with Delete button OR "No dishes found" message - if (str_contains($pageSource, 'No dishes found')) { - $this->assertStringContainsString('No dishes found', $pageSource); - } else { - $this->assertStringContainsString('Delete', $pageSource); - } - }); - } -} \ No newline at end of file diff --git a/tests/Browser/Dishes/EditDishTest.php b/tests/Browser/Dishes/EditDishTest.php deleted file mode 100644 index af2d2f8..0000000 --- a/tests/Browser/Dishes/EditDishTest.php +++ /dev/null @@ -1,73 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser) - ->assertPathIs('/dishes') - ->assertSee('MANAGE DISHES'); - - // Verify that edit functionality is available by looking for the text in the page source - $pageSource = $browser->driver->getPageSource(); - $this->assertStringContainsString('Edit', $pageSource); - }); - } - - public function testEditModalComponents(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser) - ->assertSee('MANAGE DISHES') - ->assertSee('Add Dish'); - }); - } - - public function testDishesPageStructure(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToDishes($browser) - ->assertSee('MANAGE DISHES') - ->assertSee('Add Dish'); - - // Check that the dishes CRUD structure is present - $pageSource = $browser->driver->getPageSource(); - // Either we have dishes with Edit/Delete buttons OR "No dishes found" message - if (str_contains($pageSource, 'No dishes found')) { - $this->assertStringContainsString('No dishes found', $pageSource); - } else { - $this->assertStringContainsString('Edit', $pageSource); - $this->assertStringContainsString('Delete', $pageSource); - } - }); - } -} \ No newline at end of file diff --git a/tests/Browser/LoginHelpers.php b/tests/Browser/LoginHelpers.php deleted file mode 100644 index 2d4aac2..0000000 --- a/tests/Browser/LoginHelpers.php +++ /dev/null @@ -1,62 +0,0 @@ -exists) { - // Generate unique email for this test run - self::$testEmail = fake()->unique()->safeEmail(); - - self::$testPlanner = \App\Models\Planner::factory()->create([ - 'email' => self::$testEmail, - 'password' => \Illuminate\Support\Facades\Hash::make(self::$testPassword), - ]); - } - } - - protected function loginAndNavigate(Browser $browser, string $page = '/dashboard'): Browser - { - $this->ensureTestPlannerExists(); - - // Clear browser session and cookies to start fresh - $browser->driver->manage()->deleteAllCookies(); - - return $browser->visit('http://dishplanner_app:8000/login') - ->waitFor('input[id="email"]', DuskTestCase::TIMEOUT_SHORT) - ->clear('input[id="email"]') - ->type('input[id="email"]', self::$testEmail) - ->clear('input[id="password"]') - ->type('input[id="password"]', self::$testPassword) - ->press('Sign In') - ->waitForLocation('/dashboard', DuskTestCase::TIMEOUT_MEDIUM) // Wait for successful login redirect - ->pause(DuskTestCase::PAUSE_SHORT) // Brief pause for any initialization - ->visit('http://dishplanner_app:8000' . $page) - ->pause(DuskTestCase::PAUSE_MEDIUM); // Let Livewire components initialize - } - - protected function loginAndGoToDishes(Browser $browser): Browser - { - return $this->loginAndNavigate($browser, '/dishes'); - } - - protected function loginAndGoToUsers(Browser $browser): Browser - { - return $this->loginAndNavigate($browser, '/users'); - } - - protected function loginAndGoToSchedule(Browser $browser): Browser - { - return $this->loginAndNavigate($browser, '/schedule'); - } -} diff --git a/tests/Browser/Pages/DishesPage.php b/tests/Browser/Pages/DishesPage.php deleted file mode 100644 index e8eeee5..0000000 --- a/tests/Browser/Pages/DishesPage.php +++ /dev/null @@ -1,86 +0,0 @@ -assertPathIs($this->url()) - ->assertSee('MANAGE DISHES'); - } - - /** - * Get the element shortcuts for the page. - * - * @return array - */ - public function elements(): array - { - return [ - '@add-button' => 'button[wire\\:click="create"]', - '@dishes-list' => '[wire\\:id]', // Livewire component - '@search' => 'input[type="search"]', - '@no-dishes' => '*[text*="No dishes found"]', - ]; - } - - /** - * Open the create dish modal. - */ - public function openCreateModal(Browser $browser): void - { - $browser->waitFor('@add-button') - ->click('@add-button') - ->pause(1000); - } - - /** - * Click edit button for a dish. - */ - public function clickEditForDish(Browser $browser, string $dishName): void - { - $browser->within("tr:contains('{$dishName}')", function ($row) { - $row->click('button.bg-accent-blue'); - }); - } - - /** - * Click delete button for a dish. - */ - public function clickDeleteForDish(Browser $browser, string $dishName): void - { - $browser->within("tr:contains('{$dishName}')", function ($row) { - $row->click('button.bg-red-500'); - }); - } - - /** - * Assert a dish is visible in the list. - */ - public function assertDishVisible(Browser $browser, string $dishName): void - { - $browser->assertSee($dishName); - } - - /** - * Assert no dishes message is shown. - */ - public function assertNoDishes(Browser $browser): void - { - $browser->assertSee('No dishes found'); - } -} \ No newline at end of file diff --git a/tests/Browser/Pages/LoginPage.php b/tests/Browser/Pages/LoginPage.php deleted file mode 100644 index 6732270..0000000 --- a/tests/Browser/Pages/LoginPage.php +++ /dev/null @@ -1,47 +0,0 @@ -assertPathIs($this->url()) - ->assertSee('Login') - ->assertPresent((new LoginForm)->selector()); - } - - /** - * Get the element shortcuts for the page. - * - * @return array - */ - public function elements(): array - { - return [ - '@register-link' => 'a[href*="register"]', - ]; - } - - /** - * Navigate to the registration page. - */ - public function goToRegistration(Browser $browser): void - { - $browser->click('@register-link'); - } -} \ No newline at end of file diff --git a/tests/Browser/Pages/Page.php b/tests/Browser/Pages/Page.php deleted file mode 100644 index ecef801..0000000 --- a/tests/Browser/Pages/Page.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - public static function siteElements(): array - { - return [ - '@nav' => 'nav', - '@alert' => '[role="alert"]', - ]; - } -} \ No newline at end of file diff --git a/tests/Browser/Pages/SchedulePage.php b/tests/Browser/Pages/SchedulePage.php deleted file mode 100644 index d2db379..0000000 --- a/tests/Browser/Pages/SchedulePage.php +++ /dev/null @@ -1,110 +0,0 @@ -assertPathIs($this->url()) - ->assertSee('SCHEDULE'); - } - - public function elements(): array - { - return [ - '@generate-button' => 'button[wire\\:click="generate"]', - '@clear-month-button' => 'button[wire\\:click="clearMonth"]', - '@previous-month' => 'button[wire\\:click="previousMonth"]', - '@next-month' => 'button[wire\\:click="nextMonth"]', - '@month-select' => 'select[wire\\:model="selectedMonth"]', - '@year-select' => 'select[wire\\:model="selectedYear"]', - '@clear-existing-checkbox' => 'input[wire\\:model="clearExisting"]', - '@calendar-grid' => '.grid.grid-cols-7', - ]; - } - - public function clickGenerate(Browser $browser): void - { - $browser->waitFor('@generate-button') - ->click('@generate-button') - ->pause(2000); // Wait for generation - } - - public function clickClearMonth(Browser $browser): void - { - $browser->waitFor('@clear-month-button') - ->click('@clear-month-button') - ->pause(1000); - } - - public function goToPreviousMonth(Browser $browser): void - { - $browser->waitFor('@previous-month') - ->click('@previous-month') - ->pause(500); - } - - public function goToNextMonth(Browser $browser): void - { - $browser->waitFor('@next-month') - ->click('@next-month') - ->pause(500); - } - - public function selectMonth(Browser $browser, int $month): void - { - $browser->waitFor('@month-select') - ->select('@month-select', $month) - ->pause(500); - } - - public function selectYear(Browser $browser, int $year): void - { - $browser->waitFor('@year-select') - ->select('@year-select', $year) - ->pause(500); - } - - public function toggleClearExisting(Browser $browser): void - { - $browser->waitFor('@clear-existing-checkbox') - ->click('@clear-existing-checkbox'); - } - - public function selectUser(Browser $browser, string $userName): void - { - $browser->check("input[type='checkbox'][value]", $userName); - } - - public function assertSuccessMessage(Browser $browser, string $message = null): void - { - if ($message) { - $browser->assertSee($message); - } else { - $browser->assertPresent('.border-success'); - } - } - - public function assertDishScheduled(Browser $browser, string $dishName): void - { - $browser->assertSee($dishName); - } - - public function assertNoDishesScheduled(Browser $browser): void - { - $browser->assertSee('No dishes scheduled'); - } - - public function assertMonthDisplayed(Browser $browser, string $monthYear): void - { - $browser->assertSee($monthYear); - } -} diff --git a/tests/Browser/Pages/UsersPage.php b/tests/Browser/Pages/UsersPage.php deleted file mode 100644 index 71aed0a..0000000 --- a/tests/Browser/Pages/UsersPage.php +++ /dev/null @@ -1,93 +0,0 @@ -assertPathIs($this->url()) - ->assertSee('MANAGE USERS'); - } - - /** - * Get the element shortcuts for the page. - * - * @return array - */ - public function elements(): array - { - return [ - '@add-button' => 'button[wire\\:click="create"]', - '@users-list' => '[wire\\:id]', // Livewire component - '@no-users' => '*[text*="No users found"]', - ]; - } - - /** - * Open the create user modal. - */ - public function openCreateModal(Browser $browser): void - { - $browser->waitFor('@add-button') - ->click('@add-button') - ->pause(1000); - } - - /** - * Click delete button for a user. - */ - public function clickDeleteForUser(Browser $browser, string $userName): void - { - $browser->within("tr:contains('{$userName}')", function ($row) { - $row->click('button.bg-danger'); - }); - } - - /** - * Click the first available delete button. - */ - public function clickFirstDeleteButton(Browser $browser): void - { - $browser->waitFor('button.bg-danger', 5) - ->click('button.bg-danger') - ->pause(1000); - } - - /** - * Assert a user is visible in the list. - */ - public function assertUserVisible(Browser $browser, string $userName): void - { - $browser->assertSee($userName); - } - - /** - * Assert a user is not visible in the list. - */ - public function assertUserNotVisible(Browser $browser, string $userName): void - { - $browser->assertDontSee($userName); - } - - /** - * Assert success message is shown. - */ - public function assertSuccessMessage(Browser $browser, string $message): void - { - $browser->assertSee($message); - } -} \ No newline at end of file diff --git a/tests/Browser/RedirectTest.php b/tests/Browser/RedirectTest.php deleted file mode 100644 index 3c5954e..0000000 --- a/tests/Browser/RedirectTest.php +++ /dev/null @@ -1,38 +0,0 @@ -browse(function (Browser $browser) { - $browser->visit('http://dishplanner_app:8000/dashboard') - ->assertPathIs('/login') - ->assertSee('Login'); - }); - } - - /** - * Test that login page loads correctly - */ - public function testLoginPageLoads() - { - $this->browse(function (Browser $browser) { - $browser->visit('http://dishplanner_app:8000/login') - ->assertPathIs('/login') - ->assertSee('Login') - ->assertSee('Email') - ->assertSee('Password'); - }); - } -} \ No newline at end of file diff --git a/tests/Browser/Schedule/GenerateScheduleTest.php b/tests/Browser/Schedule/GenerateScheduleTest.php deleted file mode 100644 index f3e945c..0000000 --- a/tests/Browser/Schedule/GenerateScheduleTest.php +++ /dev/null @@ -1,124 +0,0 @@ -unique()->safeEmail(); - self::$planner = Planner::factory()->create([ - 'email' => self::$email, - 'password' => Hash::make(self::$password), - ]); - - // Create a user for this planner - self::$user = User::factory()->create([ - 'planner_id' => self::$planner->id, - 'name' => 'Test User', - ]); - - // Create a dish and assign to user - self::$dish = Dish::factory()->create([ - 'planner_id' => self::$planner->id, - 'name' => 'Test Dish', - ]); - - // Attach user to dish (creates UserDish) - self::$dish->users()->attach(self::$user); - } - } - - protected function loginAsPlanner(Browser $browser): Browser - { - $browser->driver->manage()->deleteAllCookies(); - - return $browser->visit('http://dishplanner_app:8000/login') - ->waitFor('input[id="email"]', DuskTestCase::TIMEOUT_SHORT) - ->clear('input[id="email"]') - ->type('input[id="email"]', self::$email) - ->clear('input[id="password"]') - ->type('input[id="password"]', self::$password) - ->press('Login') - ->waitForLocation('/dashboard', DuskTestCase::TIMEOUT_MEDIUM) - ->pause(DuskTestCase::PAUSE_SHORT) - ->visit('http://dishplanner_app:8000/schedule') - ->pause(DuskTestCase::PAUSE_MEDIUM); - } - - public function testCanGenerateScheduleWithUserAndDish(): void - { - $this->browse(function (Browser $browser) { - $this->loginAsPlanner($browser); - - $browser->on(new SchedulePage) - ->assertSee('Test User') // User should be in selection - ->clickGenerate() - ->pause(2000) - // Verify schedule was generated by checking dish appears on calendar - ->assertSee('Test Dish'); - }); - } - - public function testGeneratedScheduleShowsDishOnCalendar(): void - { - $this->browse(function (Browser $browser) { - $this->loginAsPlanner($browser); - - $browser->on(new SchedulePage) - ->clickGenerate() - ->pause(2000) - // The dish should appear somewhere on the calendar - ->assertSee('Test Dish'); - }); - } - - public function testCanClearMonthSchedule(): void - { - $this->browse(function (Browser $browser) { - $this->loginAsPlanner($browser); - - $browser->on(new SchedulePage) - // First generate a schedule - ->clickGenerate() - ->pause(2000) - ->assertSee('Test Dish') // Verify generated - // Then clear it - ->clickClearMonth() - ->pause(1000) - // After clearing, should see "No dishes scheduled" on calendar days - ->assertSee('No dishes scheduled'); - }); - } - - public function testUserSelectionAffectsGeneration(): void - { - $this->browse(function (Browser $browser) { - $this->loginAsPlanner($browser); - - $browser->on(new SchedulePage) - // Verify the user checkbox is present - ->assertSee('Test User') - // User should be selected by default - ->assertChecked("input[value='" . self::$user->id . "']"); - }); - } -} diff --git a/tests/Browser/Schedule/SchedulePageTest.php b/tests/Browser/Schedule/SchedulePageTest.php deleted file mode 100644 index a0a2ace..0000000 --- a/tests/Browser/Schedule/SchedulePageTest.php +++ /dev/null @@ -1,107 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $browser->on(new SchedulePage) - ->assertSee('SCHEDULE') - ->assertSee('Generate Schedule'); - }); - } - - public function testSchedulePageHasMonthNavigation(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $browser->on(new SchedulePage) - ->assertPresent('@previous-month') - ->assertPresent('@next-month') - ->assertSee(now()->format('F Y')); - }); - } - - public function testCanNavigateToNextMonth(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $nextMonth = now()->addMonth(); - - $browser->on(new SchedulePage) - ->goToNextMonth() - ->assertSee($nextMonth->format('F Y')); - }); - } - - public function testCanNavigateToPreviousMonth(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $prevMonth = now()->subMonth(); - - $browser->on(new SchedulePage) - ->goToPreviousMonth() - ->assertSee($prevMonth->format('F Y')); - }); - } - - public function testScheduleGeneratorShowsUserSelection(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $browser->on(new SchedulePage) - ->assertSee('Select Users') - ->assertPresent('@generate-button') - ->assertPresent('@clear-month-button'); - }); - } - - public function testCalendarDisplaysDaysOfWeek(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToSchedule($browser); - - $browser->on(new SchedulePage) - ->assertSee('Mon') - ->assertSee('Tue') - ->assertSee('Wed') - ->assertSee('Thu') - ->assertSee('Fri') - ->assertSee('Sat') - ->assertSee('Sun'); - }); - } -} diff --git a/tests/Browser/Users/CreateUserTest.php b/tests/Browser/Users/CreateUserTest.php deleted file mode 100644 index 56ebbb3..0000000 --- a/tests/Browser/Users/CreateUserTest.php +++ /dev/null @@ -1,103 +0,0 @@ -browse(function (Browser $browser) { - $this->loginAndGoToUsers($browser); - - $browser->on(new UsersPage) - ->assertSee('MANAGE USERS') - ->assertSee('Add User'); - }); - } - - public function testCanOpenCreateUserModal(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToUsers($browser); - - $browser->on(new UsersPage) - ->openCreateModal() - ->assertSee('Add New User') - ->assertSee('Name') - ->assertSee('Cancel') - ->assertSee('Create User'); - }); - } - - public function testCreateUserFormValidation(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToUsers($browser); - - $browser->on(new UsersPage) - ->openCreateModal() - ->press('Create User') - ->pause(self::PAUSE_MEDIUM) - ->assertSee('The name field is required'); - }); - } - - public function testCanCreateUser(): void - { - $this->browse(function (Browser $browser) { - $userName = 'TestCreate_' . uniqid(); - - $this->loginAndGoToUsers($browser); - - $browser->on(new UsersPage) - ->openCreateModal() - ->type('input[wire\\:model="name"]', $userName) - ->press('Create User') - ->pause(self::PAUSE_MEDIUM) - ->assertSee('User created successfully') - ->assertSee($userName); - }); - } - - public function testCanCancelUserCreation(): void - { - $this->browse(function (Browser $browser) { - $this->loginAndGoToUsers($browser); - - $browser->on(new UsersPage) - ->openCreateModal() - ->type('input[wire\\:model="name"]', 'Test Cancel User') - ->press('Cancel') - ->pause(self::PAUSE_SHORT) - // Modal should be closed, we should be back on users page - ->assertSee('MANAGE USERS') - ->assertDontSee('Add New User'); - }); - } -} \ No newline at end of file diff --git a/tests/Browser/console/.gitignore b/tests/Browser/console/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/tests/Browser/console/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/tests/Browser/screenshots/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/Browser/source/.gitignore b/tests/Browser/source/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/tests/Browser/source/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php deleted file mode 100644 index ec8f947..0000000 --- a/tests/DuskTestCase.php +++ /dev/null @@ -1,53 +0,0 @@ -addArguments([ - '--window-size=1920,1080', - '--no-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu', - '--headless=new', - '--disable-extensions', - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-renderer-backgrounding', - ]); - - return RemoteWebDriver::create( - 'http://selenium:4444/wd/hub', // Connect to Selenium container - DesiredCapabilities::chrome()->setCapability( - ChromeOptions::CAPABILITY, $options - ) - ); - } -} diff --git a/tests/Feature/AuthenticationTest.php b/tests/Feature/AuthenticationTest.php deleted file mode 100644 index e6486f4..0000000 --- a/tests/Feature/AuthenticationTest.php +++ /dev/null @@ -1,97 +0,0 @@ -get('/login'); - - $response->assertStatus(200); - $response->assertViewIs('auth.login'); - $response->assertSee('Login'); - } - - public function test_users_can_authenticate_using_the_login_screen(): void - { - Planner::factory()->create([ - 'email' => 'test@example.com', - 'password' => bcrypt('password'), - ]); - - $response = $this->post('/login', [ - 'email' => 'test@example.com', - 'password' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect('/dashboard'); - } - - public function test_users_can_not_authenticate_with_invalid_password(): void - { - Planner::factory()->create([ - 'email' => 'test@example.com', - 'password' => bcrypt('password'), - ]); - - $this->post('/login', [ - 'email' => 'test@example.com', - 'password' => 'wrong-password', - ]); - - $this->assertGuest(); - } - - public function test_session_is_created_on_login_page(): void - { - $response = $this->get('/login'); - - // Check if session was started - $this->assertNotNull(session()->getId()); - - // Check if CSRF token is generated - $this->assertNotNull(csrf_token()); - - // Check session driver - $sessionDriver = config('session.driver'); - $this->assertNotEquals('array', $sessionDriver, 'Session driver should not be array for authentication'); - - $response->assertStatus(200); - $response->assertSessionHasNoErrors(); - } - - public function test_csrf_token_is_validated_on_login(): void - { - // Try to post without CSRF token by disabling middleware that auto-adds it - $response = $this - ->withoutMiddleware(VerifyCsrfToken::class) - ->withHeaders([ - 'Accept' => 'text/html', - ]) - ->post('/login', [ - 'email' => 'test@example.com', - 'password' => 'password', - ]); - - $response->assertStatus(302); - } - - public function test_users_can_logout(): void - { - $user = Planner::factory()->create(); - - $response = $this->actingAs($user)->post('/logout'); - - $response->assertRedirect('/'); - $this->assertGuest(); - } -} diff --git a/tests/Feature/RegistrationTest.php b/tests/Feature/RegistrationTest.php deleted file mode 100644 index b68a311..0000000 --- a/tests/Feature/RegistrationTest.php +++ /dev/null @@ -1,77 +0,0 @@ -get('/register'); - - $response->assertStatus(200); - $response->assertViewIs('auth.register'); - $response->assertSee('Register'); - } - - public function test_new_users_can_register() - { - $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect('/dashboard'); - - // Check user was created - $this->assertDatabaseHas('planners', [ - 'email' => 'test@example.com', - 'name' => 'Test User', - ]); - } - - public function test_registration_fails_with_existing_email() - { - $existingUser = Planner::factory()->create([ - 'email' => 'existing@example.com', - ]); - - $response = $this->post('/register', [ - 'name' => 'Another User', - 'email' => 'existing@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response->assertRedirect(); - $response->assertSessionHasErrors('email'); - $this->assertGuest(); - } - - public function test_registration_fails_with_password_mismatch() - { - $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password', - 'password_confirmation' => 'different', - ]); - - $response->assertRedirect(); - $response->assertSessionHasErrors('password'); - $this->assertGuest(); - - // Check user was not created - $this->assertDatabaseMissing('planners', [ - 'email' => 'test@example.com', - ]); - } -} \ No newline at end of file diff --git a/tests/Feature/Schedule/ScheduleEdgeCasesTest.php b/tests/Feature/Schedule/ScheduleEdgeCasesTest.php deleted file mode 100644 index 726d0b1..0000000 --- a/tests/Feature/Schedule/ScheduleEdgeCasesTest.php +++ /dev/null @@ -1,254 +0,0 @@ -setUpHasPlanner(); - } - - public function test_generate_schedule_creates_schedule_records(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $this->assertDatabaseEmpty(Schedule::class); - - $response = $this - ->actingAs($planner) - ->post(route('api.schedule.generate'), [ - 'overwrite' => false, - ]); - - $response->assertStatus(200); - - // Should create 14 schedule records (2 weeks) - $this->assertDatabaseCount(Schedule::class, 14); - } - - public function test_overwrite_false_preserves_existing_schedules(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish1 = Dish::factory()->planner($planner)->create(['name' => 'Dish 1']); - $dish2 = Dish::factory()->planner($planner)->create(['name' => 'Dish 2']); - $dish1->users()->attach($user); - $dish2->users()->attach($user); - - // Create a pre-existing schedule for today - $schedule = Schedule::factory()->create([ - 'planner_id' => $planner->id, - 'date' => now()->format('Y-m-d'), - ]); - - $userDish1 = $user->userDishes()->where('dish_id', $dish1->id)->first(); - - $existingScheduledDish = ScheduledUserDish::factory()->create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish1->id, - ]); - - // Generate with overwrite = false - $response = $this - ->actingAs($planner) - ->post(route('api.schedule.generate'), [ - 'overwrite' => false, - ]); - - $response->assertStatus(200); - - // The existing scheduled dish should remain unchanged - $this->assertDatabaseHas(ScheduledUserDish::class, [ - 'id' => $existingScheduledDish->id, - 'user_dish_id' => $userDish1->id, - ]); - } - - public function test_schedule_isolation_between_planners(): void - { - $planner1 = $this->planner; - $planner2 = Planner::factory()->create(); - - $user1 = User::factory()->planner($planner1)->create(); - $user2 = User::factory()->planner($planner2)->create(); - - $dish1 = Dish::factory()->planner($planner1)->create(); - $dish2 = Dish::factory()->planner($planner2)->create(); - - $dish1->users()->attach($user1); - $dish2->users()->attach($user2); - - // Generate schedule for planner1 - $this - ->actingAs($planner1) - ->post(route('api.schedule.generate'), ['overwrite' => false]) - ->assertStatus(200); - - // Generate schedule for planner2 - $this - ->actingAs($planner2) - ->post(route('api.schedule.generate'), ['overwrite' => false]) - ->assertStatus(200); - - // Verify each planner only has their own schedules - $planner1Schedules = Schedule::withoutGlobalScopes() - ->where('planner_id', $planner1->id) - ->count(); - $planner2Schedules = Schedule::withoutGlobalScopes() - ->where('planner_id', $planner2->id) - ->count(); - - $this->assertEquals(14, $planner1Schedules); - $this->assertEquals(14, $planner2Schedules); - } - - public function test_skip_schedule_day_nullifies_user_dish(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $userDish = $user->userDishes()->first(); - $date = now()->format('Y-m-d'); - - // Create a schedule with a dish - $schedule = Schedule::factory()->create([ - 'planner_id' => $planner->id, - 'date' => $date, - ]); - - $scheduledUserDish = ScheduledUserDish::factory()->create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - - // Mark the day as skipped - $response = $this - ->actingAs($planner) - ->put(route('api.schedule.update', ['date' => $date]), [ - 'is_skipped' => true, - ]); - - $response->assertStatus(200); - - // Verify schedule is marked as skipped - $this->assertDatabaseHas(Schedule::class, [ - 'id' => $schedule->id, - 'is_skipped' => true, - ]); - - // Verify scheduled user dish has null user_dish_id - $scheduledUserDish->refresh(); - $this->assertNull($scheduledUserDish->user_dish_id); - } - - public function test_delete_scheduled_user_dish_removes_record(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $userDish = $user->userDishes()->first(); - - $schedule = Schedule::factory()->create([ - 'planner_id' => $planner->id, - 'date' => now()->format('Y-m-d'), - ]); - - $scheduledUserDish = ScheduledUserDish::factory()->create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - ]); - - $response = $this - ->actingAs($planner) - ->delete(route('api.scheduled-user-dishes.destroy', ['scheduledUserDish' => $scheduledUserDish->id])); - - $response->assertStatus(200); - - $this->assertDatabaseMissing(ScheduledUserDish::class, [ - 'id' => $scheduledUserDish->id, - ]); - } - - public function test_planner_cannot_delete_other_planners_scheduled_dish(): void - { - $planner1 = $this->planner; - $planner2 = Planner::factory()->create(); - - $user = User::factory()->planner($planner2)->create(); - $dish = Dish::factory()->planner($planner2)->create(); - $dish->users()->attach($user); - - $userDish = $user->userDishes()->first(); - - $schedule = Schedule::factory()->create([ - 'planner_id' => $planner2->id, - 'date' => now()->format('Y-m-d'), - ]); - - $scheduledUserDish = ScheduledUserDish::factory()->create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - ]); - - // Try to delete as planner1 (should fail) - $response = $this - ->actingAs($planner1) - ->delete(route('api.scheduled-user-dishes.destroy', ['scheduledUserDish' => $scheduledUserDish->id])); - - $response->assertStatus(403); - - // Record should still exist - $this->assertDatabaseHas(ScheduledUserDish::class, [ - 'id' => $scheduledUserDish->id, - ]); - } - - public function test_schedule_show_creates_schedule_if_not_exists(): void - { - $planner = $this->planner; - $futureDate = now()->addDays(30)->format('Y-m-d'); - - $this->assertDatabaseMissing(Schedule::class, [ - 'planner_id' => $planner->id, - 'date' => $futureDate, - ]); - - $response = $this - ->actingAs($planner) - ->get(route('api.schedule.show', ['date' => $futureDate])); - - $response->assertStatus(200); - - // Schedule should now exist - $this->assertDatabaseHas(Schedule::class, [ - 'planner_id' => $planner->id, - 'date' => $futureDate, - ]); - } -} diff --git a/tests/Traits/HasPlanner.php b/tests/Traits/HasPlanner.php deleted file mode 100644 index f687a09..0000000 --- a/tests/Traits/HasPlanner.php +++ /dev/null @@ -1,20 +0,0 @@ -planner = Planner::factory()->create(); - } - - public function createPlanner(): Planner - { - return Planner::factory()->create(); - } -} diff --git a/tests/Unit/Actions/EditUserActionTest.php b/tests/Unit/Actions/EditUserActionTest.php deleted file mode 100644 index 464cca9..0000000 --- a/tests/Unit/Actions/EditUserActionTest.php +++ /dev/null @@ -1,162 +0,0 @@ -create(); - $this->planner = $planner; - - $this->action = new EditUserAction(); - } - - public function test_successfully_updates_user_name(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - $newName = 'Updated Name'; - - $result = $this->action->execute($user, ['name' => $newName]); - - $this->assertTrue($result); - $this->assertEquals($newName, $user->fresh()->name); - } - - public function test_logs_successful_update(): void - { - Log::shouldReceive('info') - ->with('EditUserAction: Starting user update', \Mockery::type('array')) - ->once(); - - Log::shouldReceive('info') - ->with('EditUserAction: Update result', \Mockery::type('array')) - ->once(); - - Log::shouldReceive('info') - ->with('EditUserAction: User successfully updated', \Mockery::type('array')) - ->once(); - - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $this->action->execute($user, ['name' => 'Updated Name']); - } - - public function test_throws_exception_when_update_fails(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $mockUser = \Mockery::mock($user); - $mockUser->shouldReceive('update')->andReturn(false); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - - Log::shouldReceive('info')->twice(); - Log::shouldReceive('error') - ->with('EditUserAction: User update failed', \Mockery::type('array')) - ->once(); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User update returned false'); - - $this->action->execute($mockUser, ['name' => 'Updated Name']); - } - - public function test_throws_exception_when_update_does_not_persist(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $mockUser = \Mockery::mock($user); - $mockUser->shouldReceive('update')->andReturn(true); - $mockUser->shouldReceive('refresh')->andReturnSelf(); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn('Original Name'); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - - Log::shouldReceive('info')->twice(); - Log::shouldReceive('error') - ->with('EditUserAction: User update failed', \Mockery::type('array')) - ->once(); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User update did not persist to database'); - - $this->action->execute($mockUser, ['name' => 'Updated Name']); - } - - public function test_rolls_back_transaction_on_failure(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - DB::shouldReceive('beginTransaction')->once(); - DB::shouldReceive('rollBack')->once(); - DB::shouldReceive('commit')->never(); - - $mockUser = \Mockery::mock($user); - $mockUser->shouldReceive('update')->andThrow(new \Exception('Database error')); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - - Log::shouldReceive('info')->once(); - Log::shouldReceive('error') - ->with('EditUserAction: User update failed', \Mockery::type('array')) - ->once(); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Database error'); - - $this->action->execute($mockUser, ['name' => 'Updated Name']); - } - - public function test_commits_transaction_on_success(): void - { - DB::shouldReceive('beginTransaction')->once(); - DB::shouldReceive('commit')->once(); - DB::shouldReceive('rollBack')->never(); - - Log::shouldReceive('info')->times(3); - - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $result = $this->action->execute($user, ['name' => 'Updated Name']); - - $this->assertTrue($result); - } - - public function test_validates_name_is_provided(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $this->expectException(\Exception::class); - - $this->action->execute($user, []); - } - - public function test_handles_empty_name(): void - { - $user = User::factory()->planner($this->planner)->create(['name' => 'Original Name']); - - $result = $this->action->execute($user, ['name' => '']); - - $this->assertTrue($result); - $this->assertEquals('', $user->fresh()->name); - } -} diff --git a/tests/Unit/Actions/User/CreateUserActionTest.php b/tests/Unit/Actions/User/CreateUserActionTest.php deleted file mode 100644 index f899a58..0000000 --- a/tests/Unit/Actions/User/CreateUserActionTest.php +++ /dev/null @@ -1,238 +0,0 @@ -action = new CreateUserAction(); - - // Create a planner for testing - $this->planner = Planner::factory()->create(); - } - - public function test_it_can_create_a_user_successfully(): void - { - // Arrange - $userData = [ - 'name' => 'Test User', - 'planner_id' => $this->planner->id, - ]; - - // Act - $user = $this->action->execute($userData); - - // Assert - $this->assertInstanceOf(User::class, $user); - $this->assertEquals('Test User', $user->name); - $this->assertEquals($this->planner->id, $user->planner_id); - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'name' => 'Test User', - 'planner_id' => $this->planner->id, - ]); - } - - public function test_it_throws_exception_when_name_is_empty(): void - { - // Arrange - $userData = [ - 'name' => '', - 'planner_id' => $this->planner->id, - ]; - - // Act & Assert - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Name is required'); - - $this->action->execute($userData); - - // Verify no user was created - $this->assertDatabaseMissing('users', [ - 'planner_id' => $this->planner->id, - ]); - } - - public function test_it_throws_exception_when_name_is_missing(): void - { - // Arrange - $userData = [ - 'planner_id' => $this->planner->id, - ]; - - // Act & Assert - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Name is required'); - - $this->action->execute($userData); - } - - public function test_it_throws_exception_when_planner_id_is_empty(): void - { - // Arrange - $userData = [ - 'name' => 'Test User', - 'planner_id' => '', - ]; - - // Act & Assert - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Planner ID is required'); - - $this->action->execute($userData); - } - - public function test_it_throws_exception_when_planner_id_is_missing(): void - { - // Arrange - $userData = [ - 'name' => 'Test User', - ]; - - // Act & Assert - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Planner ID is required'); - - $this->action->execute($userData); - } - - public function test_it_logs_creation_process(): void - { - // Arrange - Log::spy(); - $userData = [ - 'name' => 'Test User', - 'planner_id' => $this->planner->id, - ]; - - // Act - $user = $this->action->execute($userData); - - // Assert - Log::shouldHaveReceived('info') - ->with('CreateUserAction: Starting user creation', [ - 'name' => 'Test User', - 'planner_id' => $this->planner->id, - ]); - - Log::shouldHaveReceived('info') - ->with('CreateUserAction: User successfully created', [ - 'user_id' => $user->id, - 'name' => 'Test User', - 'planner_id' => $this->planner->id, - ]); - } - - public function test_it_handles_database_transaction_rollback_on_failure(): void - { - // Arrange - $userData = [ - 'name' => 'Test User', - 'planner_id' => 999999, // Non-existent planner ID should cause foreign key constraint error - ]; - - // Act & Assert - $this->expectException(\Exception::class); - - try { - $this->action->execute($userData); - } catch (\Exception $e) { - // Verify no user was created (transaction rolled back) - $this->assertDatabaseMissing('users', [ - 'name' => 'Test User', - ]); - throw $e; - } - } - - public function test_it_logs_errors_on_failure(): void - { - // Arrange - Log::spy(); - $userData = [ - 'name' => 'Test User', - 'planner_id' => 999999, // Non-existent planner ID - ]; - - // Act & Assert - try { - $this->action->execute($userData); - } catch (\Exception $e) { - // Expected - } - - // Assert - Log::shouldHaveReceived('error') - ->with('CreateUserAction: User creation failed', \Mockery::on(function ($data) { - return $data['name'] === 'Test User' && - $data['planner_id'] === 999999 && - isset($data['error']) && - isset($data['trace']); - })); - } - - public function test_it_creates_user_with_whitespace_trimmed_name(): void - { - // Arrange - $userData = [ - 'name' => ' Test User ', - 'planner_id' => $this->planner->id, - ]; - - // Act - $user = $this->action->execute($userData); - - // Assert - $this->assertEquals(' Test User ', $user->name); // Should preserve original data as passed - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'name' => ' Test User ', - 'planner_id' => $this->planner->id, - ]); - } - - public function test_it_can_create_multiple_users_with_same_planner(): void - { - // Arrange - $userData1 = [ - 'name' => 'User One', - 'planner_id' => $this->planner->id, - ]; - - $userData2 = [ - 'name' => 'User Two', - 'planner_id' => $this->planner->id, - ]; - - // Act - $user1 = $this->action->execute($userData1); - $user2 = $this->action->execute($userData2); - - // Assert - $this->assertNotEquals($user1->id, $user2->id); - $this->assertEquals($this->planner->id, $user1->planner_id); - $this->assertEquals($this->planner->id, $user2->planner_id); - $this->assertDatabaseHas('users', ['name' => 'User One']); - $this->assertDatabaseHas('users', ['name' => 'User Two']); - } - - protected function tearDown(): void - { - \Mockery::close(); - parent::tearDown(); - } -} \ No newline at end of file diff --git a/tests/Unit/Actions/User/DeleteUserActionTest.php b/tests/Unit/Actions/User/DeleteUserActionTest.php deleted file mode 100644 index 1363176..0000000 --- a/tests/Unit/Actions/User/DeleteUserActionTest.php +++ /dev/null @@ -1,195 +0,0 @@ -action = new DeleteUserAction(); - - // Create a planner for testing - $this->planner = Planner::factory()->create(); - } - - public function test_it_can_delete_a_user_successfully(): void - { - // Arrange - $user = User::factory()->create([ - 'planner_id' => $this->planner->id, - 'name' => 'Test User' - ]); - $userId = $user->id; - - // Act - $result = $this->action->execute($user); - - // Assert - $this->assertTrue($result); - $this->assertDatabaseMissing('users', ['id' => $userId]); - } - - public function test_it_can_delete_a_user_with_associated_dishes(): void - { - // Arrange - $user = User::factory()->create(['planner_id' => $this->planner->id]); - $dish = Dish::factory()->create(['planner_id' => $this->planner->id]); - - // Associate user with dish - UserDish::create([ - 'user_id' => $user->id, - 'dish_id' => $dish->id - ]); - - $userId = $user->id; - - // Verify the association exists - $this->assertDatabaseHas('user_dishes', [ - 'user_id' => $userId, - 'dish_id' => $dish->id - ]); - - // Act - $result = $this->action->execute($user); - - // Assert - $this->assertTrue($result); - $this->assertDatabaseMissing('users', ['id' => $userId]); - // Verify cascade deletion removed the association - $this->assertDatabaseMissing('user_dishes', ['user_id' => $userId]); - // Verify the dish itself still exists - $this->assertDatabaseHas('dishes', ['id' => $dish->id]); - } - - public function test_it_logs_deletion_process(): void - { - // Arrange - Log::spy(); - $user = User::factory()->create(['planner_id' => $this->planner->id]); - $userId = $user->id; - $userName = $user->name; - - // Act - $this->action->execute($user); - - // Assert - Log::shouldHaveReceived('info') - ->with('DeleteUserAction: Starting user deletion', [ - 'user_id' => $userId, - 'user_name' => $userName, - 'planner_id' => $this->planner->id, - ]); - - Log::shouldHaveReceived('info') - ->with('DeleteUserAction: User successfully deleted', [ - 'user_id' => $userId, - 'user_name' => $userName, - ]); - } - - public function test_it_handles_database_transaction_rollback_on_failure(): void - { - // Arrange - $user = User::factory()->create(['planner_id' => $this->planner->id]); - - // Mock the user to throw an exception during deletion - $mockUser = \Mockery::mock(User::class); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - $mockUser->shouldReceive('userDishes')->andReturn($user->userDishes()); - $mockUser->shouldReceive('dishes')->andReturn($user->dishes()); - $mockUser->shouldReceive('delete')->andThrow(new \Exception('Database error')); - - // Act & Assert - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Database error'); - - $this->action->execute($mockUser); - - // Verify original user still exists (transaction rolled back) - $this->assertDatabaseHas('users', ['id' => $user->id]); - } - - public function test_it_throws_exception_when_deletion_returns_false(): void - { - // Arrange - $user = User::factory()->create(['planner_id' => $this->planner->id]); - - // Mock the user to return false on delete - $mockUser = \Mockery::mock(User::class); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - $mockUser->shouldReceive('userDishes')->andReturn($user->userDishes()); - $mockUser->shouldReceive('dishes')->andReturn($user->dishes()); - $mockUser->shouldReceive('delete')->andReturn(false); - - // Act & Assert - $this->expectException(\Exception::class); - $this->expectExceptionMessage('User deletion returned false'); - - $this->action->execute($mockUser); - } - - public function test_it_throws_exception_when_deletion_does_not_persist(): void - { - // This test is tricky to implement realistically since we can't easily - // mock the User::find() call in a way that makes sense. - // We'll skip this edge case for now, but in a real scenario you might - // want to test database connection issues, etc. - $this->markTestSkipped('Edge case test - difficult to implement without complex mocking'); - } - - public function test_it_logs_errors_on_failure(): void - { - // Arrange - Log::spy(); - $user = User::factory()->create(['planner_id' => $this->planner->id]); - - // Mock the user to throw an exception during deletion - $mockUser = \Mockery::mock(User::class); - $mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id); - $mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name); - $mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id); - $mockUser->shouldReceive('userDishes')->andReturn($user->userDishes()); - $mockUser->shouldReceive('dishes')->andReturn($user->dishes()); - $mockUser->shouldReceive('delete')->andThrow(new \Exception('Test error')); - - // Act & Assert - try { - $this->action->execute($mockUser); - } catch (\Exception $e) { - // Expected - } - - // Assert - Log::shouldHaveReceived('error') - ->with('DeleteUserAction: User deletion failed', \Mockery::on(function ($data) use ($user) { - return $data['user_id'] === $user->id && - $data['error'] === 'Test error' && - isset($data['trace']); - })); - } - - protected function tearDown(): void - { - \Mockery::close(); - parent::tearDown(); - } -} \ No newline at end of file diff --git a/tests/Unit/Actions/UserActionIntegrationTest.php b/tests/Unit/Actions/UserActionIntegrationTest.php deleted file mode 100644 index d5daa2d..0000000 --- a/tests/Unit/Actions/UserActionIntegrationTest.php +++ /dev/null @@ -1,87 +0,0 @@ -planner = Planner::factory()->create(); - $this->createAction = new CreateUserAction(); - $this->deleteAction = new DeleteUserAction(); - } - - public function test_complete_user_lifecycle_with_actions(): void - { - // Test creation - $userData = [ - 'name' => 'Integration Test User', - 'planner_id' => $this->planner->id, - ]; - - $user = $this->createAction->execute($userData); - - $this->assertInstanceOf(User::class, $user); - $this->assertEquals('Integration Test User', $user->name); - $this->assertEquals($this->planner->id, $user->planner_id); - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'name' => 'Integration Test User', - 'planner_id' => $this->planner->id, - ]); - - // Test deletion - $userId = $user->id; - $result = $this->deleteAction->execute($user); - - $this->assertTrue($result); - $this->assertDatabaseMissing('users', ['id' => $userId]); - } - - public function test_creating_and_deleting_user_with_relationships(): void - { - // Create user - $user = $this->createAction->execute([ - 'name' => 'User With Relationships', - 'planner_id' => $this->planner->id, - ]); - - // Create a dish and associate it with the user - $dish = \App\Models\Dish::factory()->create(['planner_id' => $this->planner->id]); - $user->dishes()->attach($dish->id); - - // Verify the relationship exists - $this->assertEquals(1, $user->dishes()->count()); - $this->assertDatabaseHas('user_dishes', [ - 'user_id' => $user->id, - 'dish_id' => $dish->id, - ]); - - // Delete the user - $userId = $user->id; - $result = $this->deleteAction->execute($user); - - // Verify deletion and cascade - $this->assertTrue($result); - $this->assertDatabaseMissing('users', ['id' => $userId]); - $this->assertDatabaseMissing('user_dishes', ['user_id' => $userId]); - - // Dish should still exist - $this->assertDatabaseHas('dishes', ['id' => $dish->id]); - } -} \ No newline at end of file diff --git a/tests/Unit/Schedule/Actions/ClearScheduleForMonthActionTest.php b/tests/Unit/Schedule/Actions/ClearScheduleForMonthActionTest.php deleted file mode 100644 index f3faa44..0000000 --- a/tests/Unit/Schedule/Actions/ClearScheduleForMonthActionTest.php +++ /dev/null @@ -1,92 +0,0 @@ -setUpHasPlanner(); - $this->action = new ClearScheduleForMonthAction(); - } - - public function test_clears_scheduled_user_dishes_for_month(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $month = 1; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - (new GenerateScheduleForMonthAction())->execute($planner, $month, $year, [$user->id]); - - $this->assertEquals($daysInMonth, ScheduledUserDish::where('user_id', $user->id)->count()); - - $this->action->execute($planner, $month, $year, [$user->id]); - - $this->assertEquals(0, ScheduledUserDish::where('user_id', $user->id)->count()); - $this->assertDatabaseCount(Schedule::class, $daysInMonth); - } - - public function test_only_clears_specified_users(): void - { - $planner = $this->planner; - $user1 = User::factory()->planner($planner)->create(); - $user2 = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach([$user1->id, $user2->id]); - - $month = 2; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - (new GenerateScheduleForMonthAction())->execute($planner, $month, $year, [$user1->id, $user2->id]); - $this->assertEquals($daysInMonth * 2, ScheduledUserDish::whereIn('user_id', [$user1->id, $user2->id])->count()); - - $this->action->execute($planner, $month, $year, [$user1->id]); - - $this->assertEquals($daysInMonth, ScheduledUserDish::whereIn('user_id', [$user1->id, $user2->id])->count()); - $this->assertEquals(0, ScheduledUserDish::where('user_id', $user1->id)->count()); - } - - public function test_does_not_affect_other_months(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $year = 2026; - $janDays = Carbon::createFromDate($year, 1, 1)->daysInMonth; - $febDays = Carbon::createFromDate($year, 2, 1)->daysInMonth; - - (new GenerateScheduleForMonthAction())->execute($planner, 1, $year, [$user->id]); - (new GenerateScheduleForMonthAction())->execute($planner, 2, $year, [$user->id]); - - $this->assertEquals($janDays + $febDays, ScheduledUserDish::where('user_id', $user->id)->count()); - - $this->action->execute($planner, 1, $year, [$user->id]); - - $this->assertEquals($febDays, ScheduledUserDish::where('user_id', $user->id)->count()); - } -} diff --git a/tests/Unit/Schedule/Actions/GenerateScheduleForMonthActionTest.php b/tests/Unit/Schedule/Actions/GenerateScheduleForMonthActionTest.php deleted file mode 100644 index 16714fe..0000000 --- a/tests/Unit/Schedule/Actions/GenerateScheduleForMonthActionTest.php +++ /dev/null @@ -1,135 +0,0 @@ -setUpHasPlanner(); - $this->action = new GenerateScheduleForMonthAction(); - } - - public function test_generates_schedule_for_entire_month(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $month = 1; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - $this->action->execute($planner, $month, $year, [$user->id]); - - $this->assertDatabaseCount(Schedule::class, $daysInMonth); - $this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth); - } - - public function test_generates_schedule_for_multiple_users(): void - { - $planner = $this->planner; - $users = User::factory()->planner($planner)->count(3)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($users); - - $month = 2; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - $this->action->execute($planner, $month, $year, $users->pluck('id')->toArray()); - - $this->assertDatabaseCount(Schedule::class, $daysInMonth); - $this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth * 3); - } - - public function test_clears_existing_schedules_when_flag_is_true(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $month = 3; - $year = 2026; - - $this->action->execute($planner, $month, $year, [$user->id]); - $firstRunDishId = ScheduledUserDish::first()->user_dish_id; - - $this->action->execute($planner, $month, $year, [$user->id], true); - - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - $this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth); - } - - public function test_preserves_existing_schedules_when_flag_is_false(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $month = 4; - $year = 2026; - - $this->action->execute($planner, $month, $year, [$user->id]); - $originalCount = ScheduledUserDish::count(); - - $this->action->execute($planner, $month, $year, [$user->id], false); - - $this->assertDatabaseCount(ScheduledUserDish::class, $originalCount); - } - - public function test_skips_users_without_dishes(): void - { - $planner = $this->planner; - $userWithDish = User::factory()->planner($planner)->create(); - $userWithoutDish = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($userWithDish); - - $month = 5; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - $this->action->execute($planner, $month, $year, [$userWithDish->id, $userWithoutDish->id]); - - $this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth); - $this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $userWithoutDish->id]); - } - - public function test_only_generates_for_specified_users(): void - { - $planner = $this->planner; - $user1 = User::factory()->planner($planner)->create(); - $user2 = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach([$user1->id, $user2->id]); - - $month = 6; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - $this->action->execute($planner, $month, $year, [$user1->id]); - - $this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth); - $this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $user2->id]); - } -} diff --git a/tests/Unit/Schedule/Actions/RegenerateScheduleForDateForUsersActionTest.php b/tests/Unit/Schedule/Actions/RegenerateScheduleForDateForUsersActionTest.php deleted file mode 100644 index 89e0fbf..0000000 --- a/tests/Unit/Schedule/Actions/RegenerateScheduleForDateForUsersActionTest.php +++ /dev/null @@ -1,127 +0,0 @@ -setUpHasPlanner(); - $this->action = new RegenerateScheduleForDateForUsersAction(); - } - - public function test_regenerates_schedule_for_single_date(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::parse('2026-01-15'); - - $this->action->execute($planner, $date, [$user->id]); - - $this->assertDatabaseCount(Schedule::class, 1); - $this->assertDatabaseCount(ScheduledUserDish::class, 1); - $this->assertDatabaseHas(Schedule::class, [ - 'planner_id' => $planner->id, - 'date' => '2026-01-15', - ]); - } - - public function test_deletes_and_recreates_existing_schedule(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish1 = Dish::factory()->planner($planner)->create(); - $dish2 = Dish::factory()->planner($planner)->create(); - $dish1->users()->attach($user); - $dish2->users()->attach($user); - - $date = Carbon::parse('2026-02-10'); - - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $originalUserDish = $user->userDishes->first(); - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $originalUserDish->id, - 'is_skipped' => false, - ]); - - $this->action->execute($planner, $date, [$user->id]); - - $this->assertDatabaseCount(ScheduledUserDish::class, 1); - } - - public function test_regenerates_for_multiple_users(): void - { - $planner = $this->planner; - $users = User::factory()->planner($planner)->count(3)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($users); - - $date = Carbon::parse('2026-03-20'); - - $this->action->execute($planner, $date, $users->pluck('id')->toArray()); - - $this->assertDatabaseCount(ScheduledUserDish::class, 3); - } - - public function test_creates_schedule_if_not_exists(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::parse('2026-04-25'); - - $this->assertDatabaseCount(Schedule::class, 0); - - $this->action->execute($planner, $date, [$user->id]); - - $this->assertDatabaseCount(Schedule::class, 1); - $this->assertDatabaseHas(Schedule::class, [ - 'planner_id' => $planner->id, - 'date' => '2026-04-25', - ]); - } - - public function test_skips_users_without_dishes(): void - { - $planner = $this->planner; - $userWithDish = User::factory()->planner($planner)->create(); - $userWithoutDish = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($userWithDish); - - $date = Carbon::parse('2026-05-05'); - - $this->action->execute($planner, $date, [$userWithDish->id, $userWithoutDish->id]); - - $this->assertDatabaseCount(ScheduledUserDish::class, 1); - $this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $userWithoutDish->id]); - } -} diff --git a/tests/Unit/Schedule/Services/ScheduleCalendarServiceTest.php b/tests/Unit/Schedule/Services/ScheduleCalendarServiceTest.php deleted file mode 100644 index 6e6c841..0000000 --- a/tests/Unit/Schedule/Services/ScheduleCalendarServiceTest.php +++ /dev/null @@ -1,186 +0,0 @@ -setUpHasPlanner(); - $this->service = new ScheduleCalendarService(); - } - - public function test_returns_31_calendar_days(): void - { - $planner = $this->planner; - - $calendarDays = $this->service->getCalendarDays($planner, 1, 2026); - - $this->assertCount(31, $calendarDays); - } - - public function test_includes_correct_day_numbers(): void - { - $planner = $this->planner; - $month = 2; - $year = 2026; - $daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth; - - $calendarDays = $this->service->getCalendarDays($planner, $month, $year); - - for ($i = 0; $i < $daysInMonth; $i++) { - $this->assertEquals($i + 1, $calendarDays[$i]['day']); - } - - for ($i = $daysInMonth; $i < 31; $i++) { - $this->assertNull($calendarDays[$i]['day']); - } - } - - public function test_marks_today_correctly(): void - { - $planner = $this->planner; - $today = now(); - - $calendarDays = $this->service->getCalendarDays($planner, $today->month, $today->year); - - $todayIndex = $today->day - 1; - $this->assertTrue($calendarDays[$todayIndex]['isToday']); - - foreach ($calendarDays as $index => $day) { - if ($index !== $todayIndex && $day['day'] !== null) { - $this->assertFalse($day['isToday']); - } - } - } - - public function test_includes_scheduled_dishes(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::createFromDate(2026, 3, 15); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $userDish = $user->userDishes->first(); - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - - $calendarDays = $this->service->getCalendarDays($planner, 3, 2026); - - $this->assertFalse($calendarDays[14]['isEmpty']); - $this->assertCount(1, $calendarDays[14]['scheduledDishes']); - } - - public function test_empty_days_have_empty_scheduled_dishes(): void - { - $planner = $this->planner; - - $calendarDays = $this->service->getCalendarDays($planner, 4, 2026); - - foreach ($calendarDays as $day) { - if ($day['day'] !== null) { - $this->assertTrue($day['isEmpty']); - $this->assertCount(0, $day['scheduledDishes']); - } - } - } - - public function test_only_loads_schedules_for_specified_planner(): void - { - $planner1 = $this->planner; - $planner2 = $this->createPlanner(); - - $user1 = User::factory()->planner($planner1)->create(); - $user2 = User::factory()->planner($planner2)->create(); - - $dish1 = Dish::factory()->planner($planner1)->create(); - $dish2 = Dish::factory()->planner($planner2)->create(); - - $dish1->users()->attach($user1); - $dish2->users()->attach($user2); - - $date = Carbon::createFromDate(2026, 5, 10); - - $schedule1 = Schedule::create([ - 'planner_id' => $planner1->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $schedule2 = Schedule::create([ - 'planner_id' => $planner2->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule1->id, - 'user_id' => $user1->id, - 'user_dish_id' => $user1->userDishes->first()->id, - 'is_skipped' => false, - ]); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule2->id, - 'user_id' => $user2->id, - 'user_dish_id' => $user2->userDishes->first()->id, - 'is_skipped' => false, - ]); - - $calendarDays = $this->service->getCalendarDays($planner1, 5, 2026); - - $this->assertCount(1, $calendarDays[9]['scheduledDishes']); - $this->assertEquals($user1->id, $calendarDays[9]['scheduledDishes']->first()->user_id); - } - - public function test_get_month_name_returns_correct_format(): void - { - $this->assertEquals('January 2026', $this->service->getMonthName(1, 2026)); - $this->assertEquals('December 2025', $this->service->getMonthName(12, 2025)); - $this->assertEquals('February 2027', $this->service->getMonthName(2, 2027)); - } - - public function test_handles_february_in_leap_year(): void - { - $planner = $this->planner; - - $calendarDays = $this->service->getCalendarDays($planner, 2, 2028); - - $this->assertCount(31, $calendarDays); - - for ($i = 0; $i < 29; $i++) { - $this->assertNotNull($calendarDays[$i]['day']); - } - - for ($i = 29; $i < 31; $i++) { - $this->assertNull($calendarDays[$i]['day']); - } - } -} diff --git a/tests/Unit/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateActionTest.php b/tests/Unit/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateActionTest.php deleted file mode 100644 index c452a01..0000000 --- a/tests/Unit/ScheduledUserDish/Actions/DeleteScheduledUserDishForDateActionTest.php +++ /dev/null @@ -1,152 +0,0 @@ -setUpHasPlanner(); - $this->action = new DeleteScheduledUserDishForDateAction(); - } - - public function test_deletes_scheduled_user_dish(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::parse('2026-01-15'); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $userDish = $user->userDishes->first(); - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertTrue($result); - $this->assertDatabaseCount(ScheduledUserDish::class, 0); - } - - public function test_returns_false_when_schedule_does_not_exist(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - - $date = Carbon::parse('2026-02-20'); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertFalse($result); - } - - public function test_returns_false_when_scheduled_user_dish_does_not_exist(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - - $date = Carbon::parse('2026-03-10'); - Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertFalse($result); - } - - public function test_only_deletes_for_specified_user(): void - { - $planner = $this->planner; - $user1 = User::factory()->planner($planner)->create(); - $user2 = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach([$user1->id, $user2->id]); - - $date = Carbon::parse('2026-04-05'); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $user1Dish = $user1->userDishes->first(); - $user2Dish = $user2->userDishes->first(); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user1->id, - 'user_dish_id' => $user1Dish->id, - 'is_skipped' => false, - ]); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user2->id, - 'user_dish_id' => $user2Dish->id, - 'is_skipped' => false, - ]); - - $this->action->execute($planner, $date, $user1->id); - - $this->assertDatabaseCount(ScheduledUserDish::class, 1); - $this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $user1->id]); - $this->assertDatabaseHas(ScheduledUserDish::class, ['user_id' => $user2->id]); - } - - public function test_preserves_schedule_after_deletion(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::parse('2026-05-15'); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $userDish = $user->userDishes->first(); - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - - $this->action->execute($planner, $date, $user->id); - - $this->assertDatabaseCount(Schedule::class, 1); - $this->assertDatabaseHas(Schedule::class, ['id' => $schedule->id]); - } -} diff --git a/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php b/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php deleted file mode 100644 index 2a592c6..0000000 --- a/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php +++ /dev/null @@ -1,135 +0,0 @@ -setUpHasPlanner(); - $this->action = new SkipScheduledUserDishForDateAction(); - } - - public function test_skips_scheduled_user_dish(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach($user); - - $date = Carbon::parse('2026-01-15'); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $userDish = $user->userDishes->first(); - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'user_dish_id' => $userDish->id, - 'is_skipped' => false, - ]); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertTrue($result); - $this->assertDatabaseHas(ScheduledUserDish::class, [ - 'schedule_id' => $schedule->id, - 'user_id' => $user->id, - 'is_skipped' => true, - 'user_dish_id' => null, - ]); - } - - public function test_returns_false_when_schedule_does_not_exist(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - - $date = Carbon::parse('2026-02-20'); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertFalse($result); - } - - public function test_returns_false_when_scheduled_user_dish_does_not_exist(): void - { - $planner = $this->planner; - $user = User::factory()->planner($planner)->create(); - - $date = Carbon::parse('2026-03-10'); - Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $result = $this->action->execute($planner, $date, $user->id); - - $this->assertFalse($result); - } - - public function test_only_skips_for_specified_user(): void - { - $planner = $this->planner; - $user1 = User::factory()->planner($planner)->create(); - $user2 = User::factory()->planner($planner)->create(); - $dish = Dish::factory()->planner($planner)->create(); - $dish->users()->attach([$user1->id, $user2->id]); - - $date = Carbon::parse('2026-04-05'); - $schedule = Schedule::create([ - 'planner_id' => $planner->id, - 'date' => $date->format('Y-m-d'), - 'is_skipped' => false, - ]); - - $user1Dish = $user1->userDishes->first(); - $user2Dish = $user2->userDishes->first(); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user1->id, - 'user_dish_id' => $user1Dish->id, - 'is_skipped' => false, - ]); - - ScheduledUserDish::create([ - 'schedule_id' => $schedule->id, - 'user_id' => $user2->id, - 'user_dish_id' => $user2Dish->id, - 'is_skipped' => false, - ]); - - $this->action->execute($planner, $date, $user1->id); - - $this->assertDatabaseHas(ScheduledUserDish::class, [ - 'user_id' => $user1->id, - 'is_skipped' => true, - ]); - - $this->assertDatabaseHas(ScheduledUserDish::class, [ - 'user_id' => $user2->id, - 'is_skipped' => false, - ]); - } -} diff --git a/update.sh b/update.sh deleted file mode 100755 index 7320639..0000000 --- a/update.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e - -echo "๐Ÿ”„ Pulling latest backend changes..." -git pull origin main - -echo "๐Ÿ“ฆ Installing PHP dependencies..." -composer install --no-interaction --prefer-dist --optimize-autoloader - -echo "๐Ÿ—„๏ธ Running migrations..." -php artisan migrate --force - -echo "๐Ÿงน Clearing and caching config..." -php artisan config:cache -php artisan route:cache -php artisan view:cache - -echo "โœ… Backend update complete!"