Replace frontend:main with frontend:release/v0.3
|
|
@ -1 +1,4 @@
|
||||||
.env.local
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
47
frontend/.gitignore
vendored
|
|
@ -1,45 +1,6 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
/.idea
|
|
||||||
/.env.local
|
|
||||||
/package-lock.json
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
/node_modules/
|
||||||
|
|
||||||
# debug
|
# React Router
|
||||||
npm-debug.log*
|
/.react-router/
|
||||||
yarn-debug.log*
|
/build/
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
.env
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
|
||||||
|
|
@ -1,289 +0,0 @@
|
||||||
# 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
|
|
||||||
<Button variant="primary" appearance="solid">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
// Outline accent button with icon
|
|
||||||
<Button variant="accent" appearance="outline" icon={<PlusIcon />}>
|
|
||||||
Add Item
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
// Text danger button
|
|
||||||
<Button variant="danger" appearance="text" size="small">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
// Link button
|
|
||||||
<Button href="/dishes" appearance="outline" icon={<ChevronLeftIcon />}>
|
|
||||||
Back to Dishes
|
|
||||||
</Button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Input (`src/components/ui/Input.tsx`)
|
|
||||||
|
|
||||||
Standardized text input component with label, error, and helper text support.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
helperText?: string;
|
|
||||||
fullWidth?: boolean; // Default: true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Basic input with label
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
// Input with error
|
|
||||||
<Input
|
|
||||||
label="Name"
|
|
||||||
value={name}
|
|
||||||
error="Name is required"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// Input with helper text
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
helperText="Must be at least 8 characters"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select (`src/components/ui/Select.tsx`)
|
|
||||||
|
|
||||||
Standardized select dropdown component.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
helperText?: string;
|
|
||||||
fullWidth?: boolean; // Default: true
|
|
||||||
options: Array<{ value: string | number; label: string }>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Select
|
|
||||||
label="Recurrence"
|
|
||||||
value={recurrence}
|
|
||||||
onChange={(e) => setRecurrence(e.target.value)}
|
|
||||||
options={[
|
|
||||||
{ value: 7, label: 'Weekly' },
|
|
||||||
{ value: 30, label: 'Monthly' },
|
|
||||||
{ value: 365, label: 'Yearly' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checkbox (`src/components/ui/Checkbox.tsx`)
|
|
||||||
|
|
||||||
Standardized checkbox component with label support.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
helperText?: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Checkbox
|
|
||||||
label="Enable notifications"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(e) => setEnabled(e.target.checked)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Toggle (`src/components/ui/Toggle.tsx`)
|
|
||||||
|
|
||||||
Enhanced toggle switch component.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ToggleProps {
|
|
||||||
checked: boolean;
|
|
||||||
onChange: (checked: boolean) => void;
|
|
||||||
label?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
helperText?: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Toggle
|
|
||||||
checked={isActive}
|
|
||||||
onChange={setIsActive}
|
|
||||||
label="Active"
|
|
||||||
helperText="Enable this feature"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alert (`src/components/ui/Alert.tsx`)
|
|
||||||
|
|
||||||
Standardized alert component for displaying messages.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AlertProps {
|
|
||||||
type: 'error' | 'warning' | 'info' | 'success';
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Error alert
|
|
||||||
<Alert type="error">
|
|
||||||
An error occurred while saving.
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
// Success alert
|
|
||||||
<Alert type="success">
|
|
||||||
User created successfully!
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
// Warning alert
|
|
||||||
<Alert type="warning">
|
|
||||||
This action cannot be undone.
|
|
||||||
</Alert>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Card (`src/components/layout/Card.tsx`)
|
|
||||||
|
|
||||||
Standardized card container component.
|
|
||||||
|
|
||||||
#### Props
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface CardProps {
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Card>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<h3>User Name</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="small">Edit</Button>
|
|
||||||
<Button size="small" variant="danger">Delete</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### From Old Button Components
|
|
||||||
|
|
||||||
The old button components have been removed. Use the unified `Button` component instead:
|
|
||||||
|
|
||||||
- `SolidButton` → `Button` with `appearance="solid"`
|
|
||||||
- `OutlineButton` → `Button` with `appearance="outline"`
|
|
||||||
- `SolidLinkButton` → `Button` with `appearance="solid"` and `href` prop
|
|
||||||
- `OutlineLinkButton` → `Button` with `appearance="outline"` and `href` prop
|
|
||||||
|
|
||||||
### Custom CSS Classes to Tailwind
|
|
||||||
|
|
||||||
Replace custom CSS classes with Tailwind utilities:
|
|
||||||
|
|
||||||
- `background-red` → `bg-primary`
|
|
||||||
- `background-secondary` → `bg-secondary`
|
|
||||||
- `font-size-18` → `text-lg`
|
|
||||||
- `text-accent-blue` → `text-accent`
|
|
||||||
- `border-accent` → `border-accent`
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Use semantic color names**: Prefer `text-primary`, `bg-danger` over hardcoded colors
|
|
||||||
2. **Consistent spacing**: Use Tailwind's spacing scale (p-2, p-4, etc.)
|
|
||||||
3. **Responsive design**: Consider mobile-first responsive classes
|
|
||||||
4. **Accessibility**: Use proper ARIA attributes and semantic HTML
|
|
||||||
5. **Component reusability**: Always use the standardized components instead of creating custom styled elements
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential areas for further improvement:
|
|
||||||
|
|
||||||
- Migrate remaining inline-styled inputs to use the `Input` component
|
|
||||||
- Add dark mode support
|
|
||||||
- Create compound components for common patterns (e.g., FormGroup)
|
|
||||||
- Add animation utilities for better UX
|
|
||||||
- Implement a comprehensive form validation system
|
|
||||||
22
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
|
@ -1,2 +1,87 @@
|
||||||
# DishPlanner Front End
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
|
|
|
||||||
18
frontend/app/app.css
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
@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";
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply bg-white dark:bg-gray-950;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
frontend/app/components/features/auth/LoginForm.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
|
||||||
|
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<string[]>([])
|
||||||
|
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 (
|
||||||
|
<div style={{ marginTop: '15vh' }} className="lg:w-1/3 lg:mx-auto">
|
||||||
|
<div className="pt-2 text-2xl font-syncopate text-primary">
|
||||||
|
DISH PLANNER
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-secondary rounded-lg px-5 pt-5 pb-3 lg:pt-10 lg:pb-7">
|
||||||
|
{ alertSuccess.length > 0 &&
|
||||||
|
<Alert type="success" className="mb-4 px-5 py-2 capitalize text-center">
|
||||||
|
{alertSuccess.map((msg, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{msg}
|
||||||
|
<br />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
<form onSubmit={handleSubmit} className="max-w-sm mx-auto flex flex-col">
|
||||||
|
{error && <p className="text-red-600 mb-4">{error}</p>}
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full p-2 mb-4 border rounded border-secondary bg-gray-600 text-secondary"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full p-2 mb-4 border rounded text-secondary border-secondary bg-gray-600"
|
||||||
|
/>
|
||||||
|
<SolidButton type="submit">Login</SolidButton>
|
||||||
|
<Link to={routes.auth.register()} className="lowercase hover:underline text-center mt-1 text-secondary underline">
|
||||||
|
Create an account
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginForm
|
||||||
107
frontend/app/components/features/auth/RegisterForm.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
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 <div className="border-2 border-secondary rounded-lg p-5 mt-0">
|
||||||
|
<SectionTitle>Registration successful!</SectionTitle>
|
||||||
|
Please continue to the <Link to={ routes.auth.login() }>login page</Link>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: '15vh' }} className="lg:w-1/3 lg:mx-auto">
|
||||||
|
<div className="pt-2 text-2xl font-syncopate text-primary">
|
||||||
|
DISH PLANNER
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-secondary rounded-lg p-5 mt-0 lg:pt-10 lg:pb-7">
|
||||||
|
<form onSubmit={ handleSubmit } className="max-w-sm mx-auto space-y-4 flex flex-col">
|
||||||
|
<h2 className="text-xl font-bold text-secondary text-right">Register</h2>
|
||||||
|
{ error && <p className="text-red-600">{ error }</p> }
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
value={ name }
|
||||||
|
onChange={ e => setName(e.target.value) }
|
||||||
|
required
|
||||||
|
className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={ email }
|
||||||
|
onChange={ e => setEmail(e.target.value) }
|
||||||
|
required
|
||||||
|
className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={ password }
|
||||||
|
onChange={ e => setPassword(e.target.value) }
|
||||||
|
required
|
||||||
|
className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password Again"
|
||||||
|
value={ passwordAgain }
|
||||||
|
onChange={ e => setPasswordAgain(e.target.value) }
|
||||||
|
required
|
||||||
|
className="w-full p-2 border rounded border-secondary bg-gray-600 text-secondary"
|
||||||
|
/>
|
||||||
|
<SolidButton
|
||||||
|
type="submit"
|
||||||
|
className={ isLoading ? "opacity-50 cursor-not-allowed" : "background-red" }
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</SolidButton>
|
||||||
|
<Link to={routes.auth.login()} className="lowercase mt-2 hover:underline text-center text-secondary underline">
|
||||||
|
Back to Login
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterForm
|
||||||
|
|
@ -4,7 +4,8 @@ import { UserType } from "@/types/UserType";
|
||||||
import { useFetchUsers } from "@/hooks/useFetchUsers";
|
import { useFetchUsers } from "@/hooks/useFetchUsers";
|
||||||
import Spinner from "@/components/Spinner";
|
import Spinner from "@/components/Spinner";
|
||||||
import {addUserToDish} from "@/utils/api/dishApi";
|
import {addUserToDish} from "@/utils/api/dishApi";
|
||||||
import Button from "@/components/ui/Button";
|
import OutlineButton from "@/components/ui/Buttons/OutlineButton";
|
||||||
|
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dish: DishType;
|
dish: DishType;
|
||||||
|
|
@ -53,15 +54,14 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<OutlineButton
|
||||||
appearance="outline"
|
|
||||||
className="mb-2 flex-none w-fit ml-auto"
|
className="mb-2 flex-none w-fit ml-auto"
|
||||||
onClick={() => setShowAdd(!showAdd)}
|
onClick={() => setShowAdd(!showAdd)}
|
||||||
disabled={remainingUsers.length === 0}
|
disabled={remainingUsers.length === 0}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Add User
|
Add User
|
||||||
</Button>
|
</OutlineButton>
|
||||||
|
|
||||||
{ showAdd && (
|
{ showAdd && (
|
||||||
<div className="mt-2 mb-5 w-full">
|
<div className="mt-2 mb-5 w-full">
|
||||||
|
|
@ -71,7 +71,7 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||||
<select
|
<select
|
||||||
value={selectedUser}
|
value={selectedUser}
|
||||||
onChange={(e) => setSelectedUser(e.target.value)}
|
onChange={(e) => setSelectedUser(e.target.value)}
|
||||||
className="p-2 border rounded w-full bg-secondary border-secondary"
|
className="p-2 border rounded w-full background-secondary border-secondary"
|
||||||
>
|
>
|
||||||
<option value="-1">Select user</option>
|
<option value="-1">Select user</option>
|
||||||
{remainingUsers.map((user: UserType) => (
|
{remainingUsers.map((user: UserType) => (
|
||||||
|
|
@ -83,14 +83,12 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-none py-2 pr-2 pl-4">
|
<div className="flex-none py-2 pr-2 pl-4">
|
||||||
<Button type="submit"
|
<SolidButton type="submit"
|
||||||
appearance="solid"
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
size="small"
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
>
|
>
|
||||||
Add User
|
Add User
|
||||||
</Button>
|
</SolidButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -3,10 +3,10 @@ import { useRouter } from "next/navigation";
|
||||||
import { createDish } from "@/utils/api/dishApi";
|
import { createDish } from "@/utils/api/dishApi";
|
||||||
import PageTitle from "@/components/ui/PageTitle";
|
import PageTitle from "@/components/ui/PageTitle";
|
||||||
import Alert from "@/components/ui/Alert";
|
import Alert from "@/components/ui/Alert";
|
||||||
import Button from "@/components/ui/Button";
|
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||||
|
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
import Hr from "@/components/ui/Hr";
|
import Hr from "@/components/ui/Hr"
|
||||||
import Input from "@/components/ui/Input";
|
|
||||||
|
|
||||||
const CreateDishForm = () => {
|
const CreateDishForm = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -55,38 +55,37 @@ const CreateDishForm = () => {
|
||||||
<Alert type="error">{ error }</Alert>
|
<Alert type="error">{ error }</Alert>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
<Input
|
<div>
|
||||||
label="Dish Name"
|
<label htmlFor="name" className="block text-sm font-medium">Dish Name</label>
|
||||||
type="text"
|
<input
|
||||||
id="name"
|
type="text"
|
||||||
name="name"
|
id="name"
|
||||||
value={ name }
|
name="name"
|
||||||
onChange={ (e) => setName(e.target.value) }
|
value={ name }
|
||||||
placeholder="Enter dish name"
|
onChange={ (e) => setName(e.target.value) } // Update the name state on change
|
||||||
className="mb-4"
|
className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900"
|
||||||
/>
|
placeholder="Enter dish name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<SolidButton
|
||||||
type="submit"
|
type="submit"
|
||||||
appearance="solid"
|
|
||||||
variant="primary"
|
|
||||||
disabled={ loading }
|
disabled={ loading }
|
||||||
className={ loading ? "bg-gray-400" : '' }
|
className={ loading ? "bg-gray-400" : '' }
|
||||||
>
|
>
|
||||||
{ loading ? "Saving..." : "Save Changes" }
|
{ loading ? "Saving..." : "Save Changes" }
|
||||||
</Button>
|
</SolidButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Hr />
|
<Hr />
|
||||||
|
|
||||||
<Button
|
<OutlineLinkButton
|
||||||
appearance="outline"
|
|
||||||
href="/dishes"
|
href="/dishes"
|
||||||
className="mt-4 pl-0 mr-0"
|
className="mt-4 pl-0 mr-0"
|
||||||
icon={ <ChevronLeftIcon/> }
|
icon={ <ChevronLeftIcon/> }
|
||||||
>
|
>
|
||||||
Back to dishes
|
Back to dishes
|
||||||
</Button>
|
</OutlineLinkButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -5,8 +5,7 @@ import {updateDish} from "@/utils/api/dishApi";
|
||||||
import {DishType} from "@/types/DishType";
|
import {DishType} from "@/types/DishType";
|
||||||
import useRoutes from "@/hooks/useRoutes";
|
import useRoutes from "@/hooks/useRoutes";
|
||||||
import Spinner from "@/components/Spinner";
|
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 {
|
interface Props {
|
||||||
dish: DishType
|
dish: DishType
|
||||||
|
|
@ -58,15 +57,20 @@ const EditDishForm: FC<Props> = ({ dish }) => {
|
||||||
error != '' && <Alert type="error" >{ error }</Alert>
|
error != '' && <Alert type="error" >{ error }</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Input
|
{/* Dish name input */}
|
||||||
label="Dish Name"
|
<div>
|
||||||
type="text"
|
<label htmlFor="name" className="block text-sm font-medium">Dish Name</label>
|
||||||
id="name"
|
<input
|
||||||
name="name"
|
type="text"
|
||||||
value={name}
|
id="name"
|
||||||
onChange={(e) => setName(e.target.value)}
|
name="name"
|
||||||
/>
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)} // Update the name state on change
|
||||||
|
className="p-2 border rounded w-full bg-gray-500 border-secondary background-secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save button */}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
@ -4,8 +4,7 @@ import {syncUserDishRecurrences} from "@/utils/api/usersApi";
|
||||||
import Spinner from "@/components/Spinner";
|
import Spinner from "@/components/Spinner";
|
||||||
import {UserDishType} from "@/types/ScheduledUserDishType";
|
import {UserDishType} from "@/types/ScheduledUserDishType";
|
||||||
import {RecurrenceType} from "@/types/ScheduleType";
|
import {RecurrenceType} from "@/types/ScheduleType";
|
||||||
import Button from "@/components/ui/Button";
|
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||||
import Input from "@/components/ui/Input";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userDish: UserDishType
|
userDish: UserDishType
|
||||||
|
|
@ -75,7 +74,7 @@ const EditDishUserCardEditForm: FC<Props> = ({ userDish, onSubmit}) => {
|
||||||
isWeeklyOn && (
|
isWeeklyOn && (
|
||||||
<div className="flex-grow ml-2">
|
<div className="flex-grow ml-2">
|
||||||
<label htmlFor="weekday" className="mr-2">on</label>
|
<label htmlFor="weekday" className="mr-2">on</label>
|
||||||
<select value={weekday} onChange={(e) => setWeekday(parseInt(e.currentTarget.value))} className="bg-secondary border-secondary border-2">
|
<select value={weekday} onChange={(e) => setWeekday(parseInt(e.currentTarget.value))} className="background-secondary border-secondary border-2">
|
||||||
<option value="0">Sunday</option>
|
<option value="0">Sunday</option>
|
||||||
<option value="1">Monday</option>
|
<option value="1">Monday</option>
|
||||||
<option value="2">Tuesday</option>
|
<option value="2">Tuesday</option>
|
||||||
|
|
@ -105,14 +104,14 @@ const EditDishUserCardEditForm: FC<Props> = ({ userDish, onSubmit}) => {
|
||||||
{
|
{
|
||||||
isMinimumOn && (
|
isMinimumOn && (
|
||||||
<div className="flex-grow ml-2">
|
<div className="flex-grow ml-2">
|
||||||
<Input type="number" value={minimumValue} onChange={(e) => setMinimumValue(parseInt(e.currentTarget.value))} min="0" max="365" className="w-12 px-2" fullWidth={false} />
|
<input type="number" value={minimumValue} onChange={(e) => setMinimumValue(parseInt(e.currentTarget.value))} min="0" max="365" className="background-secondary border-secondary border-2 w-12 px-2" />
|
||||||
<label htmlFor="minimum">Days</label>
|
<label htmlFor="minimum">Days</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" appearance="solid" variant="primary">Save</Button>
|
<SolidButton type="submit">Save</SolidButton>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ const divStyles = classNames(
|
||||||
|
|
||||||
const linkStyles = classNames(
|
const linkStyles = classNames(
|
||||||
'border-b-2', 'border-secondary', 'uppercase',
|
'border-b-2', 'border-secondary', 'uppercase',
|
||||||
'text-primary', 'hover:bg-secondary', 'pb-2', 'pl-5',
|
'text-primary', 'hover:background-secondary', 'pb-2', 'pl-5',
|
||||||
'space-grotesk', 'text-xl'
|
'space-grotesk', 'text-xl'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ const ScheduleEditForm: FC<Props> = ({ date }) => {
|
||||||
<PageTitle>Edit Day</PageTitle>
|
<PageTitle>Edit Day</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<SectionTitle className="text-right text-2xl capitalize font-default">{ transformDate(schedule.date) }</SectionTitle>
|
<SectionTitle className="text-right font-size-24 capitalize font-default">{ transformDate(schedule.date) }</SectionTitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ const ScheduleEditForm: FC<Props> = ({ date }) => {
|
||||||
{
|
{
|
||||||
scheduleData
|
scheduleData
|
||||||
.map((scheduleData) => <div className="flex w-full py-2" key={ scheduleData.user.id }>
|
.map((scheduleData) => <div className="flex w-full py-2" key={ scheduleData.user.id }>
|
||||||
<div className="flex-none px-2 text-lg pt-2">{ scheduleData.user.name }</div>
|
<div className="flex-none px-2 font-size-18 pt-2">{ scheduleData.user.name }</div>
|
||||||
<div className="flex-grow px-2">
|
<div className="flex-grow px-2">
|
||||||
<select
|
<select
|
||||||
className="w-full bg-gray-500 border-2 border-secondary p-2 rounded"
|
className="w-full bg-gray-500 border-2 border-secondary p-2 rounded"
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { type FC, 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: FC<ScheduleRegenerateButtonProps> = ({ onModalClose }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpen(false)
|
||||||
|
if (onModalClose) {
|
||||||
|
onModalClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalChildren = <ScheduleRegenerateForm closeModal={() => handleCloseModal()}/>
|
||||||
|
const buttonChild = <div className="p-0 flex">
|
||||||
|
<ArrowPathIcon fontSize={24} className="h-5 w-5 mt-1 mr-2" color="gray-500" aria-hidden="true" onClick={() => setOpen(true)} />
|
||||||
|
<p>Regenerate</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
return <Modal
|
||||||
|
buttonClassName="ml-auto mr-2 py-1"
|
||||||
|
buttonChildren={buttonChild}
|
||||||
|
modalChildren={modalChildren}
|
||||||
|
modalOpen={open}
|
||||||
|
setModalOpen={setOpen}
|
||||||
|
/>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduleRegenerateButton;
|
||||||
|
|
@ -3,7 +3,7 @@ import Toggle from "@/components/ui/Toggle";
|
||||||
import {FC, useEffect, useState} from "react";
|
import {FC, useEffect, useState} from "react";
|
||||||
import {generateSchedule} from "@/utils/api/scheduleApi";
|
import {generateSchedule} from "@/utils/api/scheduleApi";
|
||||||
import Alert from "@/components/ui/Alert";
|
import Alert from "@/components/ui/Alert";
|
||||||
import Button from "@/components/ui/Button";
|
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||||
|
|
||||||
interface ScheduleRegenerateFormProps {
|
interface ScheduleRegenerateFormProps {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
|
|
@ -34,7 +34,7 @@ const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) =
|
||||||
<div className="bg-gray-500 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-gray-500 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start w-full">
|
<div className="sm:flex sm:items-start w-full">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left ">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left ">
|
||||||
<DialogTitle as="h3" className="text-base font-semibold text-accent">
|
<DialogTitle as="h3" className="text-base font-semibold text-accent-blue">
|
||||||
Regenerate Schedule
|
Regenerate Schedule
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
|
|
@ -43,7 +43,7 @@ const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) =
|
||||||
error && <Alert type="error">{ error }</Alert>
|
error && <Alert type="error">{ error }</Alert>
|
||||||
}
|
}
|
||||||
<div className="flex-1/2 pr-5">
|
<div className="flex-1/2 pr-5">
|
||||||
<label htmlFor="overwrite" className="text-accent">Overwrite current schedule</label>
|
<label htmlFor="overwrite" className="text-accent-blue">Overwrite current schedule</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1/2">
|
<div className="flex-1/2">
|
||||||
<Toggle onChange={handleToggle} checked={overwrite} />
|
<Toggle onChange={handleToggle} checked={overwrite} />
|
||||||
|
|
@ -56,24 +56,21 @@ const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) =
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-500 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
<div className="bg-gray-500 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||||
<Button
|
<SolidButton
|
||||||
type="button"
|
type="button"
|
||||||
appearance="solid"
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => handleSubmit()}
|
onClick={() => 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"
|
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
|
Regenerate
|
||||||
</Button>
|
</SolidButton>
|
||||||
<Button
|
<SolidButton
|
||||||
type="button"
|
type="button"
|
||||||
appearance="solid"
|
data-autofocus
|
||||||
variant="secondary"
|
|
||||||
onClick={() => close()}
|
onClick={() => 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"
|
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
|
Cancel
|
||||||
</Button>
|
</SolidButton>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
@ -16,7 +16,7 @@ const UserDishEditCard: FC<Props> = ({ scheduledUserDish, allDishes }) => {
|
||||||
const [isSuccess, setIsSuccess] = useState(false);
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
|
||||||
const selectStyle = classNames(
|
const selectStyle = classNames(
|
||||||
'p-2', 'rounded', 'w-full', 'bg-secondary',
|
'p-2', 'rounded', 'w-full', 'background-secondary',
|
||||||
'focus:outline-none',
|
'focus:outline-none',
|
||||||
'transition-[border-color] ease-out duration-1000', 'border-2', // Keep consistent base styles
|
'transition-[border-color] ease-out duration-1000', 'border-2', // Keep consistent base styles
|
||||||
{
|
{
|
||||||
|
|
@ -11,7 +11,7 @@ const DateBadge: FC<Props> = ({ className, date }) => {
|
||||||
const isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd")
|
const isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd")
|
||||||
|
|
||||||
const textStyle = classNames("inline font-bold", {
|
const textStyle = classNames("inline font-bold", {
|
||||||
'text-accent': isToday,
|
'text-accent-blue': isToday,
|
||||||
'text-secondary': !isToday,
|
'text-secondary': !isToday,
|
||||||
}, className)
|
}, className)
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ const ScheduleDayCard: FC<Props> = ({schedule, users}) => {
|
||||||
|
|
||||||
const containerStyles = classNames(
|
const containerStyles = classNames(
|
||||||
'w-full bg-gray-500 pt-5 pb-2 rounded-2xl text-xl', {
|
'w-full bg-gray-500 pt-5 pb-2 rounded-2xl text-xl', {
|
||||||
'border-2 text-accent border-accent': isToday,
|
'border-2 text-accent-blue border-accent-blue': isToday,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,9 +22,9 @@ const ScheduleDayCardUserDish: FC<Props> = ({ schedule, user }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex bg-secondary pb-1 rounded-2xl text-xl" key={ `user-${ user.id }` }>
|
<div className="w-full flex background-secondary pb-1 rounded-2xl text-xl" key={ `user-${ user.id }` }>
|
||||||
<div className="w-1/3 text-base text-right pr-3">{ user.name } : </div>
|
<div className="w-1/3 font-size-16 text-right pr-3">{ user.name } : </div>
|
||||||
<div className="w-2/3 text-base">{ getDish(user) }</div>
|
<div className="w-2/3 font-size-16">{ getDish(user) }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -6,8 +6,7 @@ import PageTitle from "@/components/ui/PageTitle";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Alert from "@/components/ui/Alert";
|
import Alert from "@/components/ui/Alert";
|
||||||
import {UserType} from "@/types/UserType";
|
import {UserType} from "@/types/UserType";
|
||||||
import Button from "@/components/ui/Button";
|
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||||
import Input from "@/components/ui/Input";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
|
|
@ -45,17 +44,17 @@ const EditUserForm: FC<Props> = ({ user }) => {
|
||||||
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Input
|
<label htmlFor="name">Name</label>
|
||||||
label="Name"
|
<input type="text"
|
||||||
type="text"
|
placeholder=""
|
||||||
placeholder=""
|
name="name"
|
||||||
name="name"
|
id="name"
|
||||||
id="name"
|
value={name}
|
||||||
value={name}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onChange={(e) => setName(e.target.value)}
|
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" appearance="solid" variant="primary" className="mt-4">Update</Button>
|
<SolidButton type="submit" className="mt-4">Update</SolidButton>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
45
frontend/app/components/layout/AuthGuard.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from "react-router"
|
||||||
|
|
||||||
|
// Optional Loading spinner component to display while loading
|
||||||
|
const LoadingSpinner = () => (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<div className="animate-spin w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||||
|
const { isAuthenticated } = useAuth(); // Access the authentication state from AuthContext
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const publicRoutes = ['/login', '/register'];
|
||||||
|
const isPublic = publicRoutes.includes(location.pathname);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Determine behavior based on auth state and route type
|
||||||
|
if (isAuthenticated === null) {
|
||||||
|
// Await authentication resolution (e.g., token check)
|
||||||
|
setLoading(true);
|
||||||
|
} else 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 });
|
||||||
|
} else {
|
||||||
|
// Otherwise, stop loading since the state is resolved
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, location.pathname, isPublic, navigate]);
|
||||||
|
|
||||||
|
// Show a spinner while authentication state is loading
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render children only when the authentication state and path are valid
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
15
frontend/app/components/layout/Card.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React, {FC} from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card: FC<Props> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full border-2 border-secondary p-2 pl-4 my-2 rounded flex">
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
||||||
|
|
@ -53,21 +53,21 @@ const NavBar = () => {
|
||||||
|
|
||||||
{/* Desktop Menu */}
|
{/* Desktop Menu */}
|
||||||
<div className="hidden md:flex space-x-6">
|
<div className="hidden md:flex space-x-6">
|
||||||
<Link href={routes.home()} className="text-accent hover:bg-secondary">
|
<Link href={routes.home()} className="text-accent-blue hover:background-secondary">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.dish.index()} className="text-accent hover:bg-secondary">
|
<Link href={routes.dish.index()} className="text-accent-blue hover:background-secondary">
|
||||||
Dishes
|
Dishes
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.user.index()} className="text-accent hover:bg-secondary">
|
<Link href={routes.user.index()} className="text-accent-blue hover:background-secondary">
|
||||||
Users
|
Users
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.schedule.history()} className="text-accent hover:bg-secondary">
|
<Link href={routes.schedule.history()} className="text-accent-blue hover:background-secondary">
|
||||||
History
|
History
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.auth.login()}
|
<Link href={routes.auth.login()}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="text-primary text-right hover:bg-secondary">
|
className="text-primary text-right hover:background-secondary">
|
||||||
Logout
|
Logout
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
9
frontend/app/components/pages/PrivatePage.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const PrivatePage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
private
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivatePage
|
||||||
11
frontend/app/components/pages/PublicPage.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Outlet } from "react-router"
|
||||||
|
|
||||||
|
const PublicPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublicPage
|
||||||
35
frontend/app/components/ui/Alert.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from "react"
|
||||||
|
import type { FC } from "react"
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
type: 'error' | 'warning' | 'info' | 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Alert: FC<Props> = ({ 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 (
|
||||||
|
<div className={styles}>
|
||||||
|
{ children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alert
|
||||||
62
frontend/app/components/ui/Button.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
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<ButtonProps> = ({ 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 (
|
||||||
|
<Link href={href} className={styles}>
|
||||||
|
{ icon && iconElement}
|
||||||
|
{ children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={onClick} className={styles} disabled={disabled} type={ type ?? 'button'}>
|
||||||
|
{ icon && iconElement}
|
||||||
|
{ children }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button
|
||||||
37
frontend/app/components/ui/Buttons/OutlineButton.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { type 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<Props> = ({ 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 (
|
||||||
|
<button
|
||||||
|
type={ type }
|
||||||
|
className={ style }
|
||||||
|
disabled={ disabled }
|
||||||
|
onClick={ onClick }
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OutlineButton
|
||||||
49
frontend/app/components/ui/Buttons/OutlineLinkButton.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { type FC, type ReactElement } 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: FC<Props> = ({ 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 (
|
||||||
|
<Link className={linkClassNames} to={href}>
|
||||||
|
{iconElement}
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OutlineLinkButton
|
||||||
40
frontend/app/components/ui/Buttons/SolidButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { type 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<Props> = ({ 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 (
|
||||||
|
<button
|
||||||
|
className={ style }
|
||||||
|
disabled={ disabled }
|
||||||
|
onClick={ onClick }
|
||||||
|
type={ type }
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SolidButton
|
||||||
46
frontend/app/components/ui/Buttons/SolidLinkButton.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { type FC, type ReactElement } 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: FC<Props> = ({ 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 (
|
||||||
|
<Link className={style} to={href}>
|
||||||
|
<div className="flex-grow"></div>
|
||||||
|
{iconElement}
|
||||||
|
{children}
|
||||||
|
<div className="flex-grow"></div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SolidLinkButton
|
||||||
|
|
@ -7,7 +7,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Description = ({ children, className }: Props) => {
|
const Description = ({ children, className }: Props) => {
|
||||||
const style = classNames("italic text-base",
|
const style = classNames("italic font-size-16",
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
60
frontend/app/components/ui/Modal.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { type FC, 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: FC<ModalProps> = ({
|
||||||
|
buttonLabel,
|
||||||
|
buttonClassName,
|
||||||
|
modalChildren,
|
||||||
|
modalOpen,
|
||||||
|
buttonChildren,
|
||||||
|
setModalOpen,
|
||||||
|
}) => {
|
||||||
|
const buttonStyles = classNames(buttonClassName, 'anta-regular');
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="primary" appearance="outline" type="button" onClick={() => setModalOpen(true)} className={buttonStyles}>
|
||||||
|
{buttonChildren
|
||||||
|
? buttonChildren
|
||||||
|
: buttonLabel ?? 'button'
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/*<Dialog open={modalOpen} onClose={setModalOpen} className="relative z-10">*/}
|
||||||
|
{/* <DialogBackdrop*/}
|
||||||
|
{/* transition*/}
|
||||||
|
{/* className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* <div className="fixed inset-0 z-10 w-screen overflow-y-auto">*/}
|
||||||
|
{/* <div*/}
|
||||||
|
{/* className="flex min-h-full items-start mt-5 justify-center p-4 text-center sm:items-center sm:p-0">*/}
|
||||||
|
{/* <DialogPanel*/}
|
||||||
|
{/* transition*/}
|
||||||
|
{/* className="relative transform overflow-hidden rounded-lg bg-primary text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg data-closed:sm:translate-y-0 data-closed:sm:scale-95"*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* <XMarkIcon width={24} className="absolute top-4 right-4 cursor-pointer"*/}
|
||||||
|
{/* onClick={() => closeModal()}/>*/}
|
||||||
|
{/* {modalChildren}*/}
|
||||||
|
{/* </DialogPanel>*/}
|
||||||
|
{/* </div>*/}
|
||||||
|
{/* </div>*/}
|
||||||
|
{/*</Dialog>*/}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
18
frontend/app/components/ui/PageTitle.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { type FC } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: string,
|
||||||
|
className?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageTitle: FC<Props> = ({ children, className }) => {
|
||||||
|
const styles = classNames(
|
||||||
|
'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold',
|
||||||
|
className,
|
||||||
|
)
|
||||||
|
|
||||||
|
return <h1 className={styles}>{ children }</h1>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageTitle
|
||||||
|
|
@ -6,7 +6,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionTitle = ({ children, className }: Props) => {
|
const SectionTitle = ({ children, className }: Props) => {
|
||||||
const style = classNames("block text-lg uppercase w-full pl-2 text-accent",
|
const style = classNames("block font-size-18 uppercase w-full pl-2 text-accent-blue",
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
42
frontend/app/components/ui/Toggle.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {FC} from "react";
|
||||||
|
|
||||||
|
interface ToggleProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toggle: FC<ToggleProps> = ({ checked, onChange }) => {
|
||||||
|
const handleChange = () => {
|
||||||
|
onChange(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className="flex items-center relative w-max cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
{/* Hidden input */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => handleChange()} // Update state using the checkbox itself
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Toggle text-accent-blue */}
|
||||||
|
<div
|
||||||
|
className={`relative transition-colors w-14 h-7 rounded-full cursor-pointer ${
|
||||||
|
checked ? "bg-green-500" : "bg-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* The slider dot */}
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 left-0.5 w-6 h-6 rounded-full transform transition-transform bg-white ${
|
||||||
|
checked ? "translate-x-7" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toggle;
|
||||||
32
frontend/app/hooks/useRoutes.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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;
|
||||||
82
frontend/app/root.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<Meta/>
|
||||||
|
<Links/>
|
||||||
|
</head>
|
||||||
|
<body className="antialiased w-full min-h-screen bg-gray-600">
|
||||||
|
<AuthProvider>
|
||||||
|
<AuthGuard>
|
||||||
|
<div className="m-5 lg:container lg:mx-auto">{ children }</div>
|
||||||
|
</AuthGuard>
|
||||||
|
</AuthProvider>
|
||||||
|
<ScrollRestoration/>
|
||||||
|
<Scripts/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Outlet/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
|
<h1>{ message }</h1>
|
||||||
|
<p>{ details }</p>
|
||||||
|
{ stack && (
|
||||||
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
|
<code>{ stack }</code>
|
||||||
|
</pre>
|
||||||
|
) }
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
frontend/app/routes.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
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"),
|
||||||
|
] satisfies RouteConfig;
|
||||||
13
frontend/app/routes/home.tsx
Normal file
|
|
@ -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 <Welcome />;
|
||||||
|
}
|
||||||
42
frontend/app/styles/components/buttons.css
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
6
frontend/app/styles/main.css
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
@import "./theme/borders.css";
|
||||||
|
@import "./theme/fonts.css";
|
||||||
|
@import "./components/buttons.css";
|
||||||
|
|
||||||
|
@import "./base/globals.css";
|
||||||
|
@import "./theme/colors.css";
|
||||||
10
frontend/app/styles/theme/colors.css
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
216
frontend/app/styles/theme/colors/text.css
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
93
frontend/app/styles/theme/fonts.css
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
20
frontend/app/types/DishType.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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 }[];
|
||||||
|
}
|
||||||
27
frontend/app/types/ScheduleType.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType";
|
||||||
|
import type { UserType } from "@/types/UserType";
|
||||||
|
|
||||||
|
export type RecurrenceType = {
|
||||||
|
type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
21
frontend/app/types/ScheduledUserDishType.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { UserType } from "@/types/UserType";
|
||||||
|
import type { DishType } from "@/types/DishType";
|
||||||
|
import type { RecurrenceType } from "@/types/ScheduleType";
|
||||||
|
|
||||||
|
export type UserDishType = {
|
||||||
|
id: number;
|
||||||
|
dish: DishType;
|
||||||
|
user: UserType;
|
||||||
|
recurrences: RecurrenceType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserDishWithoutUserType = {
|
||||||
|
id: number;
|
||||||
|
dish: DishType;
|
||||||
|
recurrences: RecurrenceType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScheduledUserDishType = {
|
||||||
|
id: number;
|
||||||
|
user_dish: UserDishType;
|
||||||
|
}
|
||||||
7
frontend/app/types/UserType.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { UserDishWithoutUserType } from "@/types/ScheduledUserDishType";
|
||||||
|
|
||||||
|
export type UserType = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
user_dishes: UserDishWithoutUserType[];
|
||||||
|
};
|
||||||
107
frontend/app/utils/api/apiRequest.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
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 = <TBody extends Record<string, unknown> | 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 = <TBody extends Record<string, unknown> | 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' });
|
||||||
|
};
|
||||||
23
frontend/app/welcome/logo-dark.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.061 58.1641C71.1037 58.1641 58.1677 71.0742 58.1677 86.9996C58.1677 102.925 71.1037 115.835 87.061 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="white"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="white"/>
|
||||||
|
<path d="M289.314 144.671C289.314 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.314 160.596 289.314 144.671Z" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_202_2131)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.385 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.385 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="white"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="white"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="white"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="white"/>
|
||||||
|
<path d="M547.32 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.365 2.95282 554.365 13.1239C554.365 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.317 21.6426C553.595 22.8372 554.365 23.2391 554.365 30.0273V31.5345H547.332H547.32ZM522.457 18.3601H547.32V7.88763H522.457V18.349V18.3601Z" fill="white"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="white"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.746L631.335 31.5345H624.58L637.006 4.75034C637.71 3.22078 639.262 2.23828 640.936 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.283 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="white"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="white"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.147V7.02795H752.025V31.5345H745.282Z" fill="white"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.675 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_202_2131">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6 KiB |
23
frontend/app/welcome/logo-light.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.0608 58.1641C71.1035 58.1641 58.1676 71.0742 58.1676 86.9996C58.1676 102.925 71.1035 115.835 87.0608 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="#121212"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="#121212"/>
|
||||||
|
<path d="M289.313 144.671C289.313 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.313 160.596 289.313 144.671Z" fill="#121212"/>
|
||||||
|
<g clip-path="url(#clip0_171_1761)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.386 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.386 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="#121212"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="#121212"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="#121212"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="#121212"/>
|
||||||
|
<path d="M547.321 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.366 2.95282 554.366 13.1239C554.366 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.318 21.6426C553.595 22.8372 554.366 23.2391 554.366 30.0273V31.5345H547.332H547.321ZM522.457 18.3601H547.321V7.88763H522.457V18.349V18.3601Z" fill="#121212"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="#121212"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.747L631.335 31.5345H624.58L637.007 4.75034C637.71 3.22078 639.262 2.23828 640.937 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.284 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="#121212"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="#121212"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.148V7.02795H752.026V31.5345H745.282Z" fill="#121212"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.676 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="#121212"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_171_1761">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6 KiB |
89
frontend/app/welcome/welcome.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import logoDark from "./logo-dark.svg";
|
||||||
|
import logoLight from "./logo-light.svg";
|
||||||
|
|
||||||
|
export function Welcome() {
|
||||||
|
return (
|
||||||
|
<main className="flex items-center justify-center pt-16 pb-4">
|
||||||
|
<div className="flex-1 flex flex-col items-center gap-16 min-h-0">
|
||||||
|
<header className="flex flex-col items-center gap-9">
|
||||||
|
<div className="w-[500px] max-w-[100vw] p-4">
|
||||||
|
<img
|
||||||
|
src={logoLight}
|
||||||
|
alt="React Router"
|
||||||
|
className="block w-full dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={logoDark}
|
||||||
|
alt="React Router"
|
||||||
|
className="hidden w-full dark:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="max-w-[300px] w-full space-y-6 px-4">
|
||||||
|
<nav className="rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4">
|
||||||
|
<p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
|
||||||
|
What's next?
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{resources.map(({ href, text, icon }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<a
|
||||||
|
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = [
|
||||||
|
{
|
||||||
|
href: "https://reactrouter.com/docs",
|
||||||
|
text: "React Router Docs",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "https://rmx.as/discord",
|
||||||
|
text: "Join Discord",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 20"
|
||||||
|
fill="none"
|
||||||
|
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
1
frontend/archive/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env.local
|
||||||
45
frontend/archive/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
/.idea
|
||||||
|
/.env.local
|
||||||
|
/package-lock.json
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
2
frontend/archive/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# DishPlanner Front End
|
||||||
|
|
||||||
33
frontend/archive/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "dish-planner",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"export": "next export"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
|
"next": "15.2.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.1.5",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |