Compare commits

...

27 Commits

Author SHA1 Message Date
IluaAir
96ae1ef37c add route to cards 2025-12-09 23:59:04 +03:00
IluaAir
461e9a8105 Refactor Card component to introduce TaskButton for modal functionality and enhance layout with dynamic task data display. 2025-12-06 23:54:32 +03:00
IluaAir
cb73cd219b Update Card component to use a background image and adjust layout for improved aesthetics and responsiveness. 2025-11-19 01:45:37 +03:00
IluaAir
0d92ea9893 Refactor Card component layout and styles, adding an image background and improving badge styles for better user experience. 2025-11-14 23:30:12 +03:00
IluaAir
1a675834fe add first card 2025-11-14 00:43:25 +03:00
IluaAir
9f7e91d0a9 Update button styles in Calendar component to use neon effect for enhanced visual appeal 2025-10-28 23:47:18 +03:00
IluaAir
69b2571dd4 Update icon button styles to include hover translation effect for improved user interaction 2025-10-28 23:45:03 +03:00
IluaAir
bfd8c04a90 Refactor Calendar component styles, bigger columns and container glass stile 2025-10-28 23:29:08 +03:00
IluaAir
0b7a1a72d3 fiont style 2025-10-27 23:41:12 +03:00
IluaAir
9820437556 Enhance documentation and styling system with Mirage Palette, Material Design 3, and Neon accents. Add comprehensive style guide, color palette, and cheatsheet for improved developer experience. 2025-10-27 23:11:11 +03:00
IluaAir
cad5cfa780 begin new color schema 2025-10-26 23:48:06 +03:00
IluaAir
afb356944d fix radius 2025-10-26 23:39:21 +03:00
IluaAir
166bbee1d5 update 2025-10-25 23:32:23 +03:00
IluaAir
c7efdb89c2 add buttons 2025-10-23 22:24:03 +03:00
IluaAir
0c15be3b38 del logs from jwtexp 2025-10-23 22:04:24 +03:00
IluaAir
246cd54a47 full user login flow, redirect user from root path to dashboard 2025-10-21 22:40:37 +03:00
IluaAir
17b64121db Enhance Dashboard component to redirect on authentication error and streamline loading state display 2025-10-21 22:29:56 +03:00
IluaAir
692618ea21 Implement task fetching in Calendar component with loading state 2025-10-19 23:18:24 +03:00
IluaAir
024fa8b366 Add Calendar component to Dashboard for task organization by week 2025-10-19 23:10:10 +03:00
IluaAir
6f2ca0a891 Update dashboard styles and remove unnecessary border from week grid 2025-10-19 00:22:12 +03:00
IluaAir
fd8105d30a Refactor Dashboard component to handle authentication errors and improve token validation logic 2025-10-19 00:14:34 +03:00
IluaAir
6b241bcfe5 add refresh token api call 2025-10-18 23:27:30 +03:00
IluaAir
5e229585ef util for checkin' jwt validation and refresh 2025-10-18 23:10:32 +03:00
IluaAir
6f49d2b171 Implement api task fetching in Dashboard component with loading state 2025-10-16 23:41:10 +03:00
IluaAir
2722488a4e simple jwt token validation from frontend 2025-10-15 23:03:24 +03:00
IluaAir
7768f94122 user tasks 2025-10-15 22:47:07 +03:00
IluaAir
4830d2a432 tasks create 2025-10-15 22:43:52 +03:00
22 changed files with 1884 additions and 451 deletions

View File

@@ -50,6 +50,22 @@ ___
### Frontend
- **React.js**
- **Vite**
- **Tailwind CSS**
- **Material Design 3**
- **Custom Design System** (Mirage Palette + Neon)
---
## 🎨 Design System
Проект использует собственную систему дизайна:
- **Mirage Color Palette** - профессиональная темная сине-синяя тема
- **Material Design 3** - современные UI паттерны
- **Neon Cyberpunk** - уникальные неоновые акценты
**Документация:**
- [Frontend Guide](taskncoffee-app/README.md) - документация frontend
- [Styles Cheatsheet](taskncoffee-app/docs/STYLES_CHEATSHEET.md) - быстрая справка
---

View File

@@ -1,16 +1,76 @@
# React + Vite
# Task&Coffee Frontend
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
> Modern task management app with Material Design 3 + Neon aesthetic
Currently, two official plugins are available:
## 🚀 Quick Start
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
```bash
npm install
npm run dev
```
## React Compiler
---
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## 🎨 Styling System
## Expanding the ESLint configuration
We use a custom design system combining:
- **Mirage Color Palette** - Professional dark blue theme
- **Material Design 3** - Modern UI patterns
- **Neon Cyberpunk** - Unique glowing accents
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
### Usage
```jsx
import styles from '@/lib/styles';
<div className={styles.card.filled}>
<h2 className={styles.text.h2}>Hello</h2>
<button className={styles.button.primaryNeon}>Click</button>
</div>
```
### 📖 Documentation
| Document | Purpose |
|----------|---------|
| **[Cheatsheet](./docs/STYLES_CHEATSHEET.md)** | 📋 Quick reference - copy/paste examples |
| **[Style Guide](./docs/STYLE_GUIDE.md)** | 📚 Complete guide with examples |
| **[Color Palette](./docs/COLOR_PALETTE.md)** | 🎨 Color meanings & usage |
**👉 Start with [Cheatsheet](./docs/STYLES_CHEATSHEET.md)** for quick examples!
---
## 🛠️ Tech Stack
- React 18 + Vite
- Tailwind CSS + Custom Design System
- Radix UI (accessibility)
- Lucide React (icons)
---
## 📁 Project Structure
```
src/
├── api/ API services
├── components/ Reusable components
├── lib/
│ ├── styles.js ⭐ Style presets (60+ ready-to-use)
│ └── utils.js Helper functions
├── pages/ Page components
├── index.css ⭐ Color variables (Mirage palette)
└── neon.css ⭐ Neon glow effects
```
---
## 📚 Learn More
- [Design System Overview](../DESIGN_SYSTEM_SUMMARY.md)
- [Vite](https://vitejs.dev/) | [React](https://react.dev/) | [Tailwind](https://tailwindcss.com/)
---
**Made with 💙 and neon ✨**

View File

@@ -0,0 +1,259 @@
# 🎨 Task&Coffee Color Palette
**Usage examples?** See [STYLE_GUIDE.md](./STYLE_GUIDE.md)
**Quick reference?** See [STYLES_CHEATSHEET.md](./STYLES_CHEATSHEET.md)
**Back to main?** See [README.md](../README.md)
---
## Mirage Palette - Visual Guide
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MIRAGE COLOR SCALE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50 ░░░░░░░░░ #f4f6fb Lightest - heading text on dark bg
100 ▒▒▒▒▒▒▒▒▒ #e7edf7 Card foreground text
200 ▓▓▓▓▓▓▓▓▓ #cbd9ec Main foreground text
300 ▓▓▓▓▓▓▓▓▓ #9db8dc Muted foreground text
────────────────────────────────────────────────────────────
400 ████████ #6893c8 PRIMARY ACCENT ⭐
500 ████████ #4474b3 Accent hover state
────────────────────────────────────────────────────────────
600 ████████ #335b96 Secondary, elevated cards
700 ████████ #2a4a7a Dark cards alternative
800 ████████ #264066 Muted elements, popovers
900 ████████ #243756 CARDS ⭐
950 ████████ #111928 BACKGROUND (darkest) ⭐
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## 🌟 Semantic Color Mapping
### Background & Surfaces
```
--background #111928 (mirage-950) Main app background
--card #243756 (mirage-900) Card surfaces
--popover #264066 (mirage-800) Floating elements
--muted #264066 (mirage-800) Subtle backgrounds
--sidebar #111928 (mirage-950) Navigation rail
```
### Foreground & Text
```
--foreground #cbd9ec (mirage-200) Primary text
--card-foreground #e7edf7 (mirage-100) Text on cards
--muted-foreground #9db8dc (mirage-300) Secondary text
--popover-foreground #e7edf7 (mirage-100) Text in popovers
```
### Interactive Elements
```
--primary #6893c8 (mirage-400) Primary actions, links
--primary-foreground #111928 (mirage-950) Text on primary
--secondary #2a4a7a (mirage-700) Secondary actions
--accent #4474b3 (mirage-500) Hover states
```
### Borders & Inputs
```
--border rgba(157, 184, 220, 0.15) Subtle borders
--input rgba(157, 184, 220, 0.2) Input borders
--ring #6893c8 (mirage-400) Focus ring
```
---
## 💫 Neon Accent Colors
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEON ACCENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔵 Neon Blue #c6e2ff Light blue with glow effect
Glow color: rgba(30, 132, 242, 0.6)
💗 Neon Pink #ffc5ec Light pink with glow effect
Glow color: rgba(255, 20, 147, 0.6)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## 🎯 Usage by Element Type
### Text Colors
```
Headings: mirage-200 (#cbd9ec)
Body text: mirage-200 (#cbd9ec)
Secondary text: mirage-300 (#9db8dc)
Disabled text: mirage-300 (#9db8dc) with opacity
```
### Backgrounds
```
Page background: mirage-950 (#111928)
Card background: mirage-900 (#243756)
Hover background: mirage-800 (#264066)
Active background: mirage-700 (#2a4a7a)
```
### Buttons
```
Primary: mirage-400 (#6893c8) bg
Primary hover: mirage-500 (#4474b3) bg
Secondary: mirage-700 (#2a4a7a) bg
Secondary hover: mirage-600 (#335b96) bg
Ghost hover: mirage-800 (#264066) bg
```
### Borders
```
Default: mirage-300 with 15% opacity
Input: mirage-300 with 20% opacity
Focus: mirage-400 (#6893c8) solid
Divider: mirage-800 (#264066)
```
---
## 🌈 Color Relationships
```
LIGHT ↑
┌─────────────────────────────────┐
│ mirage-50 │ Lightest text │
│ mirage-100 │ Card text │
│ mirage-200 │ Main text │
│ mirage-300 │ Muted text │
├─────────────────────────────────┤
│ mirage-400 │ PRIMARY ⭐ │ ← Interactive
│ mirage-500 │ Hover state │
├─────────────────────────────────┤
│ mirage-600 │ Secondary │
│ mirage-700 │ Alt cards │
│ mirage-800 │ Muted bg │
│ mirage-900 │ CARDS ⭐ │ ← Surfaces
│ mirage-950 │ BACKGROUND ⭐ │
└─────────────────────────────────┘
DARK ↓
```
---
## 📊 Contrast Ratios (WCAG)
```
Background (950) + Text (200): AAA ✓ (14.2:1)
Card (900) + Card Text (100): AAA ✓ (12.8:1)
Primary (400) + Primary FG (950): AA ✓ (5.2:1)
Muted (800) + Muted Text (300): AA ✓ (4.8:1)
```
All color combinations meet WCAG AA standards minimum! 🎉
---
## 🎨 Example Compositions
### Card with Neon Accent
```
┌────────────────────────────────┐
│ mirage-900 background │
│ ┌──────────────────────────┐ │
│ │ mirage-100 text │ │
│ │ mirage-300 muted text │ │
│ │ │ │
│ │ [mirage-400 button] │ │
│ │ + neon blue glow │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
```
### Primary Button
```
┌──────────────────────┐
│ mirage-400 bg │ ← Background
│ mirage-950 text │ ← Text
│ + subtle shadow │
│ hover: neon glow │ ← Neon effect on hover
└──────────────────────┘
```
### Today's Task Column
```
┌────────────────────────────────┐
│ mirage-400/10 background │ ← 10% opacity
│ + mirage-400 border (2px) │ ← Solid border
│ + neon-blue glow │ ← Glow effect
│ │
│ Tasks: mirage-900 cards │
└────────────────────────────────┘
```
---
## 🔄 Dark Mode Toggle
When dark mode is active (`.dark` class):
```
Background: mirage-900 → mirage-950
Cards: mirage-700 → mirage-900
Primary: mirage-300 → mirage-400
Text: mirage-100 → mirage-200
```
Currently configured for dark-first design.
---
## 💡 Best Practices
1. **Depth hierarchy:**
- Background: mirage-950
- Elevated: mirage-900
- More elevated: mirage-800
2. **Text contrast:**
- Always use mirage-100/200 for text on dark backgrounds
- Use mirage-300 for less important text
3. **Neon sparingly:**
- Use neon effects only for:
- Brand elements (logo, headers)
- Important CTAs
- Today's tasks/current items
- Don't overuse - it should feel special
4. **Accessible focus states:**
- Always use mirage-400 for focus rings
- Add 3px ring with 50% opacity
---
## 🚀 Quick Reference
| Element | Light Value | Dark Value | Usage |
|---------|-------------|------------|-------|
| Page BG | `mirage-950` | `mirage-900` | Main background |
| Card BG | `mirage-900` | `mirage-700` | Surface |
| Text | `mirage-200` | `mirage-100` | Primary text |
| Primary | `mirage-400` | `mirage-300` | CTA buttons |
| Border | `mirage-300/15%` | `mirage-300/20%` | Dividers |
---
**See also:**
- `STYLE_GUIDE.md` - Complete style documentation
- `STYLES_CHEATSHEET.md` - Quick usage examples
- `src/lib/styles.js` - Tailwind class presets

View File

@@ -0,0 +1,244 @@
# Task&Coffee Styles Cheatsheet 🎨
Быстрая справка по использованию стилей в проекте.
**Full guide?** See [STYLE_GUIDE.md](./STYLE_GUIDE.md)
**Color reference?** See [COLOR_PALETTE.md](./COLOR_PALETTE.md)
**Back to main?** See [README.md](../README.md)
---
## Импорт
```javascript
import styles from '@/lib/styles';
import { cn } from '@/lib/utils';
```
---
## 📦 Карточки
```jsx
<div className={styles.card.filled}>Basic card</div>
<div className={styles.card.elevated}>Elevated card</div>
<div className={styles.card.task}>Task card</div>
<div className={styles.card.taskNeon}>Task card with glow</div>
```
---
## 🔘 Кнопки
```jsx
<button className={styles.button.primary}>Primary</button>
<button className={styles.button.primaryNeon}>Primary Neon</button>
<button className={styles.button.secondary}>Secondary</button>
<button className={styles.button.outline}>Outline</button>
<button className={styles.button.ghost}>Ghost</button>
<button className={styles.button.destructive}>Delete</button>
<button className={styles.button.icon}>🔍</button>
<button className={styles.button.iconNeon}></button>
```
---
## 📝 Inputs
```jsx
<input className={styles.input.default} />
<input className={styles.input.neon} />
<input className={styles.input.search} />
```
---
## 🏷️ Badges
```jsx
<span className={styles.badge.low}>Low</span>
<span className={styles.badge.medium}>Medium</span>
<span className={styles.badge.high}>High</span>
<span className={styles.badge.critical}>Critical</span>
<span className={styles.badge.success}>Success</span>
<span className={styles.badge.warning}>Warning</span>
<span className={styles.badge.error}>Error</span>
```
---
## ✨ Neon Effects (CSS Classes)
```jsx
<div className="neon-glow-blue">Blue glow on hover</div>
<div className="neon-glow-pink">Pink glow on hover</div>
<div className="neon-glow-soft">Soft glow</div>
<div className="neon-border-blue">Blue neon border</div>
<div className="neon-border-pink">Pink neon border</div>
<span className="neon-text-blue">Blue neon text</span>
<span className="neon-text-pink">Pink neon text</span>
<Icon className="neon-icon" />
```
---
## 🎭 Neon Animated Text
```jsx
<span className="sign-inline">Task&Coffee</span>
<span className="sign-pink-inline">Task&Coffee</span>
```
---
## 📐 Layout
```jsx
<div className={styles.layout.container}>
<aside className={styles.layout.sidebar}>Sidebar</aside>
<main className={styles.layout.main}>
<div className={styles.layout.content}>Content</div>
</main>
</div>
<div className={styles.layout.gridCards}>Cards grid</div>
```
---
## 🔤 Typography
```jsx
<h1 className={styles.text.h1}>Heading 1</h1>
<h2 className={styles.text.h2}>Heading 2</h2>
<p className={styles.text.body}>Body text</p>
<p className={styles.text.bodyMuted}>Muted text</p>
<small className={styles.text.small}>Small text</small>
```
---
## 🛠️ Utilities
```jsx
// Glow effects
<div className={styles.utility.glowBlue}>Blue glow</div>
<div className={styles.utility.glowPink}>Pink glow</div>
<div className={styles.utility.glowSoft}>Soft glow</div>
// Glass effect
<div className={styles.utility.glass}>Glassmorphism</div>
// Transitions
<div className={styles.utility.transition}>Smooth</div>
<div className={styles.utility.transitionSlow}>Slow</div>
// Focus ring
<button className={styles.utility.focusRing}>Accessible</button>
```
---
## 🎨 Цветовые переменные
### В CSS
```css
background: var(--color-background);
color: var(--color-foreground);
background: var(--color-card);
color: var(--color-primary);
border: 1px solid var(--color-border);
```
### В Tailwind
```jsx
<div className="bg-background text-foreground">
<div className="bg-card text-card-foreground">
<div className="bg-primary text-primary-foreground">
<div className="border border-border">
```
---
## 🔄 Комбинирование
```jsx
import { cn } from '@/lib/utils';
<div className={cn(
styles.card.task,
"neon-glow-soft",
isActive && "neon-border-blue"
)}>
Combined styles
</div>
```
---
## 📏 Радиусы
- `rounded-lg` = **0.5rem** ← используем для кнопок и карточек
- `rounded-xl` = 0.75rem
- `rounded-2xl` = 1rem
---
## 🎯 Полный пример компонента
```jsx
import styles from '@/lib/styles';
import { cn } from '@/lib/utils';
function TaskCard({ task, isToday }) {
return (
<div className={cn(
styles.card.task,
"neon-glow-soft",
isToday && "neon-border-blue"
)}>
<div className="flex items-center gap-2">
<span className={styles.badge[task.priority]}>
{task.priority}
</span>
<h3 className={styles.text.small}>{task.title}</h3>
</div>
<p className={styles.text.smallMuted}>{task.description}</p>
<button className={styles.button.primaryNeon}>
Complete
</button>
</div>
);
}
```
---
## 🌈 Mirage Colors
| Name | Hex | Usage |
|------|-----|-------|
| mirage-950 | `#111928` | Background (darkest) |
| mirage-900 | `#243756` | Cards |
| mirage-800 | `#264066` | Muted elements |
| mirage-700 | `#2a4a7a` | Secondary |
| mirage-600 | `#335b96` | Elevated cards |
| mirage-500 | `#4474b3` | Accent hover |
| mirage-400 | `#6893c8` | Primary accent |
| mirage-300 | `#9db8dc` | Muted text |
| mirage-200 | `#cbd9ec` | Main text |
| mirage-100 | `#e7edf7` | Card text |
| mirage-50 | `#f4f6fb` | Lightest text |
---
## 💡 Tips
1. Используйте готовые стили из `styles.js`
2. Комбинируйте с `cn()` для кастомизации
3. Неон - только для акцентов!
4. Всегда используйте CSS переменные
5. `rounded-lg` для консистентности
**Полная документация:** См. `STYLE_GUIDE.md`

View File

@@ -0,0 +1,371 @@
# Task&Coffee Style Guide
> Complete guide with examples and best practices
**Quick reference?** See [STYLES_CHEATSHEET.md](./STYLES_CHEATSHEET.md)
**Color details?** See [COLOR_PALETTE.md](./COLOR_PALETTE.md)
**Back to main?** See [README.md](../README.md)
---
## 📦 Using Styles
### Import
```javascript
import styles from '@/lib/styles';
import { cn } from '@/lib/utils';
```
### Примеры использования
#### 1. Карточки (Cards)
```jsx
// Filled card - основной вариант
<div className={cardStyles.filled}>
<h3>Card Title</h3>
<p>Card content...</p>
</div>
// Elevated card с тенью
<div className={cardStyles.elevated}>
<h3>Important Card</h3>
</div>
// Task card с неоновым эффектом
<div className={cardStyles.taskNeon}>
<p>Task description</p>
</div>
// Или используя cn helper
import { cn } from '@/lib/utils';
<div className={cn(cardStyles.filled, "additional-class")}>
Content
</div>
```
#### 2. Кнопки (Buttons)
```jsx
// Primary button
<button className={buttonStyles.primary}>
Submit
</button>
// Primary с неоновым эффектом
<button className={buttonStyles.primaryNeon}>
Create Task
</button>
// Secondary button
<button className={buttonStyles.secondary}>
Cancel
</button>
// Ghost button
<button className={buttonStyles.ghost}>
Learn More
</button>
// Icon button с неоновым эффектом
<button className={buttonStyles.iconNeon}>
<Icon />
</button>
```
#### 3. Layouts
```jsx
// Dashboard layout
<div className={layoutStyles.container}>
<aside className={layoutStyles.sidebar}>
{/* Sidebar content */}
</aside>
<main className={layoutStyles.main}>
<div className={layoutStyles.content}>
{/* Main content */}
</div>
</main>
</div>
// Grid для карточек
<div className={layoutStyles.gridCards}>
<Card />
<Card />
<Card />
</div>
```
#### 4. Inputs
```jsx
// Стандартный input
<input
type="text"
className={inputStyles.default}
placeholder="Enter text..."
/>
// Input с неоновым фокусом
<input
type="text"
className={inputStyles.neon}
placeholder="Task name..."
/>
// Search input
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="search"
className={inputStyles.search}
placeholder="Search tasks..."
/>
</div>
```
#### 5. Typography
```jsx
// Headings
<h1 className={textStyles.h1}>Main Title</h1>
<h2 className={textStyles.h2}>Section Title</h2>
// Body text
<p className={textStyles.body}>Regular text</p>
<p className={textStyles.bodyMuted}>Muted text</p>
// Неоновый текст (используется с neon.css)
<span className={cn(textStyles.neonBlue, "sign-inline")}>
Task&
</span>
<span className={cn(textStyles.neonPink, "sign-pink-inline")}>
Coffee
</span>
```
#### 6. Badges
```jsx
// Priority badges
<span className={badgeStyles.low}>Low</span>
<span className={badgeStyles.medium}>Medium</span>
<span className={badgeStyles.high}>High</span>
<span className={badgeStyles.critical}>Critical</span>
// Status badges
<span className={badgeStyles.success}>Completed</span>
<span className={badgeStyles.warning}>In Progress</span>
<span className={badgeStyles.error}>Failed</span>
```
---
## ✨ Неоновые эффекты
### CSS классы из neon.css
```jsx
// Неоновое свечение при hover - голубое
<div className="neon-glow-blue bg-card p-4 rounded-lg">
Card with blue glow on hover
</div>
// Неоновое свечение - розовое
<div className="neon-glow-pink bg-card p-4 rounded-lg">
Card with pink glow on hover
</div>
// Мягкое свечение
<button className="neon-glow-soft bg-primary px-4 py-2 rounded-lg">
Subtle glow
</button>
// Неоновая граница
<div className="neon-border-blue p-4 rounded-lg">
Border with neon effect
</div>
// Неоновый текст (без анимации)
<span className="neon-text-blue">
Static neon text
</span>
// Иконка с неоновым эффектом
<Icon className="neon-icon" />
```
### Анимированный неоновый текст
```jsx
// Для заголовков и брендинга
<div className="sign-inline">Coffee</div>
<div className="sign-pink-inline">Task&</div>
// Для больших заголовков (fullscreen)
<div className="sign">
<span>Task&Coffee</span>
</div>
```
---
## 🎭 Комбинирование стилей
### Пример: Карточка задачи с неоновым эффектом
```jsx
import { cn } from '@/lib/utils';
import { cardStyles, badgeStyles, textStyles } from '@/lib/styles';
function TaskCard({ task }) {
return (
<div className={cn(
cardStyles.task,
"neon-glow-soft",
task.isToday && "neon-border-blue"
)}>
<div className="flex items-center gap-2 mb-2">
<span className={badgeStyles[task.priority]}>
{task.priority}
</span>
<h3 className={textStyles.small}>
{task.title}
</h3>
</div>
<p className={textStyles.smallMuted}>
{task.description}
</p>
</div>
);
}
```
### Пример: Кнопка с неоновым эффектом и иконкой
```jsx
import { PlusIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { buttonStyles } from '@/lib/styles';
function CreateButton() {
return (
<button className={cn(
buttonStyles.primaryNeon,
"gap-2"
)}>
<PlusIcon className="w-4 h-4" />
Create Task
</button>
);
}
```
---
## 🎨 Миграция с dashboard.css на Tailwind
### До (dashboard.css):
```css
.dashboard-task-card {
background: var(--color-card);
padding: 0.75rem;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
```
### После (с использованием стилей):
```jsx
<div className={cardStyles.task}>
{/* content */}
</div>
// Или чистый Tailwind:
<div className="bg-card p-3 rounded-lg shadow-sm">
{/* content */}
</div>
```
---
## 📐 Радиусы округления
Теперь все радиусы синхронизированы:
- `rounded-sm` = `0.25rem` (4px)
- `rounded-md` = `0.375rem` (6px)
- `rounded-lg` = `0.5rem` (8px) ✅ **Используем для карточек и кнопок**
- `rounded-xl` = `0.75rem` (12px)
- `rounded-2xl` = `1rem` (16px)
Базовый радиус установлен в `--radius: 0.5rem` для консистентности с Material Design 3.
---
## 🔧 Утилиты
### Neon Glow Utilities
```javascript
import { utilityStyles } from '@/lib/styles';
<div className={cn(cardStyles.filled, utilityStyles.glowBlue)}>
Card with blue glow
</div>
```
### Glassmorphism
```jsx
<div className={cn(utilityStyles.glass, "p-6 rounded-lg")}>
Glass effect card
</div>
```
### Focus Ring
```jsx
<button className={cn(
buttonStyles.primary,
utilityStyles.focusRing
)}>
Accessible button
</button>
```
---
## 💡 Best Practices
1. **Используйте готовые стили** из `styles.js` для консистентности
2. **Комбинируйте с cn()** для добавления кастомных классов
3. **Неоновые эффекты** используйте умеренно - только для акцентов
4. **Радиусы** - используйте `rounded-lg` для основных элементов
5. **Цвета** - всегда используйте CSS переменные, не хардкодьте цвета
6. **Accessibility** - не забывайте про `focusRing` для интерактивных элементов
---
## 🚀 Быстрый старт
```jsx
// 1. Импортируйте стили
import styles from '@/lib/styles';
import { cn } from '@/lib/utils';
// 2. Используйте в компонентах
function MyComponent() {
return (
<div className={styles.layout.container}>
<div className={styles.card.filled}>
<h2 className={styles.text.h2}>Title</h2>
<p className={styles.text.bodyMuted}>Description</p>
<button className={styles.button.primaryNeon}>
Action
</button>
</div>
</div>
);
}
```
Теперь у вас есть полная система стилей, объединяющая Mirage palette, Material Design 3 и неоновую эстетику! 🎉

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

View File

@@ -4,13 +4,14 @@ import { SignUp } from './pages/SignUp'
import { AuthLayout } from './layouts/AuthLayout'
import { Routes, Route, Navigate } from 'react-router'
import Dashboard from './pages/Dashboard'
import RootRedirect from './pages/RootRedirect'
import { TaskButton } from './components/Card'
function App() {
return (
<Routes>
<Route path="/" element={<Navigate to="/auth/login" replace />} />
<Route path="/" element={<RootRedirect />} />
<Route path="/auth" element={
<AuthLayout className="flex min-h-svh flex-col items-center justify-center bg-muted text-foreground" />
@@ -22,6 +23,10 @@ function App() {
<Route path="/dashboard" element={
<Dashboard />
} />
<Route path="/card" element={<TaskButton />} />
<Route path="/card/:id" element={
<TaskButton />
} />
<Route path="*" element={<Navigate to="/auth/login" replace />} />
</Routes>
)

View File

@@ -81,3 +81,20 @@ export const login = async (username, password) => {
throw error.response?.data || error.message;
}
};
export const refreshToken = async () => {
try {
const fingerprint = localStorage.getItem('fingerprint');
const response = await client.post(API_ENDPOINTS.AUTH.REFRESH, {
fingerprint: fingerprint,
});
if (response.data.access_token && response.data.token_type === 'bearer') {
localStorage.setItem('access_token', response.data.access_token);
}
return response.data;
} catch (error) {
localStorage.removeItem('access_token');
localStorage.removeItem('fingerprint');
throw error.response?.data || error.message;
}
};

View File

@@ -0,0 +1,22 @@
import client from './client';
import { API_ENDPOINTS } from './ApiSources';
/**
* Create a new task
* @param {Object} taskData - Task data
* @param {string} taskData.title - Task title
* @param {string} taskData.description - Task description
* @param {string} taskData.priority - Task priority
* @param {string} taskData.status - Task status
* @param {string} taskData.due_date - Task due date
* @returns {Promise<Object>} Created task data
*/
export const createTask = async (taskData) => {
try {
const response = await client.post(API_ENDPOINTS.TASKS.CREATE, taskData);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
};

View File

@@ -0,0 +1,44 @@
import client from './client';
import { API_ENDPOINTS } from './ApiSources';
/**
* Get all users
* @returns {Promise<Object>} All users data
*/
export const getUsers = async () => {
try {
responce = await client.get(API_ENDPOINTS.USERS.LIST);
return responce.data;
} catch (error) {
throw error.response?.data || error.message;
}
};
/**
* Get user by id
* @param {number} id - User id
* @returns {Promise<Object>} User data
*/
export const getUserById = async (id) => {
try {
const response = await client.get(API_ENDPOINTS.USERS.BY_ID(id));
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
};
/**
* Get all user tasks
* @param {number} id - User id
* @returns {Promise<Object>} All user tasks data
*/
export const getUserTasks = async (id) => {
try {
const response = await client.get(API_ENDPOINTS.USERS.TASKS(id));
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
};

View File

@@ -0,0 +1,178 @@
import { useState, useEffect } from 'react';
import { getUserTasks } from '@/api/users.service';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import styles from '@/lib/styles';
import { Plus } from 'lucide-react';
export default function Calendar() {
const [tasksFromBackend, setTasksFromBackend] = useState([]);
const [loading, setLoading] = useState(true);
// Fetch tasks when component mounts
useEffect(() => {
const fetchTasks = async () => {
try {
console.log('Calendar: Fetching user tasks...');
const response = await getUserTasks(1);
console.log('Calendar: Tasks from backend:', response);
setTasksFromBackend(response);
} catch (error) {
console.error('Calendar: Failed to fetch tasks:', error);
} finally {
setLoading(false);
}
};
fetchTasks();
}, []);
const today = new Date();
const currentDay = today.getDay();
const currentDate = new Date(today);
const dayOffset = currentDay === 0 ? -6 : 1 - currentDay;
currentDate.setDate(today.getDate() + dayOffset);
const groupTasksByDay = (tasks) => {
const grouped = {};
tasks.forEach(task => {
const taskDate = new Date(task.due_date);
const dateKey = taskDate.toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
grouped[dateKey].push({
id: task.id,
title: task.title,
priority: task.priority,
completed: task.status === 'closed' || task.status === 'completed',
dueDate: task.due_date
});
});
return grouped;
};
const groupedTasks = groupTasksByDay(tasksFromBackend);
const daysOfWeek = [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
].map((name, index) => {
const date = new Date(currentDate);
date.setDate(currentDate.getDate() + index);
const dateKey = date.toDateString();
return {
name,
date: date.getDate(),
month: date.toLocaleDateString('en-US', { month: 'short' }),
fullDate: date,
tasks: groupedTasks[dateKey] || []
};
});
if (loading) {
return (
<div className={cn(styles.card.elevated, "flex-[3]")}>
<div className="flex items-center justify-center p-8">
<p className={styles.text.body}>Loading tasks...</p>
</div>
</div>
);
}
return (
<div className={cn(
styles.utility.glass,
"p-6 rounded-2xl flex-3",
styles.utility.transitionSlow
)}>
<div className="flex gap-3 h-full overflow-x-auto px-3">
{daysOfWeek.map((day) => {
const isToday = day.fullDate.toDateString() === today.toDateString();
return (
<div
key={day.name}
className={cn(
"flex-1 min-w-[180px] max-w-[200px] flex flex-col rounded-2xl p-4 transition-all duration-300",
isToday
? "bg-primary/10 border-2 border-primary neon-glow-soft"
: "bg-background border border-border"
)}
>
{/* Header */}
<div className="text-center pb-3 border-b border-border mb-3">
<h3 className={cn(
styles.text.small,
"font-semibold mb-1",
isToday && "text-primary font-bold"
)}>
{day.name}
</h3>
<p className={cn(
styles.text.smallMuted,
isToday && "text-primary font-semibold"
)}>
{day.month} {day.date}
</p>
</div>
{/* Tasks */}
<div className="flex-1 flex flex-col gap-2 overflow-y-auto pt-1">
{day.tasks.map((task) => (
<div
key={task.id}
className={cn(
styles.card.task,
task.completed && "opacity-60 line-through"
)}
>
<div className="flex items-center gap-2">
{/* Priority indicator */}
<span
className={cn(
"w-1.5 h-1.5 rounded-full flex-shrink-0",
task.priority === 'low' && "bg-green-500",
task.priority === 'medium' && "bg-yellow-500",
task.priority === 'high' && "bg-orange-500",
task.priority === 'critical' && "bg-red-500"
)}
/>
<p className={cn(styles.text.small, "flex-1")}>
{task.title}
</p>
</div>
</div>
))}
{/* Add button */}
<button
className={cn(
styles.button.iconNeon,
"mt-2 w-full flex items-center justify-center gap-2",
)}
onClick={() => {
console.log('Add task clicked for', day.name);
}}
>
<Plus className="w-4 h-4" />
<span className="text-xs">Add Task</span>
</button>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,139 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router'
import styles from '@/lib/styles'
import { cn } from '@/lib/utils'
import { X } from 'lucide-react'
export function TaskButton({ taskData }) {
const navigate = useNavigate()
const location = useLocation()
const [isModalOpen, setIsModalOpen] = useState(false)
const taskId = taskData?.id || 1
// Проверяем, открыта ли карточка по URL
useEffect(() => {
const isCardRoute = location.pathname === `/card/${taskId}`
setIsModalOpen(isCardRoute)
}, [location.pathname, taskId])
const openModal = () => {
navigate(`/card/${taskId}`)
}
const closeModal = () => {
navigate(-1) // Возвращаемся на предыдущую страницу
}
return (
<>
<div
key={taskData?.id || 1}
className={cn(
styles.card.task,
"w-[110px] cursor-pointer hover:opacity-80 transition-opacity"
)}
onClick={openModal}
>
{taskData?.title || "Sample task card"}
</div>
{isModalOpen && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
onClick={closeModal}
>
<div onClick={(e) => e.stopPropagation()}>
<CardComponent
status={taskData?.status}
time_spent={taskData?.time_spent}
description={taskData?.description}
title={taskData?.title}
priority={taskData?.priority}
due_date={taskData?.due_date}
/>
</div>
</div>
)}
</>
)
}
export function CardComponent({
status = "todo",
time_spent = 0,
description = "No description",
title = "Task",
priority = "medium",
due_date = null
}) {
const formatTimeSpent = (minutes) => {
if (!minutes || minutes === 0) return "0 min";
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) {
return `${hours}h ${mins > 0 ? `${mins}m` : ''}`;
}
return `${mins}m`;
};
const formatDate = (dateString) => {
if (!dateString) return "No due date";
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const priorityBadgeStyle = {
low: styles.badge.low,
medium: styles.badge.medium,
high: styles.badge.high,
critical: styles.badge.critical,
};
const statusLabels = {
open: "Open",
closed: "Closed",
in_progress: "In Progress",
todo: "To Do"
};
return (
<div className={cn(styles.card.filled, "flex flex-col w-full min-w-[400px] max-w-[600px] h-[400px] overflow-hidden")}>
<div className="flex items-end flex-1 -mt-6 -mx-6 bg-center bg-cover bg-[url('/images/a.jpg')]">
<h1 className={cn(styles.text.h1, "pl-6")}>{title}</h1>
</div>
<div className="flex-1 flex flex-col gap-3 z-10 mt-6">
<div className="flex items-center gap-2 flex-wrap">
<span className={cn(priorityBadgeStyle[priority])}>
{priority.charAt(0).toUpperCase() + priority.slice(1)}
</span>
<span className={cn(styles.text.smallMuted)}>
Status: {statusLabels[status]}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<span className={cn(styles.text.smallMuted)}>
{formatTimeSpent(time_spent)}
</span>
<span className={cn(styles.text.smallMuted)}>
📅 {formatDate(due_date)}
</span>
</div>
<p className={cn(styles.text.smallMuted, "flex-1 line-clamp-3 overflow-auto")}>
{description}
</p>
<button className={cn(styles.button.primaryNeon, "w-[110px]")}>
<span className={styles.text.neonBlue}>Complete</span>
</button>
</div>
</div>
)
}

View File

@@ -19,6 +19,8 @@ const buttonVariants = cva(
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
elevated:
"bg-card p-3 rounded-lg shadow-sm text-sm transition-transform hover:-translate-y-0.5 hover:shadow-md",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@@ -8,46 +8,80 @@
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color-scheme: dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
background-color: #111928;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.13 0.028 261.692);
--card: oklch(1 0 0);
--card-foreground: oklch(0.13 0.028 261.692);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.13 0.028 261.692);
--primary: oklch(0.21 0.034 264.665);
--primary-foreground: oklch(0.985 0.002 247.839);
--secondary: oklch(0.967 0.003 264.542);
--secondary-foreground: oklch(0.21 0.034 264.665);
--muted: oklch(0.967 0.003 264.542);
--muted-foreground: oklch(0.551 0.027 264.364);
--accent: oklch(0.967 0.003 264.542);
--accent-foreground: oklch(0.21 0.034 264.665);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.928 0.006 264.531);
--input: oklch(0.928 0.006 264.531);
--ring: oklch(0.707 0.022 261.325);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.002 247.839);
--sidebar-foreground: oklch(0.13 0.028 261.692);
--sidebar-primary: oklch(0.21 0.034 264.665);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.967 0.003 264.542);
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
--sidebar-border: oklch(0.928 0.006 264.531);
--sidebar-ring: oklch(0.707 0.022 261.325);
/* Mirage Palette Colors */
--mirage-50: #f4f6fb;
--mirage-100: #e7edf7;
--mirage-200: #cbd9ec;
--mirage-300: #9db8dc;
--mirage-400: #6893c8;
--mirage-500: #4474b3;
--mirage-600: #335b96;
--mirage-700: #2a4a7a;
--mirage-800: #264066;
--mirage-900: #243756;
--mirage-950: #111928;
/* Neon Accent Colors */
--neon-blue: #c6e2ff;
--neon-blue-glow: rgba(30, 132, 242, 0.6);
--neon-pink: #ffc5ec;
--neon-pink-glow: rgba(255, 20, 147, 0.6);
/* Material Design 3 Semantic Colors with Mirage Palette */
--radius: 0.5rem;
--background: #111928; /* mirage-950 - самый темный для фона */
--foreground: #cbd9ec; /* mirage-200 - светлый текст */
--card: #243756; /* mirage-900 - карточки темнее фона */
--card-foreground: #e7edf7; /* mirage-100 - текст на карточках */
--popover: #264066; /* mirage-800 - всплывающие элементы */
--popover-foreground: #e7edf7; /* mirage-100 */
--primary: #6893c8; /* mirage-400 - основной акцент (средний синий) */
--primary-foreground: #111928; /* mirage-950 - темный текст на акценте */
--secondary: #2a4a7a; /* mirage-700 - вторичный цвет */
--secondary-foreground: #e7edf7; /* mirage-100 */
--muted: #264066; /* mirage-800 - приглушенные элементы */
--muted-foreground: #9db8dc; /* mirage-300 - приглушенный текст */
--accent: #4474b3; /* mirage-500 - акцент при наведении */
--accent-foreground: #f4f6fb; /* mirage-50 */
--destructive: #ef4444; /* красный для удаления */
--destructive-foreground: #f4f6fb;
--border: rgba(157, 184, 220, 0.15); /* mirage-300 с прозрачностью */
--input: rgba(157, 184, 220, 0.2); /* чуть плотнее для полей ввода */
--ring: #6893c8; /* mirage-400 - фокус как primary */
/* Sidebar */
--sidebar: #111928; /* mirage-950 - как фон */
--sidebar-foreground: #cbd9ec; /* mirage-200 */
--sidebar-primary: #6893c8; /* mirage-400 */
--sidebar-primary-foreground: #111928;
--sidebar-accent: #335b96; /* mirage-600 */
--sidebar-accent-foreground: #e7edf7;
--sidebar-border: rgba(157, 184, 220, 0.1);
--sidebar-ring: #6893c8;
/* Chart Colors - используем палитру */
--chart-1: #6893c8; /* mirage-400 */
--chart-2: #4474b3; /* mirage-500 */
--chart-3: #335b96; /* mirage-600 */
--chart-4: #9db8dc; /* mirage-300 */
--chart-5: #2a4a7a; /* mirage-700 */
}
@theme inline {
@@ -89,37 +123,39 @@
}
.dark {
--background: oklch(0.13 0.028 261.692);
--foreground: oklch(0.985 0.002 247.839);
--card: oklch(0.21 0.034 264.665);
--card-foreground: oklch(0.985 0.002 247.839);
--popover: oklch(0.21 0.034 264.665);
--popover-foreground: oklch(0.985 0.002 247.839);
--primary: oklch(0.928 0.006 264.531);
--primary-foreground: oklch(0.21 0.034 264.665);
--secondary: oklch(0.278 0.033 256.848);
--secondary-foreground: oklch(0.985 0.002 247.839);
--muted: oklch(0.278 0.033 256.848);
--muted-foreground: oklch(0.707 0.022 261.325);
--accent: oklch(0.278 0.033 256.848);
--accent-foreground: oklch(0.985 0.002 247.839);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.034 264.665);
--sidebar-foreground: oklch(0.985 0.002 247.839);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.278 0.033 256.848);
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
/* Темная тема немного светлее основной (для toggle режима) */
--background: #243756; /* mirage-900 */
--foreground: #e7edf7; /* mirage-100 */
--card: #2a4a7a; /* mirage-700 */
--card-foreground: #f4f6fb; /* mirage-50 */
--popover: #264066; /* mirage-800 */
--popover-foreground: #e7edf7;
--primary: #9db8dc; /* mirage-300 - светлее для контраста */
--primary-foreground: #111928;
--secondary: #335b96; /* mirage-600 */
--secondary-foreground: #f4f6fb;
--muted: #264066;
--muted-foreground: #9db8dc;
--accent: #6893c8; /* mirage-400 */
--accent-foreground: #111928;
--destructive: #ef4444;
--destructive-foreground: #f4f6fb;
--border: rgba(157, 184, 220, 0.2);
--input: rgba(157, 184, 220, 0.25);
--ring: #9db8dc;
--chart-1: #9db8dc;
--chart-2: #6893c8;
--chart-3: #4474b3;
--chart-4: #cbd9ec;
--chart-5: #335b96;
--sidebar: #243756;
--sidebar-foreground: #e7edf7;
--sidebar-primary: #9db8dc;
--sidebar-primary-foreground: #111928;
--sidebar-accent: #4474b3;
--sidebar-accent-foreground: #f4f6fb;
--sidebar-border: rgba(157, 184, 220, 0.15);
--sidebar-ring: #9db8dc;
}
@layer base {

View File

@@ -0,0 +1,168 @@
/**
* Tailwind CSS Class Presets for Task&Coffee
* Material Design 3 + Neon Aesthetic
* Using Mirage Color Palette
*/
// Card Styles - Material Design 3 Variants
export const cardStyles = {
// Filled card - основной вариант для большинства карточек
filled: "bg-card text-card-foreground p-6 rounded-lg shadow-sm",
// Elevated card - карточки с эффектом поднятия
elevated: "bg-card text-card-foreground p-6 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300",
// Outlined card - карточки с границей
outlined: "bg-background border-2 border-border text-foreground p-6 rounded-lg",
// Task card - компактные карточки для задач
task: "bg-card text-card-foreground p-3 rounded-lg shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200",
// Task card with neon accent
taskNeon: "bg-card text-card-foreground p-3 rounded-lg shadow-sm hover:shadow-[0_0_15px_rgba(104,147,200,0.3)] hover:-translate-y-0.5 transition-all duration-200",
};
// Button Styles
export const buttonStyles = {
// Primary button - основные действия
primary: "bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md",
// Primary with neon glow
primaryNeon: "bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 hover:shadow-[0_0_20px_rgba(104,147,200,0.5)]",
// Secondary button - вторичные действия
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 px-4 py-2 rounded-lg font-medium transition-all duration-200",
// Outline button - менее важные действия
outline: "border-2 border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-lg font-medium transition-all duration-200",
// Ghost button - минималистичные кнопки
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-lg font-medium transition-all duration-200",
// Destructive button - опасные действия
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md",
// Icon button
icon: "p-2 rounded-lg hover:bg-accent transition-all duration-200 hover:-translate-y-0.5",
// Icon button with neon effect
iconNeon: "p-2 rounded-lg hover:bg-accent transition-all duration-200 hover:shadow-[0_0_15px_rgba(104,147,200,0.4)] hover:-translate-y-0.5",
};
// Layout Styles
export const layoutStyles = {
// Main container
container: "flex min-h-screen bg-background text-foreground",
// Sidebar/Navigation rail
sidebar: "w-20 bg-sidebar border-r border-sidebar-border flex flex-col items-center py-4 gap-4",
// Main content area
main: "flex-1 p-6 bg-background flex flex-col",
// Content wrapper with max width
content: "max-w-7xl mx-auto w-full flex flex-1 flex-col gap-6",
// Grid layouts
gridCards: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
gridTasks: "grid grid-cols-1 gap-3",
};
// Input Styles
export const inputStyles = {
// Standard input
default: "w-full px-4 py-2 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all duration-200",
// Input with neon focus effect
neon: "w-full px-4 py-2 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:shadow-[0_0_15px_rgba(104,147,200,0.3)] transition-all duration-200",
// Search input
search: "w-full px-4 py-2 pl-10 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all duration-200",
};
// Text Styles
export const textStyles = {
// Headings
h1: "text-4xl font-bold text-foreground",
h2: "text-3xl font-semibold text-foreground",
h3: "text-2xl font-semibold text-foreground",
h4: "text-xl font-semibold text-foreground",
// Body text
body: "text-base text-foreground",
bodyMuted: "text-base text-muted-foreground",
// Small text
small: "text-sm text-foreground",
smallMuted: "text-sm text-muted-foreground",
// Neon text (to be combined with neon.css animations)
neonBlue: "text-[#c6e2ff]",
neonPink: "text-[#ffc5ec]",
};
// Badge/Chip Styles
export const badgeStyles = {
// Default badge
default: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-primary/20 text-primary border border-primary/30 cursor-default select-none",
// Priority badges
low: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30 cursor-default select-none",
medium: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 cursor-default select-none",
high: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-orange-500/20 text-orange-400 border border-orange-500/30 cursor-default select-none",
critical: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30 cursor-default select-none",
// Status badges
success: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30",
warning: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30",
error: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30",
info: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-blue-500/20 text-blue-400 border border-blue-500/30",
};
// Utility Styles
export const utilityStyles = {
// Neon glow effects (for hover states)
glowBlue: "hover:shadow-[0_0_20px_rgba(104,147,200,0.6)]",
glowPink: "hover:shadow-[0_0_20px_rgba(255,20,147,0.6)]",
glowSoft: "hover:shadow-[0_0_15px_rgba(104,147,200,0.3)]",
// Glassmorphism effect
glass: "bg-card/50 backdrop-blur-md border border-border/50",
// Smooth transitions
transition: "transition-all duration-200 ease-in-out",
transitionSlow: "transition-all duration-300 ease-in-out",
// Focus visible states
focusRing: "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background",
};
// Complete component compositions
export const componentStyles = {
// Day column in calendar
dayColumn: "flex-1 min-w-[180px] flex flex-col bg-background rounded-lg p-4 transition-all duration-300",
dayColumnToday: "flex-1 min-w-[180px] flex flex-col bg-primary/10 border-2 border-primary rounded-lg p-4 transition-all duration-300",
// Avatar
avatar: "w-14 h-14 rounded-lg overflow-hidden",
avatarLarge: "w-20 h-20 rounded-xl overflow-hidden",
// Divider
divider: "h-px bg-border w-full",
dividerVertical: "w-px bg-border h-full",
};
// Export all styles as a single object for convenience
export const styles = {
card: cardStyles,
button: buttonStyles,
layout: layoutStyles,
input: inputStyles,
text: textStyles,
badge: badgeStyles,
utility: utilityStyles,
component: componentStyles,
};
export default styles;

View File

@@ -1,6 +1,39 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
import { jwtDecode as decode } from "jwt-decode";
import { refreshToken } from "../api/auth.service";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
let refreshPromise = null;
export async function jwtexp(token) {
const decoded = decode(token);
const currentTime = Date.now() / 1000;
const tokenExp = decoded.exp;
if (tokenExp < currentTime || tokenExp - currentTime < 120) {
if (refreshPromise) {
console.log("Refresh already in progress, waiting...");
return await refreshPromise;
}
refreshPromise = (async () => {
try {
await refreshToken();
return true;
} catch (error) {
console.error("Failed to refresh token:", error);
return false;
} finally {
refreshPromise = null;
}
})();
return await refreshPromise;
}
return true;
}

View File

@@ -5,9 +5,7 @@ import App from './App.jsx'
import { BrowserRouter } from 'react-router'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
<BrowserRouter>
<App />
</BrowserRouter>
)

View File

@@ -136,3 +136,86 @@
0 0 16px rgba(30,132,242,0.92);
}
}
/*-- Utility Neon Classes for Cards and Buttons --*/
/* Neon glow on hover - blue */
.neon-glow-blue {
transition: box-shadow 0.3s ease-in-out;
}
.neon-glow-blue:hover {
box-shadow: 0 0 20px rgba(104, 147, 200, 0.6),
0 0 30px rgba(104, 147, 200, 0.4),
0 0 40px rgba(104, 147, 200, 0.2);
}
/* Neon glow on hover - pink */
.neon-glow-pink {
transition: box-shadow 0.3s ease-in-out;
}
.neon-glow-pink:hover {
box-shadow: 0 0 20px rgba(255, 20, 147, 0.6),
0 0 30px rgba(255, 20, 147, 0.4),
0 0 40px rgba(255, 20, 147, 0.2);
}
/* Soft neon glow - for subtle effects */
.neon-glow-soft {
transition: box-shadow 0.3s ease-in-out;
}
.neon-glow-soft:hover {
box-shadow: 0 0 15px rgba(104, 147, 200, 0.3),
0 0 25px rgba(104, 147, 200, 0.2);
}
/* Neon border - blue */
.neon-border-blue {
border: 1px solid rgba(104, 147, 200, 0.4);
box-shadow: 0 0 10px rgba(104, 147, 200, 0.2);
transition: all 0.3s ease-in-out;
}
.neon-border-blue:hover {
border-color: rgba(104, 147, 200, 0.8);
box-shadow: 0 0 15px rgba(104, 147, 200, 0.4),
0 0 25px rgba(104, 147, 200, 0.2);
}
/* Neon border - pink */
.neon-border-pink {
border: 1px solid rgba(255, 20, 147, 0.4);
box-shadow: 0 0 10px rgba(255, 20, 147, 0.2);
transition: all 0.3s ease-in-out;
}
.neon-border-pink:hover {
border-color: rgba(255, 20, 147, 0.8);
box-shadow: 0 0 15px rgba(255, 20, 147, 0.4),
0 0 25px rgba(255, 20, 147, 0.2);
}
/* Text with subtle neon effect (non-animated) */
.neon-text-blue {
color: #c6e2ff;
text-shadow: 0 0 10px rgba(30, 132, 242, 0.5),
0 0 20px rgba(30, 132, 242, 0.3);
}
.neon-text-pink {
color: #ffc5ec;
text-shadow: 0 0 10px rgba(255, 20, 147, 0.5),
0 0 20px rgba(255, 20, 147, 0.3);
}
/* Icon with neon glow on hover */
.neon-icon {
transition: filter 0.3s ease-in-out;
}
.neon-icon:hover {
filter: drop-shadow(0 0 8px rgba(104, 147, 200, 0.8))
drop-shadow(0 0 15px rgba(104, 147, 200, 0.5));
}

View File

@@ -1,9 +1,13 @@
'use client';
import { useState, useEffect } from 'react';
import '../neon.css';
import './dashboard.css';
import { MenuDock } from '@/components/ui/shadcn-io/menu-dock';
import { Home, Settings, Bell } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { jwtexp, cn } from '@/lib/utils';
import Calendar from '@/components/Calendar';
import { useNavigate } from "react-router";
import styles from '@/lib/styles';
const menuItems = [
{ label: 'home', icon: Home, onClick: () => console.log('Home clicked') },
@@ -12,77 +16,75 @@ const menuItems = [
];
export default function Dashboard() {
// Mock data from backend
const tasksFromBackend = [
{ title: "testtitle", due_date: "2025-10-14", priority: "medium", id: 1, status: "open" },
{ title: "title", due_date: "2025-10-15", priority: "high", id: 3, status: "open" },
{ title: "title title2", due_date: "2025-10-15", priority: "medium", id: 4, status: "closed" },
{ title: "title title3", due_date: "2025-10-15", priority: "medium", id: 5, status: "open" },
{ title: "title16", due_date: "2025-10-15", priority: "medium", id: 6, status: "completed" },
{ title: "super late title", due_date: "2025-10-15", priority: "low", id: 2, status: "open" }
];
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [authError, setAuthError] = useState(false);
// Get current date and week
const today = new Date();
const currentDay = today.getDay();
// Calculate the Monday of current week
const currentDate = new Date(today);
const dayOffset = currentDay === 0 ? -6 : 1 - currentDay;
currentDate.setDate(today.getDate() + dayOffset);
// Group tasks by day
const groupTasksByDay = (tasks) => {
const grouped = {};
tasks.forEach(task => {
const taskDate = new Date(task.due_date);
const dateKey = taskDate.toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
// Validate token on page load
useEffect(() => {
console.log('Dashboard useEffect triggered');
const validateAuth = async () => {
try {
console.log('Validating authentication...');
const token = localStorage.getItem('access_token');
if (token) {
console.log('Token found, validating...');
const isTokenValid = await jwtexp(token);
if (!isTokenValid) {
console.error('Token validation failed');
setAuthError(true);
setLoading(false);
return;
}
} else {
console.error('No access token found');
setAuthError(true);
setLoading(false);
return;
}
console.log('Authentication validated successfully');
} catch (error) {
console.error('Authentication validation error:', error);
setAuthError(true);
} finally {
setLoading(false);
}
grouped[dateKey].push({
id: task.id,
title: task.title,
priority: task.priority,
completed: task.status === 'closed' || task.status === 'completed',
dueDate: task.due_date
});
});
return grouped;
};
const groupedTasks = groupTasksByDay(tasksFromBackend);
// Generate days of week with dates and tasks
const daysOfWeek = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
].map((name, index) => {
const date = new Date(currentDate);
date.setDate(currentDate.getDate() + index);
const dateKey = date.toDateString();
return {
name,
date: date.getDate(),
month: date.toLocaleDateString('en-US', { month: 'short' }),
fullDate: date,
tasks: groupedTasks[dateKey] || []
};
});
validateAuth();
}, []);
// Redirect to login if authentication fails
useEffect(() => {
if (authError) {
console.log('Authentication error detected, redirecting to login...');
navigate('/auth/login', { replace: true });
}
}, [authError, navigate]);
if (loading || authError) {
return (
<div className={styles.layout.container}>
<div className="flex items-center justify-center min-h-screen">
<p className={styles.text.h3}>
{loading ? 'Loading dashboard...' : 'Redirecting...'}
</p>
</div>
</div>
);
}
return (
<div className="dashboard-container">
<div className={styles.layout.container}>
{/* Navigation Rail - Material Design 3 */}
<aside className="dashboard-sidebar">
{/* Avatar with large M3 container */}
<div className="dashboard-avatar-container">
<Avatar className="dashboard-avatar">
<aside className={styles.layout.sidebar}>
{/* Avatar */}
<div className="mb-2">
<Avatar className={styles.component.avatar}>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback className="dashboard-avatar-fallback">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
</div>
@@ -97,50 +99,19 @@ export default function Dashboard() {
</aside>
{/* Main Content Area */}
<main className="dashboard-main">
<div className="dashboard-content">
<h1 className="dashboard-header">
<main className={styles.layout.main}>
<div className={styles.layout.content}>
{/* Header with neon text */}
<h1 className={cn(styles.text.h1, "mb-6")}>
<span className="sign-pink-inline">Task&</span>
<span className="sign-inline">Coffee</span>
</h1>
<div className="dashboard-surface-container">
<div className="dashboard-week-grid border-1">
{daysOfWeek.map((day, index) => {
const isToday = day.fullDate.toDateString() === today.toDateString();
return (
<div
key={day.name}
className={`dashboard-day-column ${isToday ? 'today' : ''} dashboard-card-filled`}
>
<div className="dashboard-day-header">
<h3 className="dashboard-day-title">{day.name}</h3>
<p className="dashboard-day-date">{day.month} {day.date}</p>
</div>
<div className="dashboard-tasks-container">
{day.tasks.length > 0 ? (
day.tasks.map((task) => (
<div
key={task.id}
className={`dashboard-task-card ${task.completed ? 'completed' : ''} priority-${task.priority}`}
>
<div className="dashboard-task-header">
<span className={`dashboard-task-priority ${task.priority}`}></span>
<p className="dashboard-task-title">{task.title}</p>
</div>
</div>
))
) : (
<p className="dashboard-no-tasks">No tasks</p>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Calendar component */}
<Calendar />
<div className="dashboard-spacer"></div>
{/* Spacer */}
<div className="flex-[0.5]"></div>
</div>
</main>
</div>

View File

@@ -12,10 +12,11 @@ import {
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { login } from "@/api/auth.service"
import { Link } from "react-router"
import { Link, useNavigate } from "react-router"
export function LoginPage({ className }) {
const navigate = useNavigate()
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [isLoading, setIsLoading] = useState(false)
@@ -33,6 +34,9 @@ export function LoginPage({ className }) {
setSuccess("Login successful!")
console.log("Logged in:", result)
// Redirect to dashboard after successful login
navigate('/dashboard', { replace: true })
} catch (err) {
setError(err.detail || "Login failed. Please check your credentials.")
console.error("Login error:", err)

View File

@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { jwtexp } from '@/lib/utils';
export default function RootRedirect() {
const navigate = useNavigate();
const [checking, setChecking] = useState(true);
useEffect(() => {
const checkAuthAndRedirect = async () => {
try {
const token = localStorage.getItem('access_token');
if (token) {
const isTokenValid = await jwtexp(token);
if (isTokenValid) {
navigate('/dashboard', { replace: true });
} else {
console.error('Token is invalid, redirecting...');
navigate('/auth/login', { replace: true });
}
} else {
console.error('No token found, redirecting...');
navigate('/auth/login', { replace: true });
}
} catch (error) {
console.error('Error checking auth:', error);
navigate('/auth/login', { replace: true });
} finally {
setChecking(false);
}
};
checkAuthAndRedirect();
}, [navigate]);
if (checking) {
return (
<div className="flex items-center justify-center min-h-screen">
<p className="text-xl">Checking authentication...</p>
</div>
);
}
return null;
}

View File

@@ -1,265 +0,0 @@
/* Dashboard Layout */
.dashboard-container {
display: flex;
min-height: 100vh;
background: var(--color-background);
}
/* Navigation Rail - Material Design 3 */
.dashboard-sidebar {
width: 80px;
background: color-mix(in srgb, var(--color-card) 50%, transparent);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 1rem;
padding-bottom: 1rem;
gap: 1rem;
}
.dashboard-avatar-container {
margin-bottom: 0.5rem;
}
.dashboard-avatar {
width: 3.5rem;
height: 3.5rem;
border-radius: 1rem;
}
.dashboard-avatar-fallback {
border-radius: 1rem;
}
/* Main Content Area */
.dashboard-main {
flex: 1;
padding: 1.5rem;
background: var(--color-background);
display: flex;
flex-direction: column;
}
.dashboard-content {
max-width: 80rem;
margin-left: auto;
margin-right: auto;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
gap: 1.5rem;
}
/* Header */
.dashboard-header {
font-size: 1.875rem;
font-weight: bold;
}
/* Material Design 3 Cards Grid */
.dashboard-cards-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.dashboard-cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.dashboard-cards-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* M3 Card Variants */
.dashboard-card-filled {
background: var(--color-card);
padding: 1.5rem;
border-radius: 1.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.dashboard-card-elevated {
background: var(--color-card);
padding: 1.5rem;
border-radius: 1.5rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
transition: box-shadow 0.3s;
}
.dashboard-card-elevated:hover {
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
.dashboard-card-outlined {
background: var(--color-background);
border: 2px solid var(--color-border);
padding: 1.5rem;
border-radius: 1.5rem;
}
.dashboard-card-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dashboard-card-text {
color: var(--color-muted-foreground);
font-size: 0.875rem;
}
/* Surface Container */
.dashboard-surface-container {
background: color-mix(in srgb, var(--color-card) 30%, transparent);
padding: 1.5rem;
border-radius: 1.5rem;
flex: 3;
}
.dashboard-surface-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.dashboard-surface-text {
color: var(--color-muted-foreground);
}
/* Spacer */
.dashboard-spacer {
flex: 0.5;
}
/* Weekly View */
.dashboard-week-grid {
display: flex;
gap: 1rem;
height: 100%;
overflow-x: auto;
padding: 0.6rem;
}
.dashboard-day-column {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
background: var(--color-background);
border-radius: 1rem;
padding: 1rem;
transition: all 0.3s ease;
}
.dashboard-day-column.today {
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
border: 2px solid var(--color-primary);
}
.dashboard-day-header {
text-align: center;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0.75rem;
}
.dashboard-day-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
}
.dashboard-day-date {
font-size: 0.875rem;
color: var(--color-muted-foreground);
margin: 0;
}
.dashboard-day-column.today .dashboard-day-title {
color: var(--color-primary);
}
.dashboard-day-column.today .dashboard-day-date {
color: var(--color-primary);
font-weight: 600;
}
.dashboard-tasks-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
padding-top: 4px;
}
.dashboard-task-card {
background: var(--color-card);
padding: 0.75rem;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
font-size: 0.875rem;
transition: transform 0.2s;
}
.dashboard-task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.dashboard-task-card.completed {
opacity: 0.6;
text-decoration: line-through;
}
.dashboard-task-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dashboard-task-priority {
width: 4px;
height: 4px;
border-radius: 50%;
flex-shrink: 0;
}
.dashboard-task-priority.low {
background: #10b981; /* green */
}
.dashboard-task-priority.medium {
background: #f59e0b; /* orange */
}
.dashboard-task-priority.high {
background: #ef4444; /* red */
}
.dashboard-task-priority.critical {
background: #a90404; /* red */
}
.dashboard-task-title {
margin: 0;
font-size: 0.875rem;
color: var(--color-foreground);
flex: 1;
}
.dashboard-no-tasks {
color: var(--color-muted-foreground);
font-size: 0.875rem;
text-align: center;
padding: 2rem 0;
font-style: italic;
}