Improve style consistency (#5)
Reviewed-on: https://codeberg.org/lvl0/dish-planner/pulls/5 Co-authored-by: myrmidex <myrmidex@myrmidex.net> Co-committed-by: myrmidex <myrmidex@myrmidex.net>
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
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
.env.local
|
||||
|
|
|
|||
49
frontend/.gitignore
vendored
|
|
@ -1,6 +1,45 @@
|
|||
.DS_Store
|
||||
/node_modules/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
/.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
|
||||
|
|
|
|||
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,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next export"
|
||||
},
|
||||
"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"
|
||||
"@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": {
|
||||
"@react-router/dev": "^7.5.3",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@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"
|
||||
"@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 |
|
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">
|
||||
Are you sure you want to delete this dish?
|
||||
</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/>
|
||||
recurrence: <strong className="text-lg">{recurrence}</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()}>
|
||||
<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
|
||||
</div>
|
||||
|
|
@ -9,7 +9,7 @@ import {fetchDish} from "@/utils/api/dishApi";
|
|||
import SyncUsersForm from "@/components/features/dishes/SyncUsersForm";
|
||||
import {ChevronLeftIcon} from "@heroicons/react/16/solid";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Hr from "@/components/ui/Hr"
|
||||
|
||||
export default function EditDishPage({ params }: { params: Promise<{ id: number }> }) {
|
||||
|
|
@ -43,10 +43,9 @@ export default function EditDishPage({ params }: { params: Promise<{ id: number
|
|||
<div>
|
||||
<div className="flex mb-3">
|
||||
<PageTitle>Edit Dish</PageTitle>
|
||||
<OutlineLinkButton href={routes.dish.index()}>
|
||||
<ChevronLeftIcon className="w-5 h-5 mr-1" />
|
||||
<Button appearance="outline" href={routes.dish.index()} icon={<ChevronLeftIcon />}>
|
||||
<p className="text-sm">BACK</p>
|
||||
</OutlineLinkButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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 {createUser} from "@/utils/api/usersApi";
|
||||
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';
|
||||
|
||||
const CreateUsersPage = () => {
|
||||
|
|
@ -40,18 +41,18 @@ const CreateUsersPage = () => {
|
|||
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
||||
}
|
||||
|
||||
<label htmlFor="name">Name</label>
|
||||
<input type="text"
|
||||
placeholder=""
|
||||
name="name"
|
||||
id="name"
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
<Input
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder=""
|
||||
name="name"
|
||||
id="name"
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -10,7 +10,7 @@ import React from "react";
|
|||
import {deleteUser} from "@/utils/api/usersApi";
|
||||
import {UserType} from "@/types/UserType";
|
||||
import Card from "@/components/layout/Card";
|
||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
const UsersPage = () => {
|
||||
const { users, isLoading } = useFetchUsers();
|
||||
|
|
@ -59,10 +59,9 @@ const UsersPage = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex-grow flex justify-end">
|
||||
<OutlineLinkButton href={routes.user.create()} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mt-1 mr-1"/>
|
||||
<p>Add User</p>
|
||||
</OutlineLinkButton>
|
||||
<Button href={routes.user.create()} variant="primary" appearance="outline" icon={<PlusIcon />}>
|
||||
Add User
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -6,9 +6,10 @@ import { login } from "@/utils/api/auth";
|
|||
import { useRouter } from 'next/navigation';
|
||||
import Link from "next/link";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import Input from "@/components/ui/Input";
|
||||
|
||||
export default function LoginForm() {
|
||||
const { login: authLogin } = useAuth();
|
||||
|
|
@ -70,23 +71,23 @@ export default function LoginForm() {
|
|||
}
|
||||
<form onSubmit={handleSubmit} className="max-w-sm mx-auto flex flex-col">
|
||||
{error && <p className="text-red-600 mb-4">{error}</p>}
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
className="mb-4"
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full p-2 mb-4 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
className="mb-4"
|
||||
/>
|
||||
<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">
|
||||
Create an account
|
||||
</Link>
|
||||
|
|
@ -6,7 +6,8 @@ import { useRouter } from 'next/navigation';
|
|||
import useRoutes from "@/hooks/useRoutes";
|
||||
import Link from "next/link";
|
||||
import SectionTitle from "@/components/ui/SectionTitle";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Input from "@/components/ui/Input";
|
||||
|
||||
export default function LoginForm() {
|
||||
const router = useRouter();
|
||||
|
|
@ -57,44 +58,42 @@ export default function LoginForm() {
|
|||
<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>
|
||||
{ error && <p className="text-red-600">{ error }</p> }
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={ name }
|
||||
onChange={ e => setName(e.target.value) }
|
||||
required
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={ email }
|
||||
onChange={ e => setEmail(e.target.value) }
|
||||
required
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={ password }
|
||||
onChange={ e => setPassword(e.target.value) }
|
||||
required
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password Again"
|
||||
value={ passwordAgain }
|
||||
onChange={ e => setPasswordAgain(e.target.value) }
|
||||
required
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
/>
|
||||
<SolidButton
|
||||
<Button
|
||||
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
|
||||
</SolidButton>
|
||||
</Button>
|
||||
<Link href={routes.auth.login()} className="lowercase hover:underline text-center text-secondary underline">
|
||||
Back to Login
|
||||
</Link>
|
||||
|
|
@ -4,8 +4,7 @@ import { UserType } from "@/types/UserType";
|
|||
import { useFetchUsers } from "@/hooks/useFetchUsers";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import {addUserToDish} from "@/utils/api/dishApi";
|
||||
import OutlineButton from "@/components/ui/Buttons/OutlineButton";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
interface Props {
|
||||
dish: DishType;
|
||||
|
|
@ -54,14 +53,15 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<OutlineButton
|
||||
<Button
|
||||
appearance="outline"
|
||||
className="mb-2 flex-none w-fit ml-auto"
|
||||
onClick={() => setShowAdd(!showAdd)}
|
||||
disabled={remainingUsers.length === 0}
|
||||
type="button"
|
||||
>
|
||||
Add User
|
||||
</OutlineButton>
|
||||
</Button>
|
||||
|
||||
{ showAdd && (
|
||||
<div className="mt-2 mb-5 w-full">
|
||||
|
|
@ -71,7 +71,7 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
|||
<select
|
||||
value={selectedUser}
|
||||
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>
|
||||
{remainingUsers.map((user: UserType) => (
|
||||
|
|
@ -83,12 +83,14 @@ const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
|||
</div>
|
||||
|
||||
<div className="flex-none py-2 pr-2 pl-4">
|
||||
<SolidButton type="submit"
|
||||
<Button type="submit"
|
||||
appearance="solid"
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="mt-1"
|
||||
>
|
||||
Add User
|
||||
</SolidButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -3,10 +3,10 @@ import { useRouter } from "next/navigation";
|
|||
import { createDish } from "@/utils/api/dishApi";
|
||||
import PageTitle from "@/components/ui/PageTitle";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
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 router = useRouter()
|
||||
|
|
@ -55,37 +55,38 @@ const CreateDishForm = () => {
|
|||
<Alert type="error">{ error }</Alert>
|
||||
) }
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium">Dish Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={ name }
|
||||
onChange={ (e) => setName(e.target.value) } // Update the name state on change
|
||||
className="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-secondary focus:bg-gray-900"
|
||||
placeholder="Enter dish name"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Dish Name"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={ name }
|
||||
onChange={ (e) => setName(e.target.value) }
|
||||
placeholder="Enter dish name"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<SolidButton
|
||||
<Button
|
||||
type="submit"
|
||||
appearance="solid"
|
||||
variant="primary"
|
||||
disabled={ loading }
|
||||
className={ loading ? "bg-gray-400" : '' }
|
||||
>
|
||||
{ loading ? "Saving..." : "Save Changes" }
|
||||
</SolidButton>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Hr />
|
||||
|
||||
<OutlineLinkButton
|
||||
<Button
|
||||
appearance="outline"
|
||||
href="/dishes"
|
||||
className="mt-4 pl-0 mr-0"
|
||||
icon={ <ChevronLeftIcon/> }
|
||||
>
|
||||
Back to dishes
|
||||
</OutlineLinkButton>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -5,7 +5,8 @@ import {updateDish} from "@/utils/api/dishApi";
|
|||
import {DishType} from "@/types/DishType";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import Button from "@/components/ui/Button"
|
||||
import Button from "@/components/ui/Button";
|
||||
import Input from "@/components/ui/Input";
|
||||
|
||||
interface Props {
|
||||
dish: DishType
|
||||
|
|
@ -57,20 +58,15 @@ const EditDishForm: FC<Props> = ({ dish }) => {
|
|||
error != '' && <Alert type="error" >{ error }</Alert>
|
||||
}
|
||||
|
||||
{/* Dish name input */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium">Dish Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
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>
|
||||
<Input
|
||||
label="Dish Name"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Save button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
|
|
@ -4,7 +4,8 @@ import {syncUserDishRecurrences} from "@/utils/api/usersApi";
|
|||
import Spinner from "@/components/Spinner";
|
||||
import {UserDishType} from "@/types/ScheduledUserDishType";
|
||||
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 {
|
||||
userDish: UserDishType
|
||||
|
|
@ -74,7 +75,7 @@ const EditDishUserCardEditForm: FC<Props> = ({ userDish, onSubmit}) => {
|
|||
isWeeklyOn && (
|
||||
<div className="flex-grow ml-2">
|
||||
<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="1">Monday</option>
|
||||
<option value="2">Tuesday</option>
|
||||
|
|
@ -104,14 +105,14 @@ const EditDishUserCardEditForm: FC<Props> = ({ userDish, onSubmit}) => {
|
|||
{
|
||||
isMinimumOn && (
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<SolidButton type="submit">Save</SolidButton>
|
||||
<Button type="submit" appearance="solid" variant="primary">Save</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ const divStyles = classNames(
|
|||
|
||||
const linkStyles = classNames(
|
||||
'border-b-2', 'border-secondary', 'uppercase',
|
||||
'text-primary', 'hover:background-secondary', 'pb-2', 'pl-5',
|
||||
'text-primary', 'hover:bg-secondary', 'pb-2', 'pl-5',
|
||||
'space-grotesk', 'text-xl'
|
||||
)
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ const ScheduleEditForm: FC<Props> = ({ date }) => {
|
|||
<PageTitle>Edit Day</PageTitle>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ const ScheduleEditForm: FC<Props> = ({ date }) => {
|
|||
{
|
||||
scheduleData
|
||||
.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">
|
||||
<select
|
||||
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 {generateSchedule} from "@/utils/api/scheduleApi";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
interface ScheduleRegenerateFormProps {
|
||||
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="sm:flex sm:items-start w-full">
|
||||
<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
|
||||
</DialogTitle>
|
||||
<div className="mt-5">
|
||||
|
|
@ -43,7 +43,7 @@ const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) =
|
|||
error && <Alert type="error">{ error }</Alert>
|
||||
}
|
||||
<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 className="flex-1/2">
|
||||
<Toggle onChange={handleToggle} checked={overwrite} />
|
||||
|
|
@ -56,21 +56,24 @@ const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) =
|
|||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-500 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<SolidButton
|
||||
<Button
|
||||
type="button"
|
||||
appearance="solid"
|
||||
variant="primary"
|
||||
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"
|
||||
>
|
||||
Regenerate
|
||||
</SolidButton>
|
||||
<SolidButton
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
data-autofocus
|
||||
appearance="solid"
|
||||
variant="secondary"
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</SolidButton>
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ const UserDishEditCard: FC<Props> = ({ scheduledUserDish, allDishes }) => {
|
|||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
||||
const selectStyle = classNames(
|
||||
'p-2', 'rounded', 'w-full', 'background-secondary',
|
||||
'p-2', 'rounded', 'w-full', 'bg-secondary',
|
||||
'focus:outline-none',
|
||||
'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 textStyle = classNames("inline font-bold", {
|
||||
'text-accent-blue': isToday,
|
||||
'text-accent': isToday,
|
||||
'text-secondary': !isToday,
|
||||
}, className)
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ const ScheduleDayCard: FC<Props> = ({schedule, users}) => {
|
|||
|
||||
const containerStyles = classNames(
|
||||
'w-full bg-gray-500 pt-5 pb-2 rounded-2xl text-xl', {
|
||||
'border-2 text-accent-blue border-accent-blue': isToday,
|
||||
'border-2 text-accent border-accent': isToday,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -22,9 +22,9 @@ const ScheduleDayCardUserDish: FC<Props> = ({ schedule, user }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex background-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-2/3 font-size-16">{ getDish(user) }</div>
|
||||
<div className="w-full flex bg-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-2/3 text-base">{ getDish(user) }</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,7 +6,8 @@ import PageTitle from "@/components/ui/PageTitle";
|
|||
import Link from "next/link";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
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 {
|
||||
user: UserType;
|
||||
|
|
@ -44,17 +45,17 @@ const EditUserForm: FC<Props> = ({ user }) => {
|
|||
error != '' && <Alert type="error" className="mt-4">{ error }</Alert>
|
||||
}
|
||||
|
||||
<label htmlFor="name">Name</label>
|
||||
<input type="text"
|
||||
placeholder=""
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
<Input
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder=""
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
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>
|
||||
</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 */}
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</Link>
|
||||
<Link href={routes.auth.login()}
|
||||
onClick={handleLogout}
|
||||
className="text-primary text-right hover:background-secondary">
|
||||
className="text-primary text-right hover:bg-secondary">
|
||||
Logout
|
||||
</Link>
|
||||
</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 style = classNames("italic font-size-16",
|
||||
const style = classNames("italic text-base",
|
||||
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;
|
||||