Improve style consistency #5

Merged
myrmidex merged 1 commit from refs/pull/5/head into main 2025-10-13 17:39:01 +02:00
137 changed files with 1264 additions and 750 deletions

View file

@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

View file

@ -0,0 +1,6 @@
.DS_Store
/node_modules/
# React Router
/.react-router/
/build/

View file

@ -0,0 +1,87 @@
# Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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.

View file

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

View file

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

View 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"
}
}

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View 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
}
}

View file

@ -1,4 +1 @@
.react-router
build
node_modules
README.md
.env.local

49
frontend/.gitignore vendored
View file

@ -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
View 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

View file

@ -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.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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.

View file

@ -1 +0,0 @@
.env.local

View file

@ -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

View file

@ -1,2 +0,0 @@
# DishPlanner Front End

View file

@ -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"
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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"]
}

View file

@ -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"
}
}

View file

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

View file

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View file

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View file

@ -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>

View file

@ -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} />

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -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"
<Input
label="Name"
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"
/>
<SolidButton type="submit" className="mt-4">Create</SolidButton>
<Button type="submit" appearance="solid" variant="primary" className="mt-4">Create</Button>
</form>
</div>
);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
<Input
label="Dish Name"
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"
onChange={ (e) => setName(e.target.value) }
placeholder="Enter dish name"
className="mb-4"
/>
</div>
<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>
);

View file

@ -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
<Input
label="Dish Name"
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"
onChange={(e) => setName(e.target.value)}
/>
</div>
{/* Save button */}
<Button
type="submit"
disabled={loading}

View file

@ -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>
);
}

View file

@ -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'
)

View file

@ -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"

View file

@ -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>
</>;
};

View file

@ -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
{

View file

@ -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)

View file

@ -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,
}
)

View file

@ -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>
);
};

View file

@ -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"
<Input
label="Name"
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"
/>
<SolidButton type="submit" className="mt-4">Update</SolidButton>
<Button type="submit" appearance="solid" variant="primary" className="mt-4">Update</Button>
</form>
</div>
);

View 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;

View file

@ -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>

View 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;

View 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;

View 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;

View file

@ -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
)

View 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;

Some files were not shown because too many files have changed in this diff Show more