Improve style consistency #5
4
frontend-react-router-backup/.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
6
frontend-react-router-backup/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
87
frontend-react-router-backup/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# 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.
|
||||||
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 6 KiB |
30
frontend-react-router-backup/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "my-react-router-app",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "react-router build",
|
||||||
|
"dev": "react-router dev",
|
||||||
|
"start": "react-router-serve ./build/server/index.js",
|
||||||
|
"typecheck": "react-router typegen && tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-router/node": "^7.5.3",
|
||||||
|
"@react-router/serve": "^7.5.3",
|
||||||
|
"isbot": "^5.1.27",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router": "^7.5.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.5.3",
|
||||||
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"tailwindcss": "^4.1.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^6.3.3",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
27
frontend-react-router-backup/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"**/.server/**/*",
|
||||||
|
"**/.client/**/*",
|
||||||
|
".react-router/types/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./app/*"]
|
||||||
|
},
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
.react-router
|
.env.local
|
||||||
build
|
|
||||||
node_modules
|
|
||||||
README.md
|
|
||||||
|
|
|
||||||
49
frontend/.gitignore
vendored
|
|
@ -1,6 +1,45 @@
|
||||||
.DS_Store
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
/node_modules/
|
|
||||||
|
|
||||||
# React Router
|
/.idea
|
||||||
/.react-router/
|
/.env.local
|
||||||
/build/
|
/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
|
||||||
|
|
|
||||||
289
frontend/COMPONENT_GUIDE.md
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Component Guide - Dish Planner
|
||||||
|
|
||||||
|
This guide documents the standardized UI components in the Dish Planner application. All components use Tailwind CSS with our custom color variables from the design system.
|
||||||
|
|
||||||
|
## Design System
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
Our color palette is defined in `src/styles/theme/colors/root.css` and integrated into Tailwind via `tailwind.config.ts`:
|
||||||
|
|
||||||
|
- **Primary (Rose)**: `bg-primary`, `text-primary`, `border-primary` with shades 50-950
|
||||||
|
- **Secondary (Deluge)**: `bg-secondary`, `text-secondary`, `border-secondary` with shades 50-950
|
||||||
|
- **Accent (Malibu Blue)**: `bg-accent`, `text-accent`, `border-accent` with shades 50-950
|
||||||
|
- **Yellow (Gamboge)**: `bg-yellow`, `text-yellow`, `border-yellow` with shades 50-950
|
||||||
|
- **Gray (Ebony Clay)**: `bg-gray`, `text-gray`, `border-gray` with shades 100-950
|
||||||
|
- **Semantic Colors**:
|
||||||
|
- Danger (Alizarin Crimson): `bg-danger`, `text-danger`, `border-danger`
|
||||||
|
- Success (Spring Green): `bg-success`, `text-success`, `border-success`
|
||||||
|
- Warning (Burning Orange): `bg-warning`, `text-warning`, `border-warning`
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Button (`src/components/ui/Button.tsx`)
|
||||||
|
|
||||||
|
Unified button component supporting multiple variants, appearances, and states.
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ButtonProps {
|
||||||
|
appearance?: 'solid' | 'outline' | 'text'; // Default: 'solid'
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent' | 'danger'; // Default: 'primary'
|
||||||
|
size?: 'small' | 'medium' | 'large'; // Default: 'medium'
|
||||||
|
type?: 'button' | 'submit' | 'reset'; // Default: 'button'
|
||||||
|
href?: string; // For link buttons
|
||||||
|
icon?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Solid primary button
|
||||||
|
<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
|
||||||
|
|
@ -1,87 +1,2 @@
|
||||||
# Welcome to React Router!
|
# DishPlanner Front End
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.env.local
|
|
||||||
45
frontend/archive/.gitignore
vendored
|
|
@ -1,45 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# DishPlanner Front End
|
|
||||||
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import React, {FC} from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
type: 'error' | 'warning' | 'info' | 'success';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Alert: FC<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
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import Link from "next/link";
|
|
||||||
import React, { FC, ReactElement, ReactNode } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
appearance?: 'solid' | 'outline' | 'text';
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
href?: string;
|
|
||||||
icon?: ReactNode;
|
|
||||||
onClick?: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
size?: 'small' | 'medium' | 'large';
|
|
||||||
type?: 'button' | 'submit' | 'reset';
|
|
||||||
variant?: 'primary' | 'secondary' | 'accent';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: FC<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
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import React, { FC } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
type: 'submit' | 'button';
|
|
||||||
}
|
|
||||||
|
|
||||||
const OutlineButton: FC<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
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React, { FC, ReactElement } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
href: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
variant?: "primary" | "secondary";
|
|
||||||
}
|
|
||||||
|
|
||||||
const OutlineLinkButton: FC<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} href={href}>
|
|
||||||
{iconElement}
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OutlineLinkButton
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import React, {FC} from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
type: 'submit' | 'button';
|
|
||||||
}
|
|
||||||
|
|
||||||
const SolidButton: FC<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
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import React, { FC, ReactElement } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
href: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
variant?: "primary" | "secondary";
|
|
||||||
}
|
|
||||||
|
|
||||||
const SolidLinkButton: FC<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} href={href}>
|
|
||||||
<div className="flex-grow"></div>
|
|
||||||
{iconElement}
|
|
||||||
{children}
|
|
||||||
<div className="flex-grow"></div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SolidLinkButton
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
.button-primary-solid {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: var(--color-secondary-200);
|
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: "Anta", serif;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 16px 2px 16px;
|
|
||||||
}
|
|
||||||
.button-primary-outline {
|
|
||||||
background-color: var(--color-background);
|
|
||||||
color: var(--color-primary);
|
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: "Anta", serif;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 16px 2px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-secondary-solid {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
border: 1px solid var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-accent-solid {
|
|
||||||
background-color: var(--color-accent-blue);
|
|
||||||
color: var(--color-secondary-900);
|
|
||||||
border: 1px solid var(--color-accent-blue);
|
|
||||||
}
|
|
||||||
.button-accent-outline {
|
|
||||||
background-color: var(--color-background);
|
|
||||||
color: var(--color-accent-blue);
|
|
||||||
border: 1px solid var(--color-accent-blue);
|
|
||||||
}
|
|
||||||
.button-accent-outline:hover {
|
|
||||||
background-color: var(--color-background-400);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import type { Config } from "tailwindcss";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
background: "var(--background)",
|
|
||||||
foreground: "var(--foreground)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
} satisfies Config;
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2017",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,33 @@
|
||||||
{
|
{
|
||||||
"name": "my-react-router-app",
|
"name": "dish-planner",
|
||||||
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-router build",
|
"dev": "next dev --turbopack",
|
||||||
"dev": "react-router dev",
|
"build": "next build",
|
||||||
"start": "react-router-serve ./build/server/index.js",
|
"start": "next start",
|
||||||
"typecheck": "react-router typegen && tsc"
|
"lint": "next lint",
|
||||||
|
"export": "next export"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "^7.5.3",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@react-router/serve": "^7.5.3",
|
"@heroicons/react": "^2.2.0",
|
||||||
"isbot": "^5.1.27",
|
"classnames": "^2.5.1",
|
||||||
"react": "^19.1.0",
|
"luxon": "^3.5.0",
|
||||||
"react-dom": "^19.1.0",
|
"next": "15.2.4",
|
||||||
"react-router": "^7.5.3"
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.5.3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19",
|
||||||
"tailwindcss": "^4.1.4",
|
"eslint": "^9",
|
||||||
"typescript": "^5.8.3",
|
"eslint-config-next": "15.1.5",
|
||||||
"vite": "^6.3.3",
|
"postcss": "^8",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"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 |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
|
|
@ -58,7 +58,7 @@ export default function EditDishPage({params}: { params: Promise<{ id: number }>
|
||||||
<div className="text-2xl">
|
<div className="text-2xl">
|
||||||
Are you sure you want to delete this dish?
|
Are you sure you want to delete this dish?
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2 background-secondary p-3">
|
<div className="my-2 bg-secondary p-3">
|
||||||
name: <strong className="text-lg">{name}</strong> <br/>
|
name: <strong className="text-lg">{name}</strong> <br/>
|
||||||
recurrence: <strong className="text-lg">{recurrence}</strong>
|
recurrence: <strong className="text-lg">{recurrence}</strong>
|
||||||
users: <strong className="text-lg">{users.map((user) => user.name).join(', ')}</strong>
|
users: <strong className="text-lg">{users.map((user) => user.name).join(', ')}</strong>
|
||||||
|
|
@ -66,7 +66,7 @@ export default function EditDishPage({params}: { params: Promise<{ id: number }>
|
||||||
|
|
||||||
<Link href={routes.dish.index()}>
|
<Link href={routes.dish.index()}>
|
||||||
<div
|
<div
|
||||||
className="w-full p-2 text-center mt-6 border-2 border-secondary background-foreground text-background text-bold my-2"
|
className="w-full p-2 text-center mt-6 border-2 border-secondary bg-gray-800 text-background text-bold my-2"
|
||||||
>
|
>
|
||||||
No, take me back
|
No, take me back
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -9,7 +9,7 @@ import {fetchDish} from "@/utils/api/dishApi";
|
||||||
import SyncUsersForm from "@/components/features/dishes/SyncUsersForm";
|
import SyncUsersForm from "@/components/features/dishes/SyncUsersForm";
|
||||||
import {ChevronLeftIcon} from "@heroicons/react/16/solid";
|
import {ChevronLeftIcon} from "@heroicons/react/16/solid";
|
||||||
import useRoutes from "@/hooks/useRoutes";
|
import useRoutes from "@/hooks/useRoutes";
|
||||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
import Button from "@/components/ui/Button";
|
||||||
import Hr from "@/components/ui/Hr"
|
import Hr from "@/components/ui/Hr"
|
||||||
|
|
||||||
export default function EditDishPage({ params }: { params: Promise<{ id: number }> }) {
|
export default function EditDishPage({ params }: { params: Promise<{ id: number }> }) {
|
||||||
|
|
@ -43,10 +43,9 @@ export default function EditDishPage({ params }: { params: Promise<{ id: number
|
||||||
<div>
|
<div>
|
||||||
<div className="flex mb-3">
|
<div className="flex mb-3">
|
||||||
<PageTitle>Edit Dish</PageTitle>
|
<PageTitle>Edit Dish</PageTitle>
|
||||||
<OutlineLinkButton href={routes.dish.index()}>
|
<Button appearance="outline" href={routes.dish.index()} icon={<ChevronLeftIcon />}>
|
||||||
<ChevronLeftIcon className="w-5 h-5 mr-1" />
|
|
||||||
<p className="text-sm">BACK</p>
|
<p className="text-sm">BACK</p>
|
||||||
</OutlineLinkButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditDishForm dish={dish} />
|
<EditDishForm dish={dish} />
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -7,7 +7,8 @@ import {useState} from "react";
|
||||||
import Alert from "@/components/ui/Alert";
|
import Alert from "@/components/ui/Alert";
|
||||||
import {createUser} from "@/utils/api/usersApi";
|
import {createUser} from "@/utils/api/usersApi";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const CreateUsersPage = () => {
|
const CreateUsersPage = () => {
|
||||||
|
|
@ -40,18 +41,18 @@ const CreateUsersPage = () => {
|
||||||
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<label htmlFor="name">Name</label>
|
<Input
|
||||||
<input type="text"
|
label="Name"
|
||||||
placeholder=""
|
type="text"
|
||||||
name="name"
|
placeholder=""
|
||||||
id="name"
|
name="name"
|
||||||
autoFocus={true}
|
id="name"
|
||||||
value={name}
|
autoFocus={true}
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={name}
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SolidButton type="submit" className="mt-4">Create</SolidButton>
|
<Button type="submit" appearance="solid" variant="primary" className="mt-4">Create</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -10,7 +10,7 @@ import React from "react";
|
||||||
import {deleteUser} from "@/utils/api/usersApi";
|
import {deleteUser} from "@/utils/api/usersApi";
|
||||||
import {UserType} from "@/types/UserType";
|
import {UserType} from "@/types/UserType";
|
||||||
import Card from "@/components/layout/Card";
|
import Card from "@/components/layout/Card";
|
||||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
const UsersPage = () => {
|
const UsersPage = () => {
|
||||||
const { users, isLoading } = useFetchUsers();
|
const { users, isLoading } = useFetchUsers();
|
||||||
|
|
@ -59,10 +59,9 @@ const UsersPage = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-grow flex justify-end">
|
<div className="flex-grow flex justify-end">
|
||||||
<OutlineLinkButton href={routes.user.create()} variant="primary">
|
<Button href={routes.user.create()} variant="primary" appearance="outline" icon={<PlusIcon />}>
|
||||||
<PlusIcon className="w-4 h-4 mt-1 mr-1"/>
|
Add User
|
||||||
<p>Add User</p>
|
</Button>
|
||||||
</OutlineLinkButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -6,9 +6,10 @@ import { login } from "@/utils/api/auth";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useRoutes from "@/hooks/useRoutes";
|
import useRoutes from "@/hooks/useRoutes";
|
||||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import Alert from "@/components/ui/Alert";
|
import Alert from "@/components/ui/Alert";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const { login: authLogin } = useAuth();
|
const { login: authLogin } = useAuth();
|
||||||
|
|
@ -70,23 +71,23 @@ export default function LoginForm() {
|
||||||
}
|
}
|
||||||
<form onSubmit={handleSubmit} className="max-w-sm mx-auto flex flex-col">
|
<form onSubmit={handleSubmit} className="max-w-sm mx-auto flex flex-col">
|
||||||
{error && <p className="text-red-600 mb-4">{error}</p>}
|
{error && <p className="text-red-600 mb-4">{error}</p>}
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
className="mb-4"
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
className="mb-4"
|
||||||
/>
|
/>
|
||||||
<SolidButton type="submit">Login</SolidButton>
|
<Button type="submit" appearance="solid" variant="primary">Login</Button>
|
||||||
<Link href={routes.auth.register()} className="lowercase hover:underline text-center mt-1 text-secondary underline">
|
<Link href={routes.auth.register()} className="lowercase hover:underline text-center mt-1 text-secondary underline">
|
||||||
Create an account
|
Create an account
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -6,7 +6,8 @@ import { useRouter } from 'next/navigation';
|
||||||
import useRoutes from "@/hooks/useRoutes";
|
import useRoutes from "@/hooks/useRoutes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import SectionTitle from "@/components/ui/SectionTitle";
|
import SectionTitle from "@/components/ui/SectionTitle";
|
||||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -57,44 +58,42 @@ export default function LoginForm() {
|
||||||
<form onSubmit={ handleSubmit } className="max-w-sm mx-auto space-y-4 flex flex-col">
|
<form onSubmit={ handleSubmit } className="max-w-sm mx-auto space-y-4 flex flex-col">
|
||||||
<h2 className="text-xl font-bold text-secondary">Register</h2>
|
<h2 className="text-xl font-bold text-secondary">Register</h2>
|
||||||
{ error && <p className="text-red-600">{ error }</p> }
|
{ error && <p className="text-red-600">{ error }</p> }
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
value={ name }
|
value={ name }
|
||||||
onChange={ e => setName(e.target.value) }
|
onChange={ e => setName(e.target.value) }
|
||||||
required
|
required
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
value={ email }
|
value={ email }
|
||||||
onChange={ e => setEmail(e.target.value) }
|
onChange={ e => setEmail(e.target.value) }
|
||||||
required
|
required
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={ password }
|
value={ password }
|
||||||
onChange={ e => setPassword(e.target.value) }
|
onChange={ e => setPassword(e.target.value) }
|
||||||
required
|
required
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password Again"
|
placeholder="Password Again"
|
||||||
value={ passwordAgain }
|
value={ passwordAgain }
|
||||||
onChange={ e => setPasswordAgain(e.target.value) }
|
onChange={ e => setPasswordAgain(e.target.value) }
|
||||||
required
|
required
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
|
||||||
/>
|
/>
|
||||||
<SolidButton
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={ isLoading ? "opacity-50 cursor-not-allowed" : "background-red" }
|
appearance="solid"
|
||||||
|
variant="primary"
|
||||||
|
className={ isLoading ? "opacity-50 cursor-not-allowed" : "bg-primary" }
|
||||||
>
|
>
|
||||||
Create Account
|
Create Account
|
||||||
</SolidButton>
|
</Button>
|
||||||
<Link href={routes.auth.login()} className="lowercase hover:underline text-center text-secondary underline">
|
<Link href={routes.auth.login()} className="lowercase hover:underline text-center text-secondary underline">
|
||||||
Back to Login
|
Back to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -4,8 +4,7 @@ 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 OutlineButton from "@/components/ui/Buttons/OutlineButton";
|
import Button from "@/components/ui/Button";
|
||||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dish: DishType;
|
dish: DishType;
|
||||||
|
|
@ -54,14 +53,15 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OutlineButton
|
<Button
|
||||||
|
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
|
||||||
</OutlineButton>
|
</Button>
|
||||||
|
|
||||||
{ 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 background-secondary border-secondary"
|
className="p-2 border rounded w-full bg-secondary border-secondary"
|
||||||
>
|
>
|
||||||
<option value="-1">Select user</option>
|
<option value="-1">Select user</option>
|
||||||
{remainingUsers.map((user: UserType) => (
|
{remainingUsers.map((user: UserType) => (
|
||||||
|
|
@ -83,12 +83,14 @@ 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">
|
||||||
<SolidButton type="submit"
|
<Button type="submit"
|
||||||
|
appearance="solid"
|
||||||
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
>
|
>
|
||||||
Add User
|
Add User
|
||||||
</SolidButton>
|
</Button>
|
||||||
</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 SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
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,37 +55,38 @@ const CreateDishForm = () => {
|
||||||
<Alert type="error">{ error }</Alert>
|
<Alert type="error">{ error }</Alert>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<label htmlFor="name" className="block text-sm font-medium">Dish Name</label>
|
label="Dish Name"
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
id="name"
|
||||||
id="name"
|
name="name"
|
||||||
name="name"
|
value={ name }
|
||||||
value={ name }
|
onChange={ (e) => setName(e.target.value) }
|
||||||
onChange={ (e) => setName(e.target.value) } // Update the name state on change
|
placeholder="Enter dish name"
|
||||||
className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900"
|
className="mb-4"
|
||||||
placeholder="Enter dish name"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SolidButton
|
<Button
|
||||||
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" }
|
||||||
</SolidButton>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Hr />
|
<Hr />
|
||||||
|
|
||||||
<OutlineLinkButton
|
<Button
|
||||||
|
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
|
||||||
</OutlineLinkButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -5,7 +5,8 @@ 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
|
||||||
|
|
@ -57,20 +58,15 @@ const EditDishForm: FC<Props> = ({ dish }) => {
|
||||||
error != '' && <Alert type="error" >{ error }</Alert>
|
error != '' && <Alert type="error" >{ error }</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* Dish name input */}
|
<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}
|
/>
|
||||||
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,7 +4,8 @@ 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 SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userDish: UserDishType
|
userDish: UserDishType
|
||||||
|
|
@ -74,7 +75,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="background-secondary border-secondary border-2">
|
<select value={weekday} onChange={(e) => setWeekday(parseInt(e.currentTarget.value))} className="bg-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>
|
||||||
|
|
@ -104,14 +105,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="background-secondary border-secondary border-2 w-12 px-2" />
|
<Input type="number" value={minimumValue} onChange={(e) => setMinimumValue(parseInt(e.currentTarget.value))} min="0" max="365" className="w-12 px-2" fullWidth={false} />
|
||||||
<label htmlFor="minimum">Days</label>
|
<label htmlFor="minimum">Days</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SolidButton type="submit">Save</SolidButton>
|
<Button type="submit" appearance="solid" variant="primary">Save</Button>
|
||||||
</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:background-secondary', 'pb-2', 'pl-5',
|
'text-primary', 'hover:bg-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 font-size-24 capitalize font-default">{ transformDate(schedule.date) }</SectionTitle>
|
<SectionTitle className="text-right text-2xl 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 font-size-18 pt-2">{ scheduleData.user.name }</div>
|
<div className="flex-none px-2 text-lg 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"
|
||||||
|
|
@ -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 SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
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-blue">
|
<DialogTitle as="h3" className="text-base font-semibold text-accent">
|
||||||
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-blue">Overwrite current schedule</label>
|
<label htmlFor="overwrite" className="text-accent">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,21 +56,24 @@ 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">
|
||||||
<SolidButton
|
<Button
|
||||||
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
|
||||||
</SolidButton>
|
</Button>
|
||||||
<SolidButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
data-autofocus
|
appearance="solid"
|
||||||
|
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
|
||||||
</SolidButton>
|
</Button>
|
||||||
</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', 'background-secondary',
|
'p-2', 'rounded', 'w-full', 'bg-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-blue': isToday,
|
'text-accent': 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-blue border-accent-blue': isToday,
|
'border-2 text-accent border-accent': isToday,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,9 +22,9 @@ const ScheduleDayCardUserDish: FC<Props> = ({ schedule, user }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex background-secondary pb-1 rounded-2xl text-xl" key={ `user-${ user.id }` }>
|
<div className="w-full flex bg-secondary pb-1 rounded-2xl text-xl" key={ `user-${ user.id }` }>
|
||||||
<div className="w-1/3 font-size-16 text-right pr-3">{ user.name } : </div>
|
<div className="w-1/3 text-base text-right pr-3">{ user.name } : </div>
|
||||||
<div className="w-2/3 font-size-16">{ getDish(user) }</div>
|
<div className="w-2/3 text-base">{ getDish(user) }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -6,7 +6,8 @@ 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 SolidButton from "@/components/ui/Buttons/SolidButton";
|
import Button from "@/components/ui/Button";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
|
|
@ -44,17 +45,17 @@ const EditUserForm: FC<Props> = ({ user }) => {
|
||||||
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<label htmlFor="name">Name</label>
|
<Input
|
||||||
<input type="text"
|
label="Name"
|
||||||
placeholder=""
|
type="text"
|
||||||
name="name"
|
placeholder=""
|
||||||
id="name"
|
name="name"
|
||||||
value={name}
|
id="name"
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={name}
|
||||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SolidButton type="submit" className="mt-4">Update</SolidButton>
|
<Button type="submit" appearance="solid" variant="primary" className="mt-4">Update</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
22
frontend/src/components/layout/Card.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React, {FC} from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card: FC<Props> = ({ children, className }) => {
|
||||||
|
const styles = classNames(
|
||||||
|
"w-full border-2 border-secondary p-4 my-2 rounded-lg flex bg-gray-700",
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles}>
|
||||||
|
{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-blue hover:background-secondary">
|
<Link href={routes.home()} className="text-accent hover:bg-secondary">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.dish.index()} className="text-accent-blue hover:background-secondary">
|
<Link href={routes.dish.index()} className="text-accent hover:bg-secondary">
|
||||||
Dishes
|
Dishes
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.user.index()} className="text-accent-blue hover:background-secondary">
|
<Link href={routes.user.index()} className="text-accent hover:bg-secondary">
|
||||||
Users
|
Users
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={routes.schedule.history()} className="text-accent-blue hover:background-secondary">
|
<Link href={routes.schedule.history()} className="text-accent hover:bg-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:background-secondary">
|
className="text-primary text-right hover:bg-secondary">
|
||||||
Logout
|
Logout
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
29
frontend/src/components/ui/Alert.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, {FC} from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
type: 'error' | 'warning' | 'info' | 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Alert: FC<Props> = ({ children, className, type } ) => {
|
||||||
|
const styles = classNames(
|
||||||
|
"px-4 py-3 rounded-lg border-2",
|
||||||
|
{
|
||||||
|
'bg-danger-50 border-danger text-danger-800': type === 'error',
|
||||||
|
'bg-warning-50 border-warning text-warning-800': type === 'warning',
|
||||||
|
'bg-accent-50 border-accent text-accent-800': type === 'info',
|
||||||
|
'bg-success-50 border-success text-success-800': type === 'success',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles} role="alert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alert;
|
||||||
118
frontend/src/components/ui/Button.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, { FC, ReactElement, ReactNode } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
appearance?: 'solid' | 'outline' | 'text';
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
href?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent' | 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: FC<ButtonProps> = ({
|
||||||
|
appearance = 'solid',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
href,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
size = 'medium',
|
||||||
|
type = 'button',
|
||||||
|
variant = 'primary'
|
||||||
|
}) => {
|
||||||
|
const baseStyles = "inline-flex items-center justify-center gap-2 rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
|
|
||||||
|
// Size styles
|
||||||
|
const sizeStyles = classNames({
|
||||||
|
"text-sm py-1.5 px-3": size === "small",
|
||||||
|
"text-base py-2 px-4": size === "medium",
|
||||||
|
"text-lg py-3 px-6": size === "large",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Variant + Appearance styles
|
||||||
|
const variantStyles = classNames({
|
||||||
|
// Primary Solid
|
||||||
|
'bg-primary text-white border-2 border-primary hover:bg-primary-600 focus:ring-primary-500':
|
||||||
|
variant === 'primary' && appearance === 'solid',
|
||||||
|
// Primary Outline
|
||||||
|
'bg-transparent text-primary border-2 border-primary hover:bg-primary-50 focus:ring-primary-500':
|
||||||
|
variant === 'primary' && appearance === 'outline',
|
||||||
|
// Primary Text
|
||||||
|
'bg-transparent text-primary border-2 border-transparent hover:bg-primary-50 focus:ring-primary-500':
|
||||||
|
variant === 'primary' && appearance === 'text',
|
||||||
|
|
||||||
|
// Secondary Solid
|
||||||
|
'bg-secondary text-white border-2 border-secondary hover:bg-secondary-600 focus:ring-secondary-500':
|
||||||
|
variant === 'secondary' && appearance === 'solid',
|
||||||
|
// Secondary Outline
|
||||||
|
'bg-transparent text-secondary border-2 border-secondary hover:bg-secondary-50 focus:ring-secondary-500':
|
||||||
|
variant === 'secondary' && appearance === 'outline',
|
||||||
|
// Secondary Text
|
||||||
|
'bg-transparent text-secondary border-2 border-transparent hover:bg-secondary-50 focus:ring-secondary-500':
|
||||||
|
variant === 'secondary' && appearance === 'text',
|
||||||
|
|
||||||
|
// Accent Solid
|
||||||
|
'bg-accent text-white border-2 border-accent hover:bg-accent-600 focus:ring-accent-500':
|
||||||
|
variant === 'accent' && appearance === 'solid',
|
||||||
|
// Accent Outline
|
||||||
|
'bg-transparent text-accent border-2 border-accent hover:bg-accent-50 focus:ring-accent-500':
|
||||||
|
variant === 'accent' && appearance === 'outline',
|
||||||
|
// Accent Text
|
||||||
|
'bg-transparent text-accent border-2 border-transparent hover:bg-accent-50 focus:ring-accent-500':
|
||||||
|
variant === 'accent' && appearance === 'text',
|
||||||
|
|
||||||
|
// Danger Solid
|
||||||
|
'bg-danger text-white border-2 border-danger hover:bg-danger-600 focus:ring-danger-500':
|
||||||
|
variant === 'danger' && appearance === 'solid',
|
||||||
|
// Danger Outline
|
||||||
|
'bg-transparent text-danger border-2 border-danger hover:bg-danger-50 focus:ring-danger-500':
|
||||||
|
variant === 'danger' && appearance === 'outline',
|
||||||
|
// Danger Text
|
||||||
|
'bg-transparent text-danger border-2 border-transparent hover:bg-danger-50 focus:ring-danger-500':
|
||||||
|
variant === 'danger' && appearance === 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = classNames(baseStyles, sizeStyles, variantStyles, className);
|
||||||
|
|
||||||
|
const iconClassNames = classNames({
|
||||||
|
"h-4 w-4": size === "small",
|
||||||
|
"h-5 w-5": size === "medium",
|
||||||
|
"h-6 w-6": size === "large",
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconElement = icon && React.isValidElement(icon)
|
||||||
|
? React.cloneElement(icon as ReactElement<{ className?: string }>, {
|
||||||
|
className: iconClassNames,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (href !== undefined) {
|
||||||
|
return (
|
||||||
|
<Link href={href} className={styles}>
|
||||||
|
{iconElement}
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={styles}
|
||||||
|
disabled={disabled}
|
||||||
|
type={type}
|
||||||
|
>
|
||||||
|
{iconElement}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
64
frontend/src/components/ui/Checkbox.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { FC, InputHTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
helperText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Checkbox: FC<CheckboxProps> = ({
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
helperText,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const checkboxId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
|
||||||
|
const checkboxStyles = classNames(
|
||||||
|
"h-5 w-5 rounded border-2 transition-colors cursor-pointer",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-offset-1",
|
||||||
|
{
|
||||||
|
"border-secondary text-primary focus:ring-primary-500": !error,
|
||||||
|
"border-danger text-danger focus:ring-danger-500": error,
|
||||||
|
"opacity-50 cursor-not-allowed": props.disabled,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelStyles = classNames(
|
||||||
|
"ml-2 text-sm font-medium cursor-pointer",
|
||||||
|
{
|
||||||
|
"text-secondary": !error,
|
||||||
|
"text-danger": error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={checkboxId}
|
||||||
|
className={checkboxStyles}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={checkboxId} className={labelStyles}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="mt-1 text-sm text-danger">{error}</p>
|
||||||
|
)}
|
||||||
|
{helperText && !error && (
|
||||||
|
<p className="mt-1 text-sm text-gray-400">{helperText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Checkbox;
|
||||||
|
|
@ -7,7 +7,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Description = ({ children, className }: Props) => {
|
const Description = ({ children, className }: Props) => {
|
||||||
const style = classNames("italic font-size-16",
|
const style = classNames("italic text-base",
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
68
frontend/src/components/ui/Input.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { FC, InputHTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
helperText?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: FC<InputProps> = ({
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
helperText,
|
||||||
|
fullWidth = true,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
required,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
|
||||||
|
const inputStyles = classNames(
|
||||||
|
"px-3 py-2 rounded border-2 transition-colors",
|
||||||
|
"bg-gray-600 text-secondary",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-offset-1",
|
||||||
|
{
|
||||||
|
"border-secondary focus:border-primary focus:ring-primary-500": !error,
|
||||||
|
"border-danger focus:border-danger focus:ring-danger-500": error,
|
||||||
|
"w-full": fullWidth,
|
||||||
|
"opacity-50 cursor-not-allowed": props.disabled,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelStyles = classNames(
|
||||||
|
"block text-sm font-medium mb-1",
|
||||||
|
{
|
||||||
|
"text-secondary": !error,
|
||||||
|
"text-danger": error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={fullWidth ? "w-full" : ""}>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={inputId} className={labelStyles}>
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-danger ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
id={inputId}
|
||||||
|
className={inputStyles}
|
||||||
|
required={required}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="mt-1 text-sm text-danger">{error}</p>
|
||||||
|
)}
|
||||||
|
{helperText && !error && (
|
||||||
|
<p className="mt-1 text-sm text-gray-400">{helperText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
||||||