Compare commits
110 Commits
2927a8344f
...
feat/wicke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa99a167a5 | ||
|
|
9b33b73bb4 | ||
|
|
bc47027c22 | ||
|
|
7ef934d8a8 | ||
|
|
974edce238 | ||
|
|
7009245ab0 | ||
|
|
a984243fe2 | ||
|
|
01b545c83e | ||
|
|
cb0de11de1 | ||
|
|
1daeee0eb1 | ||
|
|
ff32b6f21c | ||
|
|
8544802b7f | ||
|
|
0abf8d9aa9 | ||
|
|
da420ccd76 | ||
|
|
24a4560634 | ||
|
|
ed5ff4b8fd | ||
|
|
ffd9d31c94 | ||
|
|
eee7855d36 | ||
|
|
ea7517457a | ||
|
|
6d3ee7e400 | ||
|
|
15e3c7dff5 | ||
|
|
46c8faf10c | ||
|
|
a3689948ae | ||
|
|
d18c1a7b3e | ||
|
|
a0e53325f7 | ||
|
|
61079c8171 | ||
|
|
e1edc1afcd | ||
|
|
bd6c0d5ed8 | ||
|
|
5fc790cd42 | ||
|
|
ec5095d372 | ||
|
|
31f5d2188d | ||
|
|
e866e259bb | ||
|
|
54e99c2f7e | ||
|
|
3ba5710d8f | ||
|
|
0f6d75b1e8 | ||
|
|
ee4399aa46 | ||
|
|
839120f2f4 | ||
|
|
6965eb89fd | ||
|
|
0ba6634e99 | ||
|
|
ca81d2360c | ||
|
|
afe074c8a1 | ||
|
|
e838e91162 | ||
|
|
aa5dccb508 | ||
|
|
3414057795 | ||
|
|
1acc3e09d4 | ||
|
|
48c648e2ca | ||
|
|
b493027c86 | ||
|
|
6ea3005a65 | ||
|
|
476438c5c4 | ||
|
|
055640b73e | ||
|
|
a7e83d79d7 | ||
|
|
9404f10c93 | ||
|
|
907e51fc60 | ||
|
|
56b0a45719 | ||
|
|
58dbcf3a6a | ||
|
|
394190ace8 | ||
|
|
2a2c249a54 | ||
|
|
3fa9249bc8 | ||
|
|
b9576b91f5 | ||
|
|
be3d80e541 | ||
|
|
0b58768a24 | ||
|
|
911e821948 | ||
|
|
2fd93dc3bf | ||
|
|
a3dce5c363 | ||
|
|
7cdb9f468c | ||
|
|
4e2445198f | ||
|
|
49ad935855 | ||
|
|
1989cbf6af | ||
|
|
784d5158b4 | ||
|
|
b2d1cb5fa4 | ||
|
|
74f8350abe | ||
|
|
96734480d5 | ||
|
|
f6675bd5e4 | ||
|
|
a8a8c55041 | ||
|
|
9513867614 | ||
|
|
30f3ecc6ad | ||
|
|
2f80fe8321 | ||
|
|
e084839289 | ||
|
|
81bd30a7af | ||
|
|
a66f3dd17a | ||
|
|
6f31f99def | ||
|
|
7c7db939a2 | ||
|
|
1806c875b5 | ||
|
|
70ad65707e | ||
|
|
b9b7ac4ba1 | ||
|
|
cc7d71ad69 | ||
|
|
062820873f | ||
|
|
c226adc36c | ||
|
|
0879b3c924 | ||
|
|
884c6a8262 | ||
|
|
6b37c67b47 | ||
|
|
a43c47a5db | ||
|
|
a0f008bbd4 | ||
|
|
6385c39c10 | ||
|
|
6fbd24e36e | ||
|
|
632055c44d | ||
|
|
03717dc95b | ||
|
|
7f4aded5aa | ||
|
|
74484597d9 | ||
|
|
a558f7fc9a | ||
|
|
1c7a62acde | ||
|
|
98efdb44ac | ||
|
|
5454e23220 | ||
|
|
71b5d43e89 | ||
|
|
c7d28dca71 | ||
|
|
b5b5ca1cc0 | ||
|
|
ef9cfd6cdf | ||
|
|
74a1c446c4 | ||
|
|
1324463c2f | ||
|
|
3c6389c6e2 |
777
.cursor/rules/design.mdc
Executable file
777
.cursor/rules/design.mdc
Executable file
@@ -0,0 +1,777 @@
|
||||
---
|
||||
description: Use this rule when asked to do any frontend or UI design
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
|
||||
---
|
||||
description: Use this rule when asked to do any frontend or UI design
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
0
.dockerignore
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
64
.editorconfig
Executable file
64
.editorconfig
Executable file
@@ -0,0 +1,64 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# We recommend you to have these uncommented (set to true).
|
||||
# If you want to support older versions of Ruby, set this to 1.9
|
||||
# ruby_version = 2.7
|
||||
# If you want to support older versions of JavaScript, set this to 5
|
||||
# javascript_version = 6
|
||||
|
||||
# Extend from global settings
|
||||
[*.{rb,erb}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,jsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{json,json5,jsonc}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{css,scss,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{html,htm,erb}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{md,markdown}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
49
.env.example
Executable file
49
.env.example
Executable file
@@ -0,0 +1,49 @@
|
||||
# Application data
|
||||
RAILS_ENV=development
|
||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||
APP_NAME=Aperonight
|
||||
|
||||
# Database Configuration for production and development
|
||||
DB_HOST=localhost
|
||||
DB_ROOT_PASSWORD=root
|
||||
DB_DATABASE=aperonight
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
# Test database
|
||||
DB_TEST_ADAPTER=sqlite3
|
||||
DB_TEST_DATABASE=aperonight_test
|
||||
DB_TEST_USERNAME=root
|
||||
DB_TEST_USERNAME=root
|
||||
|
||||
# Mailer Configuration (for Devise and tests)
|
||||
MAILER_DEFAULT_URL_OPTIONS=http://localhost:3000
|
||||
# Test environment will use MailHog by default on 127.0.0.1:1025
|
||||
SMTP_ADDRESS=127.0.0.1
|
||||
SMTP_PORT=1025
|
||||
# Optional auth (usually not required for MailHog)
|
||||
# SMTP_USER_NAME=
|
||||
# SMTP_PASSWORD=
|
||||
# SMTP_DOMAIN=localhost
|
||||
SMTP_AUTHENTICATION=plain
|
||||
SMTP_ENABLE_STARTTLS=false
|
||||
|
||||
# Production SMTP Configuration (set these in .env.production)
|
||||
# SMTP_ADDRESS=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USERNAME=your_smtp_username
|
||||
# SMTP_PASSWORD=your_smtp_password
|
||||
# SMTP_AUTHENTICATION=plain
|
||||
# SMTP_DOMAIN=example.com
|
||||
# SMTP_STARTTLS=true
|
||||
|
||||
# Application variables
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_51S1M7BJWx6G2LLIXYpTvi0hxMpZ4tZSxkmr2Wbp1dQ73MKNp4Tyu4xFJBqLXK5nn4E0nEf2tdgJqEwWZLosO3QGn00kMvjXWGW
|
||||
STRIPE_SECRET_KEY=sk_test_51S1M7BJWx6G2LLIXK2pdLpRKb9Mgd3sZ30N4ueVjHepgxQKbWgMVJoa4v4ESzHQ6u6zJjO4jUvgLYPU1QLyAiFTN00sGz2ortW
|
||||
STRIPE_WEBHOOK_SECRET=LaReunion974
|
||||
|
||||
# Scaleway login
|
||||
OPENAI_API_KEY=f66dbb5f-9770-4f81-b2ea-eb7370bc9aa5
|
||||
OPENAI_BASE_URL=https://api.scaleway.ai/v1
|
||||
OPENAI_MODEL=devstral-small-2505
|
||||
0
.gitattributes
vendored
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
0
.github/workflows/ci.yml
vendored
Normal file → Executable file
11
.gitignore
vendored
Normal file → Executable file
11
.gitignore
vendored
Normal file → Executable file
@@ -8,7 +8,7 @@
|
||||
/.bundle
|
||||
|
||||
# Ignore all environment files.
|
||||
/.env*
|
||||
/.env
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
@@ -37,3 +37,12 @@
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
/node_modules
|
||||
|
||||
# Docker shared directory
|
||||
/data
|
||||
|
||||
# Ignore generated reports
|
||||
/test/reports
|
||||
|
||||
# Ignore .fuse file
|
||||
.fuse_hidden*
|
||||
0
.kamal/secrets
Normal file → Executable file
0
.kamal/secrets
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.node-version
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.rubocop.yml
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
0
.ruby-version
Normal file → Executable file
@@ -0,0 +1,738 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - AperoNight | Plateforme Événementielle Premium</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="aperonight_premium_light_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--background) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
/* Light theme background patterns */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 2px 2px, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
opacity: 0.3;
|
||||
z-index: 0;
|
||||
animation: dotFlow 30s linear infinite;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 48%, var(--connection-color) 50%, transparent 52%),
|
||||
linear-gradient(0deg, transparent 48%, var(--connection-color) 50%, transparent 52%);
|
||||
background-size: 100px 100px;
|
||||
opacity: 0.12;
|
||||
z-index: 0;
|
||||
animation: connectionFlow 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dotFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(40px, 40px); }
|
||||
}
|
||||
|
||||
@keyframes connectionFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(100px, 100px); }
|
||||
}
|
||||
|
||||
/* Page entrance orchestration */
|
||||
.page-container {
|
||||
animation: pageLoad 1000ms cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand reveal animation */
|
||||
.brand-container {
|
||||
animation: brandReveal 1400ms ease-out 300ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
@keyframes brandReveal {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Premium card elevation - light theme */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-2xl) !important;
|
||||
animation: cardElevate 800ms cubic-bezier(0.34, 1.56, 0.64, 1) 600ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: all 400ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(59, 130, 246, 0.05),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.login-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: var(--hover-lift) var(--hover-scale);
|
||||
box-shadow: var(--shadow-2xl), var(--shadow-electric);
|
||||
}
|
||||
|
||||
@keyframes cardElevate {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional input styling - light theme */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1.25rem 3.5rem 1.25rem 1.25rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--input);
|
||||
color: var(--card-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 250ms ease-out;
|
||||
outline: none;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-electric), var(--focus-ring);
|
||||
transform: scale(1.01);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Security toggle with premium feel - light theme */
|
||||
.security-toggle {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
opacity: 0.7;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.security-toggle:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
/* Flat button styling - light theme */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
background: var(--primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-display);
|
||||
cursor: pointer;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: var(--hover-lift);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Premium ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transition: width 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
height 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Sophisticated checkbox - light theme */
|
||||
.premium-checkbox {
|
||||
appearance: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.premium-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: securityCheck 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.premium-checkbox:checked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 5px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@keyframes securityCheck {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Professional link styling - light theme */
|
||||
.premium-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 250ms ease-out;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.premium-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 250ms ease-out;
|
||||
}
|
||||
|
||||
.premium-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.premium-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states - light theme */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 400ms cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
box-shadow: 0 0 0 3px var(--success-light);
|
||||
animation: validationSuccess 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes validationSuccess {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); border-color: var(--success); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Premium loading states - light theme */
|
||||
.skeleton {
|
||||
background: var(--muted);
|
||||
animation: skeletonPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Logo styling - light theme */
|
||||
.logo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-glow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Trust indicators - light theme */
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--success);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.trust-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Professional footer - light theme */
|
||||
.pro-footer {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
border: 1px solid rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Light theme brand colors */
|
||||
.brand-text-primary {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.brand-text-secondary {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.brand-text-muted {
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--primary) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
|
||||
/* Responsive enhancements */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advanced accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.login-card {
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-lg">
|
||||
<!-- Premium Brand Section - Light Theme -->
|
||||
<div class="brand-container text-center mb-10">
|
||||
<div class="relative inline-block mb-6">
|
||||
<div class="logo-glow"></div>
|
||||
<div class="relative w-20 h-20 mx-auto bg-blue-600 rounded-2xl flex items-center justify-center logo-container">
|
||||
<i data-lucide="calendar-check" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold brand-text-primary mb-2 font-display">AperoNight</h1>
|
||||
<p class="brand-text-secondary text-lg font-medium mb-2">Plateforme Événementielle Premium</p>
|
||||
<p class="brand-text-muted text-sm opacity-90">Connexion sécurisée • Interface professionnelle</p>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="trust-badge">
|
||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||
<span>Connexion Sécurisée</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-2 font-display">Accès Dashboard</h2>
|
||||
<p class="text-gray-600 text-sm">Gérez vos événements en toute simplicité</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<!-- Email professionnel -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Email professionnel</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe sécurisé -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe sécurisé</label>
|
||||
<button type="button" class="security-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="lock" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Options de connexion -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer group">
|
||||
<input type="checkbox" class="premium-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
|
||||
Maintenir la connexion
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-1 text-xs text-gray-500">
|
||||
<i data-lucide="timer" class="w-3 h-3"></i>
|
||||
<span>30 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de connexion premium -->
|
||||
<button type="submit" class="login-button group">
|
||||
<span class="relative z-10 flex items-center justify-center gap-2">
|
||||
<i data-lucide="log-in" class="w-5 h-5"></i>
|
||||
Accéder au Dashboard
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Options de récupération -->
|
||||
<div class="text-center space-y-3">
|
||||
<a href="#" class="premium-link text-sm">Mot de passe oublié ?</a>
|
||||
<div class="flex items-center justify-center space-x-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="smartphone" class="w-3 h-3"></i>
|
||||
2FA disponible
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="key" class="w-3 h-3"></i>
|
||||
SSO Enterprise
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Professional Footer - Light Theme -->
|
||||
<div class="pro-footer text-center space-y-3">
|
||||
<div class="flex items-center justify-center space-x-6 text-sm">
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="life-buoy" class="w-4 h-4"></i>
|
||||
Support Pro
|
||||
</a>
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
Sécurité Renforcée
|
||||
</a>
|
||||
<a href="#" class="footer-link transition-colors flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-4 h-4"></i>
|
||||
API Premium
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs status-text">© 2024 AperoNight • Plateforme Événementielle Premium • Tous droits réservés</p>
|
||||
<div class="flex items-center justify-center space-x-2 text-xs status-text">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
Système opérationnel
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>99.9% uptime</span>
|
||||
<span>•</span>
|
||||
<span>GDPR compliant</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Enhanced password toggle
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.security-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'unlock');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'lock');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Professional form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Professional email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Email invalide', 'error');
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Secure password validation
|
||||
if (passwordField.value.length < 8) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Mot de passe trop court (min. 8 caractères)', 'error');
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Premium loading state
|
||||
const button = document.querySelector('.login-button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
<span>Connexion sécurisée...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
showNotification('Connexion réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Professional notification system
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `
|
||||
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-2xl max-w-sm
|
||||
${type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}
|
||||
transform transition-all duration-300 ease-out translate-x-full
|
||||
`;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
lucide.createIcons();
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Enhanced floating label behavior
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.style.background = 'white';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.style.background = 'var(--input)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Professional interaction tracking
|
||||
console.log('🌟 AperoNight Premium Light Login Interface Loaded');
|
||||
console.log('✅ Security features: 2FA, SSO, GDPR compliance');
|
||||
console.log('🎨 Theme: Professional Event Platform - Light Mode');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,125 @@
|
||||
:root {
|
||||
/* AperoNight Premium Light Theme - Professional Event Platform */
|
||||
|
||||
/* Base Colors - Clean Light Background with Professional Accents */
|
||||
--background: oklch(0.9800 0.0050 240);
|
||||
--foreground: oklch(0.1500 0.0200 240);
|
||||
--surface: oklch(0.9600 0.0080 240);
|
||||
--surface-elevated: oklch(0.9400 0.0120 240);
|
||||
|
||||
/* Card & Dialog surfaces */
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1500 0.0200 240);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1500 0.0200 240);
|
||||
|
||||
/* Primary - Professional Electric Blue */
|
||||
--primary: oklch(0.5200 0.2200 220);
|
||||
--primary-foreground: oklch(0.9900 0.0050 220);
|
||||
--primary-hover: oklch(0.4600 0.2400 220);
|
||||
--primary-light: oklch(0.9200 0.1000 220);
|
||||
--primary-dark: oklch(0.3800 0.2600 220);
|
||||
|
||||
/* Secondary - Sophisticated Light Gray */
|
||||
--secondary: oklch(0.9200 0.0100 240);
|
||||
--secondary-foreground: oklch(0.3000 0.0300 240);
|
||||
--secondary-hover: oklch(0.8800 0.0150 240);
|
||||
|
||||
/* Accent - Vibrant Cyan (Events Energy) */
|
||||
--accent: oklch(0.6500 0.2400 200);
|
||||
--accent-foreground: oklch(0.9900 0.0050 200);
|
||||
--accent-light: oklch(0.9400 0.1200 200);
|
||||
--accent-dark: oklch(0.5000 0.2800 200);
|
||||
|
||||
/* Success - Event Success Green */
|
||||
--success: oklch(0.6000 0.2000 140);
|
||||
--success-foreground: oklch(0.9800 0.0100 140);
|
||||
--success-light: oklch(0.9600 0.0800 140);
|
||||
|
||||
/* Warning - Premium Amber */
|
||||
--warning: oklch(0.7200 0.1800 60);
|
||||
--warning-foreground: oklch(0.2500 0.0400 60);
|
||||
--warning-light: oklch(0.9600 0.0800 60);
|
||||
|
||||
/* Error - Professional Red */
|
||||
--destructive: oklch(0.5600 0.2200 20);
|
||||
--destructive-foreground: oklch(0.9800 0.0100 20);
|
||||
--destructive-light: oklch(0.9600 0.1000 20);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.9400 0.0100 240);
|
||||
--muted-foreground: oklch(0.5200 0.0300 240);
|
||||
--muted-dark: oklch(0.8800 0.0200 240);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8800 0.0200 240);
|
||||
--input: oklch(0.9800 0.0080 240);
|
||||
--input-border: oklch(0.8600 0.0300 240);
|
||||
--ring: oklch(0.5200 0.2200 220);
|
||||
|
||||
/* Typography - Premium Event Platform */
|
||||
--font-sans: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Space Grotesk', 'Outfit', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.75rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Light theme shadow system */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 15% 15% / 0.08), 0 1px 2px -1px hsl(240 15% 15% / 0.06);
|
||||
--shadow-sm: 0 2px 6px -1px hsl(240 15% 15% / 0.10), 0 2px 4px -2px hsl(240 15% 15% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 15% 15% / 0.12), 0 2px 4px -2px hsl(240 15% 15% / 0.08);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 15% 15% / 0.14), 0 4px 6px -2px hsl(240 15% 15% / 0.10);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 15% 15% / 0.16), 0 8px 8px -4px hsl(240 15% 15% / 0.08);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 15% 15% / 0.18), 0 8px 16px -8px hsl(240 15% 15% / 0.10);
|
||||
--shadow-2xl: 0 32px 64px -12px hsl(240 15% 15% / 0.22);
|
||||
|
||||
/* Subtle accent shadows for light theme */
|
||||
--shadow-electric: 0 4px 16px -2px hsl(220 80% 60% / 0.15), 0 2px 8px -2px hsl(220 80% 60% / 0.10);
|
||||
--shadow-accent: 0 4px 16px -2px hsl(200 80% 60% / 0.18), 0 2px 8px -2px hsl(200 80% 60% / 0.12);
|
||||
|
||||
/* Light theme gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg,
|
||||
oklch(0.9800 0.0050 240) 0%,
|
||||
oklch(0.9600 0.0080 235) 25%,
|
||||
oklch(0.9400 0.0120 230) 50%,
|
||||
oklch(0.9600 0.0080 225) 75%,
|
||||
oklch(0.9800 0.0050 220) 100%);
|
||||
--gradient-card: linear-gradient(135deg,
|
||||
oklch(1.0000 0 0) 0%,
|
||||
oklch(0.9900 0.0050 235) 100%);
|
||||
|
||||
/* Light theme pattern overlays */
|
||||
--grid-color: oklch(0.8500 0.0300 240);
|
||||
--dot-color: oklch(0.8000 0.0400 220);
|
||||
--connection-color: oklch(0.7500 0.0800 210);
|
||||
|
||||
/* Light glassmorphism */
|
||||
--glass-bg: oklch(1.0000 0 0 / 0.85);
|
||||
--glass-border: oklch(0.8800 0.0200 240 / 0.25);
|
||||
--glass-backdrop: blur(16px) saturate(180%);
|
||||
|
||||
/* Professional interaction states */
|
||||
--hover-lift: translateY(-2px);
|
||||
--hover-scale: scale(1.02);
|
||||
--focus-ring: 0 0 0 3px var(--ring);
|
||||
|
||||
/* Event-specific colors for light theme */
|
||||
--event-vip: oklch(0.6800 0.2200 45);
|
||||
--event-premium: oklch(0.5800 0.2000 280);
|
||||
--event-standard: oklch(0.6200 0.1600 160);
|
||||
--event-available: oklch(0.6000 0.1800 140);
|
||||
--event-limited: oklch(0.7000 0.1800 50);
|
||||
--event-sold-out: oklch(0.5800 0.2000 15);
|
||||
|
||||
/* Radius variations */
|
||||
--radius-xs: calc(var(--radius) - 4px);
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
--radius-2xl: calc(var(--radius) + 12px);
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
710
.superdesign/design_iterations/aperonight_premium_login_1.html
Normal file
710
.superdesign/design_iterations/aperonight_premium_login_1.html
Normal file
@@ -0,0 +1,710 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - AperoNight | Plateforme Événementielle Premium</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="aperonight_premium_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: oklch(0.1200 0.0300 240) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
/* Advanced background patterns */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 2px 2px, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
animation: dotFlow 30s linear infinite;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 48%, var(--connection-color) 50%, transparent 52%),
|
||||
linear-gradient(0deg, transparent 48%, var(--connection-color) 50%, transparent 52%);
|
||||
background-size: 100px 100px;
|
||||
opacity: 0.15;
|
||||
z-index: 0;
|
||||
animation: connectionFlow 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dotFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(40px, 40px); }
|
||||
}
|
||||
|
||||
@keyframes connectionFlow {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(100px, 100px); }
|
||||
}
|
||||
|
||||
/* Page entrance orchestration */
|
||||
.page-container {
|
||||
animation: pageLoad 1000ms cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand reveal animation */
|
||||
.brand-container {
|
||||
animation: brandReveal 1400ms ease-out 300ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
@keyframes brandReveal {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Premium card elevation */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-2xl) !important;
|
||||
animation: cardElevate 800ms cubic-bezier(0.34, 1.56, 0.64, 1) 600ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: all 400ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.login-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: var(--hover-lift) var(--hover-scale);
|
||||
box-shadow: var(--shadow-2xl), var(--shadow-electric);
|
||||
}
|
||||
|
||||
@keyframes cardElevate {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional input styling */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1.25rem 3.5rem 1.25rem 1.25rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--input);
|
||||
color: var(--card-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 250ms ease-out;
|
||||
outline: none;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-electric), var(--focus-ring);
|
||||
transform: scale(1.01);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-12px) scale(0.85);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Security toggle with premium feel */
|
||||
.security-toggle {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
opacity: 0.7;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.security-toggle:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
/* Flat button styling */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
background: var(--primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-display);
|
||||
cursor: pointer;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: var(--hover-lift);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Premium ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transition: width 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
height 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Sophisticated checkbox */
|
||||
.premium-checkbox {
|
||||
appearance: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.premium-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: securityCheck 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.premium-checkbox:checked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 5px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@keyframes securityCheck {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Professional link styling */
|
||||
.premium-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 250ms ease-out;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.premium-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 250ms ease-out;
|
||||
}
|
||||
|
||||
.premium-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.premium-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 400ms cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
box-shadow: 0 0 0 3px var(--success-light);
|
||||
animation: validationSuccess 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes validationSuccess {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); border-color: var(--success); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Premium loading states */
|
||||
.skeleton {
|
||||
background: var(--muted);
|
||||
animation: skeletonPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Logo styling */
|
||||
.logo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-glow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Trust indicators */
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--success);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.trust-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Professional footer */
|
||||
.pro-footer {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive enhancements */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advanced accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.login-card {
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-lg">
|
||||
<!-- Premium Brand Section -->
|
||||
<div class="brand-container text-center mb-10">
|
||||
<div class="relative inline-block mb-6">
|
||||
<div class="logo-glow"></div>
|
||||
<div class="relative w-20 h-20 mx-auto bg-blue-600 rounded-2xl flex items-center justify-center logo-container">
|
||||
<i data-lucide="calendar-check" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 font-display">AperoNight</h1>
|
||||
<p class="text-blue-200 text-lg font-medium mb-2">Plateforme Événementielle Premium</p>
|
||||
<p class="text-blue-300 text-sm opacity-90">Connexion sécurisée • Interface professionnelle</p>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="trust-badge">
|
||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||
<span>Connexion Sécurisée</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-2 font-display">Accès Dashboard</h2>
|
||||
<p class="text-gray-600 text-sm">Gérez vos événements en toute simplicité</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<!-- Email professionnel -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Email professionnel</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe sécurisé -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe sécurisé</label>
|
||||
<button type="button" class="security-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="lock" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Options de connexion -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer group">
|
||||
<input type="checkbox" class="premium-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
|
||||
Maintenir la connexion
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-1 text-xs text-gray-500">
|
||||
<i data-lucide="timer" class="w-3 h-3"></i>
|
||||
<span>30 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de connexion premium -->
|
||||
<button type="submit" class="login-button group">
|
||||
<span class="relative z-10 flex items-center justify-center gap-2">
|
||||
<i data-lucide="log-in" class="w-5 h-5"></i>
|
||||
Accéder au Dashboard
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Options de récupération -->
|
||||
<div class="text-center space-y-3">
|
||||
<a href="#" class="premium-link text-sm">Mot de passe oublié ?</a>
|
||||
<div class="flex items-center justify-center space-x-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="smartphone" class="w-3 h-3"></i>
|
||||
2FA disponible
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="key" class="w-3 h-3"></i>
|
||||
SSO Enterprise
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Professional Footer -->
|
||||
<div class="pro-footer text-center space-y-3">
|
||||
<div class="flex items-center justify-center space-x-6 text-sm text-blue-200">
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="life-buoy" class="w-4 h-4"></i>
|
||||
Support Pro
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
Sécurité Renforcée
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-colors flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-4 h-4"></i>
|
||||
API Premium
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs text-blue-300">© 2024 AperoNight • Plateforme Événementielle Premium • Tous droits réservés</p>
|
||||
<div class="flex items-center justify-center space-x-2 text-xs text-blue-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
Système opérationnel
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>99.9% uptime</span>
|
||||
<span>•</span>
|
||||
<span>GDPR compliant</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Enhanced password toggle
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.security-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'unlock');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'lock');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Professional form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Professional email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Email invalide', 'error');
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Secure password validation
|
||||
if (passwordField.value.length < 8) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
showNotification('Mot de passe trop court (min. 8 caractères)', 'error');
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Premium loading state
|
||||
const button = document.querySelector('.login-button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
<span>Connexion sécurisée...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
showNotification('Connexion réussie ! Redirection...', 'success');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Professional notification system
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `
|
||||
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-2xl max-w-sm
|
||||
${type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}
|
||||
transform transition-all duration-300 ease-out translate-x-full
|
||||
`;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
lucide.createIcons();
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Enhanced floating label behavior
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.style.background = 'white';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.style.background = 'var(--input)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Professional interaction tracking
|
||||
console.log('🚀 AperoNight Premium Login Interface Loaded');
|
||||
console.log('✅ Security features: 2FA, SSO, GDPR compliance');
|
||||
console.log('🎨 Theme: Professional Event Platform');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
125
.superdesign/design_iterations/aperonight_premium_theme.css
Normal file
125
.superdesign/design_iterations/aperonight_premium_theme.css
Normal file
@@ -0,0 +1,125 @@
|
||||
:root {
|
||||
/* AperoNight Premium Theme - Telecom Inspired */
|
||||
|
||||
/* Base Colors - Sophisticated Navy & Electric Accents */
|
||||
--background: oklch(0.1200 0.0300 240);
|
||||
--foreground: oklch(0.9500 0.0100 240);
|
||||
--surface: oklch(0.1600 0.0400 240);
|
||||
--surface-elevated: oklch(0.2000 0.0500 240);
|
||||
|
||||
/* Card & Dialog surfaces */
|
||||
--card: oklch(0.9800 0.0100 240);
|
||||
--card-foreground: oklch(0.1500 0.0200 240);
|
||||
--popover: oklch(0.9800 0.0100 240);
|
||||
--popover-foreground: oklch(0.1500 0.0200 240);
|
||||
|
||||
/* Primary - Premium Electric Blue */
|
||||
--primary: oklch(0.5500 0.2400 220);
|
||||
--primary-foreground: oklch(0.9800 0.0100 220);
|
||||
--primary-hover: oklch(0.4800 0.2600 220);
|
||||
--primary-light: oklch(0.8500 0.1200 220);
|
||||
--primary-dark: oklch(0.3500 0.2800 220);
|
||||
|
||||
/* Secondary - Sophisticated Slate */
|
||||
--secondary: oklch(0.8800 0.0200 240);
|
||||
--secondary-foreground: oklch(0.2500 0.0300 240);
|
||||
--secondary-hover: oklch(0.8200 0.0300 240);
|
||||
|
||||
/* Accent - Vibrant Cyan (Events Energy) */
|
||||
--accent: oklch(0.6800 0.2600 200);
|
||||
--accent-foreground: oklch(0.9800 0.0100 200);
|
||||
--accent-light: oklch(0.8800 0.1400 200);
|
||||
--accent-dark: oklch(0.4500 0.3000 200);
|
||||
|
||||
/* Success - Event Success Green */
|
||||
--success: oklch(0.6200 0.2200 140);
|
||||
--success-foreground: oklch(0.9600 0.0200 140);
|
||||
--success-light: oklch(0.9200 0.1000 140);
|
||||
|
||||
/* Warning - Premium Gold */
|
||||
--warning: oklch(0.7500 0.2000 60);
|
||||
--warning-foreground: oklch(0.2000 0.0300 60);
|
||||
--warning-light: oklch(0.9400 0.1000 60);
|
||||
|
||||
/* Error - Professional Red */
|
||||
--destructive: oklch(0.5800 0.2400 20);
|
||||
--destructive-foreground: oklch(0.9700 0.0200 20);
|
||||
--destructive-light: oklch(0.9300 0.1200 20);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.8800 0.0200 240);
|
||||
--muted-foreground: oklch(0.4800 0.0400 240);
|
||||
--muted-dark: oklch(0.7500 0.0300 240);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8400 0.0300 240);
|
||||
--input: oklch(0.9600 0.0200 240);
|
||||
--input-border: oklch(0.8200 0.0400 240);
|
||||
--ring: oklch(0.5500 0.2400 220);
|
||||
|
||||
/* Typography - Premium Event Platform */
|
||||
--font-sans: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Space Grotesk', 'Outfit', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.75rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Premium shadow system */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 30% 8% / 0.08), 0 1px 2px -1px hsl(240 30% 8% / 0.06);
|
||||
--shadow-sm: 0 2px 6px -1px hsl(240 30% 8% / 0.10), 0 2px 4px -2px hsl(240 30% 8% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 30% 8% / 0.12), 0 2px 4px -2px hsl(240 30% 8% / 0.08);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 30% 8% / 0.14), 0 4px 6px -2px hsl(240 30% 8% / 0.10);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 30% 8% / 0.16), 0 8px 8px -4px hsl(240 30% 8% / 0.08);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 30% 8% / 0.20), 0 8px 16px -8px hsl(240 30% 8% / 0.12);
|
||||
--shadow-2xl: 0 32px 64px -12px hsl(240 30% 8% / 0.25);
|
||||
|
||||
/* Electric/Glow shadows for premium effects */
|
||||
--shadow-electric: 0 4px 16px -2px hsl(220 100% 70% / 0.20), 0 2px 8px -2px hsl(220 100% 70% / 0.15);
|
||||
--shadow-accent: 0 4px 16px -2px hsl(200 100% 70% / 0.25), 0 2px 8px -2px hsl(200 100% 70% / 0.20);
|
||||
|
||||
/* Premium gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg,
|
||||
oklch(0.1200 0.0300 240) 0%,
|
||||
oklch(0.1000 0.0400 235) 25%,
|
||||
oklch(0.0800 0.0500 230) 50%,
|
||||
oklch(0.1000 0.0400 225) 75%,
|
||||
oklch(0.1200 0.0300 220) 100%);
|
||||
--gradient-card: linear-gradient(135deg,
|
||||
oklch(0.9900 0.0100 240) 0%,
|
||||
oklch(0.9700 0.0200 235) 100%);
|
||||
|
||||
/* Tech pattern overlays */
|
||||
--grid-color: oklch(0.3000 0.0500 240);
|
||||
--dot-color: oklch(0.2500 0.0600 220);
|
||||
--connection-color: oklch(0.4000 0.1200 210);
|
||||
|
||||
/* Glass morphism for premium feel */
|
||||
--glass-bg: oklch(0.9800 0.0100 240 / 0.80);
|
||||
--glass-border: oklch(0.8500 0.0300 240 / 0.30);
|
||||
--glass-backdrop: blur(16px) saturate(200%);
|
||||
|
||||
/* Professional states */
|
||||
--hover-lift: translateY(-2px);
|
||||
--hover-scale: scale(1.02);
|
||||
--focus-ring: 0 0 0 3px var(--ring);
|
||||
|
||||
/* Event-specific colors */
|
||||
--event-vip: oklch(0.6500 0.2500 45);
|
||||
--event-premium: oklch(0.5500 0.2200 280);
|
||||
--event-standard: oklch(0.6000 0.1800 160);
|
||||
--event-available: oklch(0.6200 0.2000 140);
|
||||
--event-limited: oklch(0.7200 0.2000 50);
|
||||
--event-sold-out: oklch(0.5500 0.2200 15);
|
||||
|
||||
/* Radius variations */
|
||||
--radius-xs: calc(var(--radius) - 4px);
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
--radius-2xl: calc(var(--radius) + 12px);
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
385
.superdesign/design_iterations/dashboard_2.html
Normal file
385
.superdesign/design_iterations/dashboard_2.html
Normal file
@@ -0,0 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Minimalist Typography Design</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a1a1a;
|
||||
--secondary: #6b7280;
|
||||
--accent: #3b82f6;
|
||||
--background: #fafafa;
|
||||
--surface: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
.minimal-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.minimal-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.text-subtle { color: var(--secondary); }
|
||||
.bg-subtle { background-color: #f8fafc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="min-h-screen">
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-gray-200 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<h1 class="text-xl font-semibold">ApéroNight</h1>
|
||||
<div class="flex space-x-6">
|
||||
<a href="#" class="text-gray-900 border-b-2 border-blue-500 pb-1">Dashboard</a>
|
||||
<a href="#" class="text-gray-500 hover:text-gray-900">Événements</a>
|
||||
<a href="#" class="text-gray-500 hover:text-gray-900">Profil</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="p-2 rounded-lg hover:bg-gray-100">
|
||||
<i data-lucide="bell" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-12 fade-in">
|
||||
<h1 class="text-4xl font-bold mb-2">Bonjour, Marie</h1>
|
||||
<p class="text-lg text-subtle">Voici un aperçu de vos activités et événements</p>
|
||||
</div>
|
||||
|
||||
<!-- Critical Alert - Draft Tickets -->
|
||||
<div class="minimal-card rounded-lg p-6 mb-8 border-l-4 border-orange-400 bg-orange-50 fade-in">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="p-2 bg-orange-100 rounded-lg">
|
||||
<i data-lucide="clock" class="w-5 h-5 text-orange-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Action requise</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">2 billets en attente de paiement expirent dans 25 minutes</p>
|
||||
|
||||
<!-- Ticket Details -->
|
||||
<div class="bg-white rounded-lg p-3 mb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-sm">Soirée Jazz au Sunset</span>
|
||||
<span class="text-xs text-gray-500 ml-2">2 billets • €70</span>
|
||||
</div>
|
||||
<span class="mono text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded">1/3 tentatives</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 transition-colors">
|
||||
Payer maintenant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-12 fade-in">
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Réservations</span>
|
||||
<i data-lucide="calendar-check" class="w-4 h-4 text-green-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">05</div>
|
||||
<div class="text-xs text-subtle">+2 ce mois</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Aujourd'hui</span>
|
||||
<i data-lucide="clock" class="w-4 h-4 text-blue-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">03</div>
|
||||
<div class="text-xs text-subtle">événements</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">Demain</span>
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-purple-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">07</div>
|
||||
<div class="text-xs text-subtle">événements</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-medium text-subtle">À venir</span>
|
||||
<i data-lucide="trending-up" class="w-4 h-4 text-orange-500"></i>
|
||||
</div>
|
||||
<div class="metric-number text-3xl text-gray-900 mb-1">15</div>
|
||||
<div class="text-xs text-subtle">cette semaine</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Sections -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- My Events -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold">Mes événements</h2>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Voir tout</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Event Row -->
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-red-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Concert Rock Alternative</h3>
|
||||
<span class="mono text-xs bg-green-100 text-green-800 px-2 py-1 rounded">CONFIRMÉ</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">Aujourd'hui 21:00 • Salle Pleyel</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="download" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-blue-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Networking Tech</h3>
|
||||
<span class="mono text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">DEMAIN</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">19:00 • WeWork République</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="w-2 h-12 bg-green-400 rounded-full"></div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium">Brunch du Dimanche</h3>
|
||||
<span class="mono text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">DIMANCHE</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle">11:00 • Café de Flore</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-gray-100 rounded-lg">
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Today -->
|
||||
<div class="space-y-6">
|
||||
<!-- Quick Actions -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Actions rapides</h3>
|
||||
<div class="space-y-3">
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-blue-100 rounded-lg">
|
||||
<i data-lucide="plus" class="w-4 h-4 text-blue-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Nouvel événement</span>
|
||||
</button>
|
||||
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-green-100 rounded-lg">
|
||||
<i data-lucide="search" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Rechercher</span>
|
||||
</button>
|
||||
|
||||
<button class="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left">
|
||||
<div class="p-2 bg-purple-100 rounded-lg">
|
||||
<i data-lucide="heart" class="w-4 h-4 text-purple-600"></i>
|
||||
</div>
|
||||
<span class="font-medium text-sm">Favoris</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Schedule -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Aujourd'hui</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">14:00</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Cours de Cuisine</h4>
|
||||
<p class="text-xs text-subtle">École Ducasse</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-yellow-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">20:30</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Festival de Cinéma</h4>
|
||||
<p class="text-xs text-subtle">MK2 Bibliothèque</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-red-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="mono text-xs bg-gray-100 px-2 py-1 rounded mt-1">22:00</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-sm">Soirée Jazz</h4>
|
||||
<p class="text-xs text-subtle">Le Sunset</p>
|
||||
</div>
|
||||
<span class="w-2 h-2 bg-blue-400 rounded-full mt-2"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="minimal-card rounded-lg p-6 fade-in">
|
||||
<h3 class="font-semibold mb-4">Statistiques</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Total participations</span>
|
||||
<span class="mono font-medium">127</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Événements créés</span>
|
||||
<span class="mono font-medium">12</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-subtle">Note moyenne</span>
|
||||
<span class="mono font-medium">4.8/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Events Grid -->
|
||||
<div class="mt-12">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-semibold">Événements à venir</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="text-sm text-subtle hover:text-gray-900 flex items-center space-x-1">
|
||||
<i data-lucide="filter" class="w-4 h-4"></i>
|
||||
<span>Filtrer</span>
|
||||
</button>
|
||||
<button class="text-sm text-subtle hover:text-gray-900 flex items-center space-x-1">
|
||||
<i data-lucide="grid-3x3" class="w-4 h-4"></i>
|
||||
<span>Vue</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Event Card -->
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center">
|
||||
<i data-lucide="music" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Concert Électro</h3>
|
||||
<span class="mono text-xs bg-gray-100 px-2 py-1 rounded">€45</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Samedi 21 Sept • Berghain</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">12 places restantes</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Réserver</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-green-400 to-teal-600 flex items-center justify-center">
|
||||
<i data-lucide="leaf" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Marché Bio</h3>
|
||||
<span class="mono text-xs bg-green-100 text-green-600 px-2 py-1 rounded">GRATUIT</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Dimanche 22 Sept • Place des Vosges</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-blue-600 bg-blue-100 px-2 py-1 rounded">Accès libre</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Voir détails</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="minimal-card rounded-lg overflow-hidden fade-in">
|
||||
<div class="aspect-video bg-gradient-to-br from-orange-400 to-red-600 flex items-center justify-center">
|
||||
<i data-lucide="book-open" class="w-12 h-12 text-white"></i>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-semibold">Salon du Livre</h3>
|
||||
<span class="mono text-xs bg-gray-100 px-2 py-1 rounded">€15</span>
|
||||
</div>
|
||||
<p class="text-sm text-subtle mb-3">Lundi 23 Sept • Grand Palais</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-yellow-600 bg-yellow-100 px-2 py-1 rounded">Populaire</span>
|
||||
<button class="text-accent text-sm font-medium hover:underline">Réserver</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load More -->
|
||||
<div class="text-center mt-8">
|
||||
<button class="text-accent font-medium hover:underline">Charger plus d'événements</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Stagger animations
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((el, index) => {
|
||||
el.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
556
.superdesign/design_iterations/dashboard_3.html
Normal file
556
.superdesign/design_iterations/dashboard_3.html
Normal file
@@ -0,0 +1,556 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Data Visualization Enhanced</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
|
||||
.progress-ring {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-ring__circle {
|
||||
transition: stroke-dashoffset 0.35s;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #06b6d4 100%);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-50">
|
||||
<div class="min-h-screen">
|
||||
<!-- Header -->
|
||||
<div class="gradient-bg px-6 py-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">Dashboard Analytics</h1>
|
||||
<p class="text-blue-100">Analyse détaillée de vos événements et participations</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<select class="bg-white/20 backdrop-blur-lg border border-white/30 rounded-lg px-4 py-2 text-white text-sm">
|
||||
<option>7 derniers jours</option>
|
||||
<option>30 derniers jours</option>
|
||||
<option>3 derniers mois</option>
|
||||
</select>
|
||||
<button class="bg-white/20 backdrop-blur-lg border border-white/30 rounded-lg px-4 py-2 text-white text-sm font-medium hover:bg-white/30 transition-all">
|
||||
<i data-lucide="download" class="w-4 h-4 inline mr-2"></i>
|
||||
Exporter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards with Progress -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Participation Rate -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Taux de participation</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">87%</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#10b981" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="87 13" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="trending-up" class="w-6 h-6 text-green-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-green-600 font-medium">+5%</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Événements créés -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Événements créés</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">12</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#3b82f6" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="60 40" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="plus-circle" class="w-6 h-6 text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-blue-600 font-medium">+3</span>
|
||||
<span class="text-gray-500 ml-1">ce mois</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenus -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Revenus</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">€2,340</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#8b5cf6" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="78 22" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="euro" class="w-6 h-6 text-purple-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-purple-600 font-medium">+18%</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satisfaction -->
|
||||
<div class="stat-card rounded-2xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-600">Satisfaction</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-2">4.8</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="progress-ring w-16 h-16" viewBox="0 0 40 40">
|
||||
<circle class="progress-ring__circle" stroke="#e5e7eb" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"/>
|
||||
<circle class="progress-ring__circle" stroke="#f59e0b" stroke-width="3" fill="transparent" r="16" cx="20" cy="20"
|
||||
stroke-dasharray="96 4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i data-lucide="star" class="w-6 h-6 text-yellow-500 fill-current"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-yellow-600 font-medium">+0.2</span>
|
||||
<span class="text-gray-500 ml-1">vs. mois dernier</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-6 py-8">
|
||||
|
||||
<!-- Critical Alert -->
|
||||
<div class="bg-red-50 border border-red-200 rounded-2xl p-6 mb-8">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="p-3 bg-red-100 rounded-xl">
|
||||
<i data-lucide="alert-circle" class="w-6 h-6 text-red-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-red-900 mb-2">Paiements en attente - Action urgente</h3>
|
||||
<p class="text-red-700 mb-4">2 billets expirent dans les 25 prochaines minutes</p>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-red-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Soirée Jazz au Sunset</h4>
|
||||
<p class="text-sm text-gray-600">2 billets • Tentative 1/3</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-2xl font-bold text-gray-900">€70</p>
|
||||
<div class="w-24 bg-red-200 rounded-full h-2 mt-1">
|
||||
<div class="bg-red-600 h-2 rounded-full transition-all" style="width: 15%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-red-600 mt-1">25min restantes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-xl font-medium transition-colors">
|
||||
Payer maintenant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts and Analytics -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
|
||||
<!-- Event Participation Chart -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Participation aux événements</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="text-sm bg-blue-100 text-blue-700 px-3 py-1 rounded-full">7j</button>
|
||||
<button class="text-sm text-gray-500 px-3 py-1 rounded-full">30j</button>
|
||||
<button class="text-sm text-gray-500 px-3 py-1 rounded-full">3m</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="participationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Categories Pie Chart -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Catégories d'événements</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="categoriesChart"></canvas>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Concert (40%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Cuisine (25%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Tech (20%)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-purple-500 rounded-full"></div>
|
||||
<span class="text-sm text-gray-600">Art (15%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline and Events -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- Event Timeline -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Timeline des événements</h3>
|
||||
<button class="text-blue-600 text-sm font-medium hover:underline">Voir tout</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute left-4 top-0 bottom-0 w-px bg-gray-200"></div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Timeline Item -->
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-green-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Concert Rock Alternative</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Aujourd'hui 21:00 • Salle Pleyel</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>156 participants</span>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="star" class="w-3 h-3 mr-1 fill-current text-yellow-500"></i>
|
||||
<span>4.7/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium">CONFIRMÉ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-blue-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Networking Tech</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Demain 19:00 • WeWork République</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>42/50 participants</span>
|
||||
</div>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1">
|
||||
<div class="bg-blue-600 h-1 rounded-full" style="width: 84%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-xs font-medium">DEMAIN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-purple-600">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Brunch du Dimanche</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Dimanche 11:00 • Café de Flore</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
<span>8/12 participants</span>
|
||||
</div>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1">
|
||||
<div class="bg-purple-600 h-1 rounded-full" style="width: 67%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-yellow-100 text-yellow-800 px-3 py-1 rounded-full text-xs font-medium">EN COURS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative pl-10 pb-6">
|
||||
<div class="timeline-item text-gray-400">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Cours de Photographie</h4>
|
||||
<p class="text-sm text-gray-600 mt-1">Mercredi 18:00 • Studio Martin</p>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div class="flex items-center text-xs">
|
||||
<i data-lucide="calendar" class="w-3 h-3 mr-1"></i>
|
||||
<span>Dans 3 jours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="bg-gray-100 text-gray-600 px-3 py-1 rounded-full text-xs font-medium">PLANIFIÉ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Performance Metrics -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Performance</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Taux de réussite</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-green-600 h-2 rounded-full" style="width: 94%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">94%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Engagement</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full" style="width: 78%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">78%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Recommandations</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-purple-600 h-2 rounded-full" style="width: 89%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">89%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Categories -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Top catégories</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="music" class="w-4 h-4 text-blue-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Concert</span>
|
||||
<span class="text-sm text-gray-500">40%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-blue-600 h-1 rounded-full" style="width: 40%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="utensils" class="w-4 h-4 text-green-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Cuisine</span>
|
||||
<span class="text-sm text-gray-500">25%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-green-600 h-1 rounded-full" style="width: 25%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="laptop" class="w-4 h-4 text-yellow-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Tech</span>
|
||||
<span class="text-sm text-gray-500">20%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-yellow-600 h-1 rounded-full" style="width: 20%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="palette" class="w-4 h-4 text-purple-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">Art</span>
|
||||
<span class="text-sm text-gray-500">15%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||||
<div class="bg-purple-600 h-1 rounded-full" style="width: 15%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques rapides</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Événements créés</span>
|
||||
<span class="font-medium">127</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Participants totaux</span>
|
||||
<span class="font-medium">2,456</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Note moyenne</span>
|
||||
<span class="font-medium">4.8/5</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Revenus</span>
|
||||
<span class="font-medium">€12,340</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Participation Chart
|
||||
const participationCtx = document.getElementById('participationChart').getContext('2d');
|
||||
new Chart(participationCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
|
||||
datasets: [{
|
||||
label: 'Participations',
|
||||
data: [12, 19, 8, 15, 24, 18, 22],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Categories Chart
|
||||
const categoriesCtx = document.getElementById('categoriesChart').getContext('2d');
|
||||
new Chart(categoriesCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Concert', 'Cuisine', 'Tech', 'Art'],
|
||||
datasets: [{
|
||||
data: [40, 25, 20, 15],
|
||||
backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
521
.superdesign/design_iterations/default_ui_darkmode.css
Executable file
521
.superdesign/design_iterations/default_ui_darkmode.css
Executable file
@@ -0,0 +1,521 @@
|
||||
/* ========================================
|
||||
Dark Mode UI Framework
|
||||
A beautiful dark mode design system
|
||||
======================================== */
|
||||
|
||||
/* ========================================
|
||||
CSS Variables & Theme
|
||||
======================================== */
|
||||
:root {
|
||||
/* Dark Mode Color Palette */
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
|
||||
/* Spacing & Layout */
|
||||
--radius: 0.625rem;
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 0.75rem;
|
||||
--spacing-lg: 1rem;
|
||||
--spacing-xl: 1.5rem;
|
||||
--spacing-2xl: 2rem;
|
||||
--spacing-3xl: 3rem;
|
||||
|
||||
/* Typography */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Base Styles
|
||||
======================================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Layout Components
|
||||
======================================== */
|
||||
.container {
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-2xl) var(--spacing-lg);
|
||||
}
|
||||
|
||||
.container-sm {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid-cols-auto { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
||||
|
||||
.gap-sm { gap: var(--spacing-sm); }
|
||||
.gap-md { gap: var(--spacing-md); }
|
||||
.gap-lg { gap: var(--spacing-lg); }
|
||||
.gap-xl { gap: var(--spacing-xl); }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Card Components
|
||||
======================================== */
|
||||
.card {
|
||||
background-color: var(--card);
|
||||
color: var(--card-foreground);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Button Components
|
||||
======================================== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
white-space: nowrap;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
min-height: 2.25rem;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: rgba(236, 236, 236, 0.9);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
background-color: var(--destructive);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-destructive:hover {
|
||||
background-color: rgba(220, 38, 38, 0.9);
|
||||
}
|
||||
|
||||
/* Button Sizes */
|
||||
.btn-sm {
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
font-size: var(--font-size-xs);
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
font-size: var(--font-size-base);
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: var(--spacing-sm);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Form Components
|
||||
======================================== */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
color: var(--foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgba(136, 136, 136, 0.5);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Badge Components
|
||||
======================================== */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid;
|
||||
padding: 0.125rem var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Priority Badge Variants */
|
||||
.badge-priority-high {
|
||||
background: rgba(127, 29, 29, 0.3);
|
||||
color: rgb(252, 165, 165);
|
||||
border: 1px solid rgba(153, 27, 27, 0.5);
|
||||
}
|
||||
|
||||
.badge-priority-medium {
|
||||
background: rgba(120, 53, 15, 0.3);
|
||||
color: rgb(252, 211, 77);
|
||||
border: 1px solid rgba(146, 64, 14, 0.5);
|
||||
}
|
||||
|
||||
.badge-priority-low {
|
||||
background: rgba(20, 83, 45, 0.3);
|
||||
color: rgb(134, 239, 172);
|
||||
border: 1px solid rgba(22, 101, 52, 0.5);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Tab Components
|
||||
======================================== */
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--foreground);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #1a1a1a !important;
|
||||
border-color: #f8f9fa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-button.active:hover {
|
||||
background-color: #e9ecef !important;
|
||||
border-color: #e9ecef !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Typography
|
||||
======================================== */
|
||||
.text-xs { font-size: var(--font-size-xs); }
|
||||
.text-sm { font-size: var(--font-size-sm); }
|
||||
.text-base { font-size: var(--font-size-base); }
|
||||
.text-lg { font-size: var(--font-size-lg); }
|
||||
.text-xl { font-size: var(--font-size-xl); }
|
||||
.text-2xl { font-size: var(--font-size-2xl); }
|
||||
.text-3xl { font-size: var(--font-size-3xl); }
|
||||
.text-4xl { font-size: var(--font-size-4xl); }
|
||||
|
||||
.font-normal { font-weight: 400; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
.text-primary { color: var(--primary); }
|
||||
.text-muted { color: var(--muted-foreground); }
|
||||
.text-destructive { color: var(--destructive); }
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--primary), rgba(236, 236, 236, 0.6));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Icon System
|
||||
======================================== */
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-sm { width: 0.875rem; height: 0.875rem; }
|
||||
.icon-lg { width: 1.25rem; height: 1.25rem; }
|
||||
.icon-xl { width: 1.5rem; height: 1.5rem; }
|
||||
.icon-2xl { width: 2rem; height: 2rem; }
|
||||
|
||||
/* ========================================
|
||||
Interactive Components
|
||||
======================================== */
|
||||
.checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checkbox:hover {
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background-color: rgb(22, 163, 74);
|
||||
border-color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.checkbox.checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
List Components
|
||||
======================================== */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Empty State Component
|
||||
======================================== */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-3xl) var(--spacing-lg);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.empty-state .icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Utility Classes
|
||||
======================================== */
|
||||
.hidden { display: none; }
|
||||
.block { display: block; }
|
||||
.flex { display: flex; }
|
||||
.inline-flex { display: inline-flex; }
|
||||
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
.min-h-screen { min-height: 100vh; }
|
||||
|
||||
.opacity-50 { opacity: 0.5; }
|
||||
.opacity-60 { opacity: 0.6; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
|
||||
.transition-all { transition: all 0.2s ease; }
|
||||
.transition-colors { transition: color 0.2s ease, background-color 0.2s ease; }
|
||||
.transition-opacity { transition: opacity 0.2s ease; }
|
||||
|
||||
/* ========================================
|
||||
Responsive Design
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.grid-cols-auto {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.flex-col-mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-center-mobile {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gap-sm-mobile { gap: var(--spacing-sm); }
|
||||
|
||||
.hidden-mobile { display: none; }
|
||||
.block-mobile { display: block; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.text-2xl { font-size: var(--font-size-xl); }
|
||||
.text-3xl { font-size: var(--font-size-2xl); }
|
||||
.text-4xl { font-size: var(--font-size-3xl); }
|
||||
|
||||
.container {
|
||||
padding: var(--spacing-lg) var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Animation Utilities
|
||||
======================================== */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Focus & Accessibility
|
||||
======================================== */
|
||||
.focus-visible:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
53
.superdesign/design_iterations/festival_theme.css
Normal file
53
.superdesign/design_iterations/festival_theme.css
Normal file
@@ -0,0 +1,53 @@
|
||||
:root {
|
||||
--background: oklch(0.9961 0.0039 106.7952);
|
||||
--foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--card: oklch(0.9961 0.0039 106.7952);
|
||||
--card-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--popover: oklch(0.9961 0.0039 106.7952);
|
||||
--popover-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--primary: oklch(0.4902 0.2314 320.7094);
|
||||
--primary-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--secondary: oklch(0.6471 0.1686 342.5570);
|
||||
--secondary-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--muted: oklch(0.9412 0.0196 106.7952);
|
||||
--muted-foreground: oklch(0.4706 0.0157 286.0532);
|
||||
--accent: oklch(0.7255 0.1451 51.2345);
|
||||
--accent-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--destructive: oklch(0.5765 0.2314 27.3319);
|
||||
--destructive-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--border: oklch(0.8824 0.0157 106.7952);
|
||||
--input: oklch(0.8824 0.0157 106.7952);
|
||||
--ring: oklch(0.4902 0.2314 320.7094);
|
||||
--chart-1: oklch(0.4902 0.2314 320.7094);
|
||||
--chart-2: oklch(0.6471 0.1686 342.5570);
|
||||
--chart-3: oklch(0.7255 0.1451 51.2345);
|
||||
--chart-4: oklch(0.5490 0.2157 142.4953);
|
||||
--chart-5: oklch(0.6157 0.2275 328.3634);
|
||||
--sidebar: oklch(0.9412 0.0196 106.7952);
|
||||
--sidebar-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--sidebar-primary: oklch(0.4902 0.2314 320.7094);
|
||||
--sidebar-primary-foreground: oklch(0.9961 0.0039 106.7952);
|
||||
--sidebar-accent: oklch(0.6471 0.1686 342.5570);
|
||||
--sidebar-accent-foreground: oklch(0.0902 0.0203 286.0532);
|
||||
--sidebar-border: oklch(0.8824 0.0157 106.7952);
|
||||
--sidebar-ring: oklch(0.4902 0.2314 320.7094);
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Playfair Display', serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
--radius: 1rem;
|
||||
--shadow-2xs: 0 1px 2px 0px hsl(320 70% 20% / 0.08);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(320 70% 20% / 0.10);
|
||||
--shadow-sm: 0 2px 4px 0px hsl(320 70% 20% / 0.10), 0 1px 2px -1px hsl(320 70% 20% / 0.06);
|
||||
--shadow: 0 4px 6px 0px hsl(320 70% 20% / 0.12), 0 2px 4px -1px hsl(320 70% 20% / 0.08);
|
||||
--shadow-md: 0 6px 8px 0px hsl(320 70% 20% / 0.15), 0 4px 6px -1px hsl(320 70% 20% / 0.10);
|
||||
--shadow-lg: 0 10px 15px 0px hsl(320 70% 20% / 0.20), 0 6px 8px -1px hsl(320 70% 20% / 0.15);
|
||||
--shadow-xl: 0 20px 25px 0px hsl(320 70% 20% / 0.25), 0 10px 15px -1px hsl(320 70% 20% / 0.20);
|
||||
--shadow-2xl: 0 25px 50px 0px hsl(320 70% 20% / 0.30);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
538
.superdesign/design_iterations/festival_ticket_page.html
Normal file
538
.superdesign/design_iterations/festival_ticket_page.html
Normal file
@@ -0,0 +1,538 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fête de l'Humanité 2025 - Billets</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;500;600;700&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="festival_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--background) !important;
|
||||
color: var(--foreground) !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.festival-gradient {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.4902 0.2314 320.7094) 0%,
|
||||
oklch(0.6471 0.1686 342.5570) 50%,
|
||||
oklch(0.7255 0.1451 51.2345) 100%) !important;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
background: var(--card) !important;
|
||||
border: 2px solid var(--border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.ticket-card:hover {
|
||||
transform: translateY(-4px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.ticket-card.selected {
|
||||
border-color: var(--primary) !important;
|
||||
background: linear-gradient(135deg, var(--card), oklch(0.4902 0.2314 320.7094 / 0.05)) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
background: var(--muted) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.quantity-control:hover {
|
||||
background: var(--accent) !important;
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
.cart-summary {
|
||||
background: linear-gradient(135deg,
|
||||
var(--card),
|
||||
oklch(0.4902 0.2314 320.7094 / 0.03)) !important;
|
||||
border: 2px solid var(--primary) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
.checkout-button {
|
||||
background: var(--primary) !important;
|
||||
color: var(--primary-foreground) !important;
|
||||
border: none !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.checkout-button:hover:not(:disabled) {
|
||||
background: oklch(0.4302 0.2314 320.7094) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.checkout-button:disabled {
|
||||
background: var(--muted) !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.festival-info {
|
||||
background: linear-gradient(45deg,
|
||||
oklch(0.7255 0.1451 51.2345 / 0.1),
|
||||
oklch(0.6471 0.1686 342.5570 / 0.1)) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
border: 1px solid var(--accent) !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.4902 0.2314 320.7094 / 0.9) 0%,
|
||||
oklch(0.6471 0.1686 342.5570 / 0.9) 50%,
|
||||
oklch(0.7255 0.1451 51.2345 / 0.9) 100%),
|
||||
url('https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=1200&h=600&fit=crop') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.animate-bounce-slow {
|
||||
animation: bounce 2s infinite !important;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
||||
}
|
||||
|
||||
.ripple-effect {
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.ripple-effect::after {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
transition: width 0.4s, height 0.4s !important;
|
||||
}
|
||||
|
||||
.ripple-effect:hover::after {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section h-96 flex items-center justify-center relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||
<div class="relative z-10 text-center max-w-4xl mx-auto px-4">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-4 font-serif animate-pulse-slow">Fête de l'Humanité 2025</h1>
|
||||
<p class="text-xl md:text-2xl mb-2 opacity-90">14-16 Septembre • La Courneuve</p>
|
||||
<p class="text-lg opacity-80 max-w-2xl mx-auto">Trois jours de musique, débats, culture et solidarité au cœur du plus grand festival populaire de France</p>
|
||||
<div class="flex justify-center items-center mt-6 space-x-6">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-5 h-5 mr-2"></i>
|
||||
<span>3 jours</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="music" class="w-5 h-5 mr-2"></i>
|
||||
<span>100+ concerts</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="users" class="w-5 h-5 mr-2"></i>
|
||||
<span>500k visiteurs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 animate-bounce-slow">
|
||||
<i data-lucide="chevron-down" class="w-8 h-8 text-white opacity-70"></i>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<!-- Ticket Selection Hub -->
|
||||
<div class="mb-12">
|
||||
<div class="text-center mb-10">
|
||||
<h2 class="text-4xl font-bold text-gray-900 mb-4 font-serif">Choisissez vos billets</h2>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">Découvrez nos différentes formules pour profiter pleinement du festival</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Left Column: Tickets -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Pass 3 Jours -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('pass3j', 45, 'Pass 3 jours')">
|
||||
<div class="text-center">
|
||||
<div class="festival-gradient w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="star" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Pass 3 Jours</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Accès complet au festival</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">45€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="pass3j-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Samedi 14 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('samedi', 18, 'Samedi 14 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-purple-500 to-pink-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="calendar-days" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Samedi 14</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="samedi-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dimanche 15 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('dimanche', 18, 'Dimanche 15 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-orange-500 to-red-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="sun" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Dimanche 15</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="dimanche-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lundi 16 -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('lundi', 18, 'Lundi 16 Sept')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-green-500 to-blue-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="moon" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Lundi 16</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="lundi-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif Réduit -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('reduit', 12, 'Tarif Réduit')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-yellow-500 to-orange-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="percent" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Tarif Réduit</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Étudiants, -26 ans, RSA</p>
|
||||
<div class="text-3xl font-bold text-primary mb-4">12€</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="reduit-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gratuit -12 ans -->
|
||||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('gratuit', 0, 'Gratuit -12 ans')">
|
||||
<div class="text-center">
|
||||
<div class="bg-gradient-to-br from-green-600 to-emerald-600 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="gift" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Gratuit</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Enfants -12 ans</p>
|
||||
<div class="text-3xl font-bold text-green-600 mb-4">Gratuit</div>
|
||||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3">
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', -1)">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<span class="w-8 text-center font-medium" id="gratuit-qty">0</span>
|
||||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', 1)">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Cart & Info -->
|
||||
<div class="lg:col-span-1">
|
||||
<!-- Cart Summary -->
|
||||
<div class="cart-summary p-6 mb-8 sticky top-4">
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-6 text-center">Récapitulatif</h3>
|
||||
|
||||
<div id="cart-items" class="space-y-3 mb-6 min-h-[100px]">
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||||
<p>Votre panier est vide</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Total billets:</span>
|
||||
<span class="font-medium" id="total-quantity">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Sous-total:</span>
|
||||
<span class="font-medium" id="subtotal">€0.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Frais de service:</span>
|
||||
<span class="font-medium" id="service-fee">€0.00</span>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 pt-2 mt-4">
|
||||
<div class="flex justify-between text-lg font-bold">
|
||||
<span>TOTAL:</span>
|
||||
<span class="text-primary" id="total-amount">€0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="checkout-btn" class="checkout-button w-full py-4 px-6 text-lg font-semibold mt-6 disabled" disabled>
|
||||
<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>
|
||||
Finaliser la commande
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Festival Info -->
|
||||
<div class="festival-info p-6">
|
||||
<h4 class="text-xl font-bold text-gray-900 mb-4 text-center">🎪 Festival Highlights</h4>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="music" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>100+ concerts et spectacles</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="mic" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Débats et conférences</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="utensils" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Village gastronomique</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="heart" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Village solidaire</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="gamepad-2" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Animations jeunesse</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="train" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||||
<span>Accès RER B La Courneuve</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Cart state
|
||||
let cart = {};
|
||||
const serviceFeeRate = 0.05; // 5% service fee
|
||||
|
||||
function selectTicket(id, price, name) {
|
||||
// Visual selection effect
|
||||
const cards = document.querySelectorAll('.ticket-card');
|
||||
cards.forEach(card => card.classList.remove('selected'));
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// Auto-add one ticket if none selected
|
||||
if (!cart[id] || cart[id].quantity === 0) {
|
||||
changeQuantity(id, 1, price, name);
|
||||
}
|
||||
}
|
||||
|
||||
function changeQuantity(id, delta, price, name) {
|
||||
if (!cart[id]) {
|
||||
cart[id] = { quantity: 0, price: price || 0, name: name || '' };
|
||||
}
|
||||
|
||||
// Get price and name from ticket data if not provided
|
||||
if (!price) {
|
||||
const ticketPrices = {
|
||||
'pass3j': { price: 45, name: 'Pass 3 jours' },
|
||||
'samedi': { price: 18, name: 'Samedi 14 Sept' },
|
||||
'dimanche': { price: 18, name: 'Dimanche 15 Sept' },
|
||||
'lundi': { price: 18, name: 'Lundi 16 Sept' },
|
||||
'reduit': { price: 12, name: 'Tarif Réduit' },
|
||||
'gratuit': { price: 0, name: 'Gratuit -12 ans' }
|
||||
};
|
||||
price = ticketPrices[id].price;
|
||||
name = ticketPrices[id].name;
|
||||
cart[id].price = price;
|
||||
cart[id].name = name;
|
||||
}
|
||||
|
||||
cart[id].quantity = Math.max(0, cart[id].quantity + delta);
|
||||
|
||||
// Update quantity display
|
||||
document.getElementById(id + '-qty').textContent = cart[id].quantity;
|
||||
|
||||
// Remove from cart if quantity is 0
|
||||
if (cart[id].quantity === 0) {
|
||||
delete cart[id];
|
||||
}
|
||||
|
||||
updateCartSummary();
|
||||
}
|
||||
|
||||
function updateCartSummary() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
const totalQuantityEl = document.getElementById('total-quantity');
|
||||
const subtotalEl = document.getElementById('subtotal');
|
||||
const serviceFeeEl = document.getElementById('service-fee');
|
||||
const totalAmountEl = document.getElementById('total-amount');
|
||||
const checkoutBtn = document.getElementById('checkout-btn');
|
||||
|
||||
let totalQuantity = 0;
|
||||
let subtotal = 0;
|
||||
let cartItemsHtml = '';
|
||||
|
||||
// Check if cart is empty
|
||||
const hasItems = Object.keys(cart).some(id => cart[id].quantity > 0);
|
||||
|
||||
if (!hasItems) {
|
||||
cartItemsHtml = `
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||||
<p>Votre panier est vide</p>
|
||||
</div>
|
||||
`;
|
||||
checkoutBtn.disabled = true;
|
||||
checkoutBtn.classList.add('disabled');
|
||||
} else {
|
||||
// Build cart items
|
||||
Object.keys(cart).forEach(id => {
|
||||
if (cart[id].quantity > 0) {
|
||||
totalQuantity += cart[id].quantity;
|
||||
subtotal += cart[id].quantity * cart[id].price;
|
||||
|
||||
cartItemsHtml += `
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">${cart[id].name}</div>
|
||||
<div class="text-xs text-gray-500">${cart[id].quantity} × €${cart[id].price.toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="font-medium text-sm">€${(cart[id].quantity * cart[id].price).toFixed(2)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
checkoutBtn.disabled = false;
|
||||
checkoutBtn.classList.remove('disabled');
|
||||
}
|
||||
|
||||
const serviceFee = subtotal * serviceFeeRate;
|
||||
const totalAmount = subtotal + serviceFee;
|
||||
|
||||
cartItemsContainer.innerHTML = cartItemsHtml;
|
||||
totalQuantityEl.textContent = totalQuantity;
|
||||
subtotalEl.textContent = `€${subtotal.toFixed(2)}`;
|
||||
serviceFeeEl.textContent = `€${serviceFee.toFixed(2)}`;
|
||||
totalAmountEl.textContent = `€${totalAmount.toFixed(2)}`;
|
||||
|
||||
// Recreate icons for newly added elements
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Checkout button click handler
|
||||
document.getElementById('checkout-btn').addEventListener('click', function() {
|
||||
if (this.disabled) return;
|
||||
|
||||
// Simulate checkout process
|
||||
this.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 inline-block mr-2 animate-spin"></i>Traitement...';
|
||||
this.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
alert('Redirection vers le paiement sécurisé...');
|
||||
this.innerHTML = '<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>Finaliser la commande';
|
||||
this.disabled = Object.keys(cart).length === 0;
|
||||
lucide.createIcons();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Initial setup
|
||||
updateCartSummary();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
480
.superdesign/design_iterations/quantic_login_1.html
Normal file
480
.superdesign/design_iterations/quantic_login_1.html
Normal file
@@ -0,0 +1,480 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - Quantic Telecom</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="quantic_telecom_theme.css">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans) !important;
|
||||
background: var(--gradient-background) !important;
|
||||
min-height: 100vh !important;
|
||||
position: relative !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Background grid pattern */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
opacity: 0.3;
|
||||
z-index: 0;
|
||||
animation: gridShift 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridShift {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(50px, 50px); }
|
||||
}
|
||||
|
||||
/* Page entrance animation */
|
||||
.page-container {
|
||||
animation: pageLoad 800ms ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
@keyframes pageLoad {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo animation */
|
||||
.logo-container {
|
||||
animation: logoFade 1200ms ease-out 200ms forwards;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
@keyframes logoFade {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Card slide animation */
|
||||
.login-card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: var(--glass-backdrop) !important;
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-xl) !important;
|
||||
animation: cardSlide 600ms cubic-bezier(0.4, 0, 0.2, 1) 400ms forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
@keyframes cardSlide {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Input field styling */
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--input);
|
||||
color: var(--foreground);
|
||||
font-size: 1rem;
|
||||
transition: all 200ms ease-out;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--ring);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.input-field:focus + .floating-label {
|
||||
transform: translateY(-10px) scale(0.75);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.floating-label {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--input);
|
||||
padding: 0 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.input-field:not(:placeholder-shown) + .floating-label {
|
||||
transform: translateY(-10px) scale(0.75);
|
||||
}
|
||||
|
||||
/* Password toggle */
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease-out;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: var(--gradient-primary) !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Ripple effect */
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transition: width 400ms ease-out, height 400ms ease-out;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.login-button:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.custom-checkbox {
|
||||
appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--input);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.custom-checkbox:checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
animation: checkboxTick 250ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.custom-checkbox:checked::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@keyframes checkboxTick {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Link styling */
|
||||
.forgot-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.forgot-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 200ms ease-out;
|
||||
}
|
||||
|
||||
.forgot-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Validation states */
|
||||
.input-error {
|
||||
border-color: var(--destructive) !important;
|
||||
animation: errorShake 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-8px); }
|
||||
75% { transform: translateX(8px); }
|
||||
}
|
||||
|
||||
.input-success {
|
||||
border-color: var(--success) !important;
|
||||
animation: successPulse 500ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--accent) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- Logo and Header -->
|
||||
<div class="logo-container text-center mb-8">
|
||||
<div class="mb-4">
|
||||
<div class="w-16 h-16 mx-auto bg-gradient-to-br from-blue-600 to-blue-800 rounded-xl flex items-center justify-center">
|
||||
<i data-lucide="wifi" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Quantic Telecom</h1>
|
||||
<p class="text-gray-600 text-sm">Connexion Espace Client</p>
|
||||
<p class="text-gray-500 text-xs mt-1">Votre espace client sécurisé</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Card -->
|
||||
<div class="login-card p-8">
|
||||
<form class="space-y-6">
|
||||
<!-- Email Field -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="email"
|
||||
>
|
||||
<label class="floating-label" for="email">Adresse e-mail</label>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i data-lucide="mail" class="w-5 h-5 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="input-field"
|
||||
placeholder=" "
|
||||
required
|
||||
id="password"
|
||||
>
|
||||
<label class="floating-label" for="password">Mot de passe</label>
|
||||
<button type="button" class="password-toggle" onclick="togglePassword()">
|
||||
<i data-lucide="eye" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" class="custom-checkbox" id="remember">
|
||||
<span class="text-sm text-gray-700">Se souvenir de moi</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button type="submit" class="login-button">
|
||||
<span class="relative z-10">SE CONNECTER</span>
|
||||
</button>
|
||||
|
||||
<!-- Forgot Password -->
|
||||
<div class="text-center">
|
||||
<a href="#" class="forgot-link text-sm">Mot de passe oublié ?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Support Footer -->
|
||||
<div class="text-center mt-8 space-y-2">
|
||||
<p class="text-xs text-gray-500">
|
||||
Besoin d'aide ?
|
||||
<a href="#" class="text-blue-600 hover:text-blue-800 transition-colors">Support technique</a>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">© 2024 Quantic Telecom - Tous droits réservés</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Password toggle functionality
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.password-toggle i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
toggleIcon.setAttribute('data-lucide', 'eye-off');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
toggleIcon.setAttribute('data-lucide', 'eye');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Form validation
|
||||
const form = document.querySelector('form');
|
||||
const emailField = document.getElementById('email');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset validation states
|
||||
emailField.classList.remove('input-error', 'input-success');
|
||||
passwordField.classList.remove('input-error', 'input-success');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailField.value)) {
|
||||
emailField.classList.add('input-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
emailField.classList.add('input-success');
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (passwordField.value.length < 6) {
|
||||
passwordField.classList.add('input-error');
|
||||
isValid = false;
|
||||
} else {
|
||||
passwordField.classList.add('input-success');
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Simulate login process
|
||||
const button = document.querySelector('.login-button');
|
||||
button.innerHTML = '<div class="flex items-center justify-center"><div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full mr-2"></div>Connexion...</div>';
|
||||
|
||||
setTimeout(() => {
|
||||
alert('Connexion réussie ! (Demo)');
|
||||
button.innerHTML = '<span class="relative z-10">SE CONNECTER</span>';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time input validation
|
||||
emailField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
passwordField.addEventListener('input', function() {
|
||||
this.classList.remove('input-error', 'input-success');
|
||||
});
|
||||
|
||||
// Add floating label behavior for better UX
|
||||
document.querySelectorAll('.input-field').forEach(input => {
|
||||
input.addEventListener('focus', function() {
|
||||
this.nextElementSibling.classList.add('focused');
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (!this.value) {
|
||||
this.nextElementSibling.classList.remove('focused');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
71
.superdesign/design_iterations/quantic_telecom_theme.css
Normal file
71
.superdesign/design_iterations/quantic_telecom_theme.css
Normal file
@@ -0,0 +1,71 @@
|
||||
:root {
|
||||
/* Quantic Telecom Brand Colors */
|
||||
--background: oklch(0.9800 0.0050 240);
|
||||
--foreground: oklch(0.1500 0.0100 240);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1500 0.0100 240);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1500 0.0100 240);
|
||||
|
||||
/* Primary - Telecom Blue */
|
||||
--primary: oklch(0.4800 0.2000 240);
|
||||
--primary-foreground: oklch(0.9800 0.0050 240);
|
||||
--primary-hover: oklch(0.4200 0.2200 240);
|
||||
|
||||
/* Secondary - Tech Gray */
|
||||
--secondary: oklch(0.9200 0.0100 240);
|
||||
--secondary-foreground: oklch(0.2500 0.0150 240);
|
||||
|
||||
/* Accent - Electric Blue */
|
||||
--accent: oklch(0.6500 0.2800 220);
|
||||
--accent-foreground: oklch(0.9800 0.0050 240);
|
||||
|
||||
/* Muted tones */
|
||||
--muted: oklch(0.9600 0.0080 240);
|
||||
--muted-foreground: oklch(0.4500 0.0120 240);
|
||||
|
||||
/* Success/Error states */
|
||||
--success: oklch(0.5500 0.2000 140);
|
||||
--success-foreground: oklch(0.9800 0.0050 140);
|
||||
--destructive: oklch(0.5500 0.2200 20);
|
||||
--destructive-foreground: oklch(0.9800 0.0050 20);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(0.8800 0.0150 240);
|
||||
--input: oklch(0.9600 0.0080 240);
|
||||
--ring: oklch(0.4800 0.2000 240);
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||
--font-serif: 'Inter', 'Segoe UI', system-ui, serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing and layout */
|
||||
--radius: 0.5rem;
|
||||
--spacing: 1rem;
|
||||
|
||||
/* Modern shadows for depth */
|
||||
--shadow-xs: 0 1px 3px 0 hsl(240 25% 3% / 0.06);
|
||||
--shadow-sm: 0 1px 3px 0 hsl(240 25% 3% / 0.08), 0 1px 2px -1px hsl(240 25% 3% / 0.08);
|
||||
--shadow: 0 4px 8px -2px hsl(240 25% 3% / 0.08), 0 2px 4px -2px hsl(240 25% 3% / 0.06);
|
||||
--shadow-md: 0 8px 16px -4px hsl(240 25% 3% / 0.08), 0 4px 6px -2px hsl(240 25% 3% / 0.06);
|
||||
--shadow-lg: 0 16px 24px -4px hsl(240 25% 3% / 0.08), 0 8px 8px -4px hsl(240 25% 3% / 0.04);
|
||||
--shadow-xl: 0 20px 32px -8px hsl(240 25% 3% / 0.12), 0 8px 16px -8px hsl(240 25% 3% / 0.08);
|
||||
|
||||
/* Gradients for modern appeal */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
--gradient-background: linear-gradient(135deg, oklch(0.9900 0.0030 240) 0%, oklch(0.9700 0.0080 220) 100%);
|
||||
|
||||
/* Grid overlay for tech aesthetic */
|
||||
--grid-color: oklch(0.9400 0.0100 240);
|
||||
|
||||
/* Glass morphism effects */
|
||||
--glass-bg: oklch(1.0000 0 0 / 0.70);
|
||||
--glass-border: oklch(0.9000 0.0200 240 / 0.20);
|
||||
--glass-backdrop: blur(12px) saturate(180%);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 8px);
|
||||
}
|
||||
2
.tool-versions
Executable file
2
.tool-versions
Executable file
@@ -0,0 +1,2 @@
|
||||
ruby 3.4.4
|
||||
nodejs 24.4.1
|
||||
767
.windsurfrules
Executable file
767
.windsurfrules
Executable file
@@ -0,0 +1,767 @@
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
300
AGENT.md
Executable file
300
AGENT.md
Executable file
@@ -0,0 +1,300 @@
|
||||
# Aperonight - Technical Documentation for AI Agents
|
||||
|
||||
## 🤖 Agent Implementation Guide
|
||||
|
||||
This document provides technical details for AI agents working on the Aperonight ticket selling system.
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. User Management (`app/models/user.rb`)
|
||||
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
||||
- **Relationships**: Users can create events and purchase tickets
|
||||
- **Validations**: Email format, password strength, optional name fields
|
||||
|
||||
#### 2. Event System (`app/models/event.rb`)
|
||||
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
||||
- **Geographic Data**: Latitude/longitude for venue mapping
|
||||
- **Relationships**: Belongs to user, has many ticket types and tickets through ticket types
|
||||
- **Scopes**: Featured events, published events, upcoming events with proper ordering
|
||||
|
||||
#### 3. Ticket Management
|
||||
- **TicketType** (`app/models/ticket_type.rb`): Defines ticket categories with pricing, quantity, sale periods
|
||||
- **Ticket** (`app/models/ticket.rb`): Individual tickets with unique QR codes, status tracking, price storage
|
||||
|
||||
#### 4. Payment Processing (`app/controllers/events_controller.rb`)
|
||||
- **Stripe Integration**: Complete checkout session creation and payment confirmation
|
||||
- **Session Management**: Proper handling of payment success/failure with ticket generation
|
||||
- **Security**: Authentication required, cart validation, availability checking
|
||||
|
||||
### Database Schema Key Points
|
||||
|
||||
```sql
|
||||
-- Users table (managed by Devise)
|
||||
CREATE TABLE users (
|
||||
id bigint PRIMARY KEY,
|
||||
email varchar(255) UNIQUE NOT NULL,
|
||||
encrypted_password varchar(255) NOT NULL,
|
||||
first_name varchar(255),
|
||||
last_name varchar(255),
|
||||
-- Devise fields: confirmation, reset tokens, etc.
|
||||
);
|
||||
|
||||
-- Events table
|
||||
CREATE TABLE events (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
name varchar(100) NOT NULL,
|
||||
slug varchar(100) NOT NULL,
|
||||
description text(1000) NOT NULL,
|
||||
venue_name varchar(100) NOT NULL,
|
||||
venue_address varchar(200) NOT NULL,
|
||||
latitude decimal(10,8) NOT NULL,
|
||||
longitude decimal(11,8) NOT NULL,
|
||||
start_time datetime NOT NULL,
|
||||
end_time datetime,
|
||||
state integer DEFAULT 0, -- enum: draft=0, published=1, canceled=2, sold_out=3
|
||||
featured boolean DEFAULT false,
|
||||
image varchar(500)
|
||||
);
|
||||
|
||||
-- Ticket types define pricing and availability
|
||||
CREATE TABLE ticket_types (
|
||||
id bigint PRIMARY KEY,
|
||||
event_id bigint REFERENCES events(id),
|
||||
name varchar(255) NOT NULL,
|
||||
description text,
|
||||
price_cents integer NOT NULL,
|
||||
quantity integer NOT NULL,
|
||||
sale_start_at datetime,
|
||||
sale_end_at datetime,
|
||||
requires_id boolean DEFAULT false,
|
||||
minimum_age integer
|
||||
);
|
||||
|
||||
-- Individual tickets with QR codes
|
||||
CREATE TABLE tickets (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
ticket_type_id bigint REFERENCES ticket_types(id),
|
||||
qr_code varchar(255) UNIQUE NOT NULL,
|
||||
price_cents integer NOT NULL,
|
||||
status varchar(255) DEFAULT 'active' -- active, used, expired, refunded
|
||||
);
|
||||
```
|
||||
|
||||
## 🎯 Key Implementation Details
|
||||
|
||||
### 1. Dashboard Metrics (`app/controllers/pages_controller.rb`)
|
||||
|
||||
```ruby
|
||||
# User-specific metrics with optimized queries
|
||||
@booked_events = current_user.tickets
|
||||
.joins(:ticket_type, :event)
|
||||
.where(events: { state: :published })
|
||||
.count
|
||||
|
||||
# Event counts for different timeframes
|
||||
@events_today = Event.published
|
||||
.where("DATE(start_time) = ?", Date.current)
|
||||
.count
|
||||
|
||||
# User's actual booked events (not just count)
|
||||
@user_booked_events = Event.joins(ticket_types: :tickets)
|
||||
.where(tickets: { user: current_user, status: 'active' })
|
||||
.distinct
|
||||
.limit(5)
|
||||
```
|
||||
|
||||
### 2. Stripe Payment Flow
|
||||
|
||||
#### Checkout Initiation (`events#checkout`)
|
||||
1. **Cart Validation**: Parse JSON cart data, validate ticket types and quantities
|
||||
2. **Availability Check**: Ensure sufficient tickets available before payment
|
||||
3. **Stripe Session**: Create checkout session with line items, success/cancel URLs
|
||||
4. **Metadata Storage**: Store order details in Stripe session metadata for later retrieval
|
||||
|
||||
```ruby
|
||||
# Key Stripe configuration
|
||||
session = Stripe::Checkout::Session.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: line_items,
|
||||
mode: 'payment',
|
||||
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
|
||||
cancel_url: event_url(@event.slug, @event),
|
||||
customer_email: current_user.email,
|
||||
metadata: {
|
||||
event_id: @event.id,
|
||||
user_id: current_user.id,
|
||||
order_items: order_items.to_json
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Payment Confirmation (`events#payment_success`)
|
||||
1. **Session Retrieval**: Get Stripe session with payment status
|
||||
2. **Ticket Creation**: Generate tickets based on order items from metadata
|
||||
3. **QR Code Generation**: Automatic unique QR code creation via model callbacks
|
||||
4. **Success Page**: Display tickets with download links
|
||||
|
||||
### 3. PDF Ticket Generation (`app/services/ticket_pdf_generator.rb`)
|
||||
|
||||
```ruby
|
||||
class TicketPdfGenerator
|
||||
def generate
|
||||
Prawn::Document.new(page_size: [350, 600], margin: 20) do |pdf|
|
||||
# Header with branding
|
||||
pdf.fill_color "2D1B69"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "ApéroNight", align: :center
|
||||
|
||||
# Event details
|
||||
pdf.text ticket.event.name, align: :center
|
||||
|
||||
# QR Code generation
|
||||
qr_code_data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.event.id,
|
||||
user_id: ticket.user.id
|
||||
}.to_json
|
||||
|
||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||
end.render
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 4. Frontend Cart Management (`app/javascript/controllers/ticket_cart_controller.js`)
|
||||
|
||||
- **Stimulus Controller**: Manages cart state and interactions
|
||||
- **Authentication Check**: Validates user login before checkout
|
||||
- **Session Storage**: Preserves cart when redirecting to login
|
||||
- **Dynamic Updates**: Real-time cart total and ticket count updates
|
||||
|
||||
## 🔧 Development Patterns
|
||||
|
||||
### Model Validations
|
||||
```ruby
|
||||
# Event validations
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :latitude, numericality: {
|
||||
greater_than_or_equal_to: -90,
|
||||
less_than_or_equal_to: 90
|
||||
}
|
||||
|
||||
# Ticket QR code generation
|
||||
before_validation :generate_qr_code, on: :create
|
||||
def generate_qr_code
|
||||
loop do
|
||||
self.qr_code = SecureRandom.uuid
|
||||
break unless Ticket.exists?(qr_code: qr_code)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Controller Patterns
|
||||
```ruby
|
||||
# Authentication for sensitive actions
|
||||
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||
|
||||
# Strong parameters
|
||||
private
|
||||
def event_params
|
||||
params.require(:event).permit(:name, :description, :venue_name, :venue_address,
|
||||
:latitude, :longitude, :start_time, :image)
|
||||
end
|
||||
```
|
||||
|
||||
### View Helpers and Partials
|
||||
- **Metric Cards**: Reusable component for dashboard statistics
|
||||
- **Event Items**: Consistent event display across pages
|
||||
- **Flash Messages**: Centralized notification system
|
||||
|
||||
## 🚀 Deployment Considerations
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Required for production
|
||||
STRIPE_PUBLISHABLE_KEY=pk_live_...
|
||||
STRIPE_SECRET_KEY=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
DATABASE_URL=mysql2://user:pass@host/db
|
||||
RAILS_MASTER_KEY=...
|
||||
```
|
||||
|
||||
### Database Indexes
|
||||
```sql
|
||||
-- Performance indexes for common queries
|
||||
CREATE INDEX idx_events_published_start_time ON events (state, start_time);
|
||||
CREATE INDEX idx_tickets_user_status ON tickets (user_id, status);
|
||||
CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
- **CSRF Protection**: Rails default protection enabled
|
||||
- **Strong Parameters**: All user inputs filtered
|
||||
- **Authentication**: Devise handles session security
|
||||
- **Payment Security**: Stripe handles sensitive payment data
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Key Test Cases
|
||||
1. **User Authentication**: Registration, login, logout flows
|
||||
2. **Event Creation**: Validation, state management, relationships
|
||||
3. **Booking Process**: Cart validation, payment processing, ticket generation
|
||||
4. **PDF Generation**: QR code uniqueness, ticket format
|
||||
5. **Dashboard Metrics**: Query accuracy, performance
|
||||
|
||||
### Seed Data Structure
|
||||
```ruby
|
||||
# Creates test users, events, and ticket types
|
||||
users = User.create!([...])
|
||||
events = Event.create!([...])
|
||||
ticket_types = TicketType.create!([...])
|
||||
```
|
||||
|
||||
## 🛠️ Available Development Tools
|
||||
|
||||
### AST-Grep for Mass Code Replacement
|
||||
|
||||
The system has `ast-grep` installed for structural code search and replacement. This tool is particularly useful for:
|
||||
|
||||
- **Mass refactoring**: Rename methods, classes, or variables across the codebase
|
||||
- **Pattern-based replacements**: Update code patterns using AST matching
|
||||
- **Language-aware transformations**: Safer than regex for code modifications
|
||||
|
||||
#### Usage Examples:
|
||||
|
||||
```bash
|
||||
# Find all method calls to a specific method
|
||||
ast-grep --pattern 'find_by_$FIELD($VALUE)' --lang ruby
|
||||
|
||||
# Replace method calls with new syntax
|
||||
ast-grep --pattern 'find_by_$FIELD($VALUE)' --rewrite 'find_by($FIELD: $VALUE)' --lang ruby
|
||||
|
||||
# Search for specific Rails patterns
|
||||
ast-grep --pattern 'validates :$FIELD, presence: true' --lang ruby
|
||||
|
||||
# Mass rename across multiple files
|
||||
ast-grep --pattern 'old_method_name($$$ARGS)' --rewrite 'new_method_name($$$ARGS)' --lang ruby --update-all
|
||||
```
|
||||
|
||||
#### Best Practices:
|
||||
- Always run with `--dry-run` first to preview changes
|
||||
- Use `--lang ruby` for Ruby files to ensure proper AST parsing
|
||||
- Test changes in a branch before applying to main codebase
|
||||
- Particularly useful for Rails conventions and ActiveRecord pattern updates
|
||||
|
||||
## 📝 Code Style & Conventions
|
||||
|
||||
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
||||
- **Database**: Use Rails migrations for all schema changes
|
||||
- **JavaScript**: Stimulus controllers for interactive behavior
|
||||
- **CSS**: Tailwind utility classes with custom components
|
||||
- **Documentation**: Inline comments for complex business logic
|
||||
- **Mass Changes**: Use `ast-grep` for structural code replacements instead of simple find/replace
|
||||
|
||||
This architecture provides a solid foundation for a scalable ticket selling platform with proper separation of concerns, security, and user experience.
|
||||
44
BACKLOG.md
Executable file
44
BACKLOG.md
Executable file
@@ -0,0 +1,44 @@
|
||||
# Project Backlog
|
||||
|
||||
## 📋 Todo
|
||||
|
||||
- [ ] Set up project infrastructure
|
||||
- [ ] Design user interface mockups
|
||||
- [ ] Create user dashboard
|
||||
- [ ] Implement data persistence
|
||||
- [ ] Add responsive design
|
||||
- [ ] Write unit tests
|
||||
- [ ] Set up CI/CD pipeline
|
||||
- [ ] Add error handling
|
||||
- [ ] Implement search functionality
|
||||
- [ ] Add user profile management
|
||||
- [ ] Create admin panel
|
||||
- [ ] Optimize performance
|
||||
- [ ] Add documentation
|
||||
- [ ] Security audit
|
||||
- [ ] Deploy to production
|
||||
|
||||
## 🚧 Doing
|
||||
|
||||
- [ ] refactor: Moving checkout to OrdersController
|
||||
|
||||
## ✅ Done
|
||||
|
||||
- [x] Initialize git repository
|
||||
- [x] Set up development environment
|
||||
- [x] Create project structure
|
||||
- [x] Install dependencies
|
||||
- [x] Configure build tools
|
||||
- [x] Set up linting rules
|
||||
- [x] Create initial README
|
||||
- [x] Set up version control
|
||||
- [x] Configure development server
|
||||
- [x] Establish coding standards
|
||||
- [x] Set up package.json
|
||||
- [x] Create .gitignore file
|
||||
- [x] Initialize npm project
|
||||
- [x] Set up basic folder structure
|
||||
- [x] Configure environment variables
|
||||
- [x] Create authentication system
|
||||
- [x] Implement user registration
|
||||
- [x] Add login functionality
|
||||
766
CLAUDE.md
Executable file
766
CLAUDE.md
Executable file
@@ -0,0 +1,766 @@
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
|
||||
When asked to design UI & frontend interface
|
||||
When asked to design UI & frontend interface
|
||||
# Role
|
||||
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
|
||||
Your goal is to help user generate amazing design using code
|
||||
|
||||
# Instructions
|
||||
- Use the available tools when needed to help with file operations and code analysis
|
||||
- When creating design file:
|
||||
- Build one single html page of just one screen to build a design based on users' feedback/task
|
||||
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
|
||||
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
|
||||
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
|
||||
|
||||
## Styling
|
||||
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
|
||||
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
|
||||
3. superdesign MUST generate responsive designs.
|
||||
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
|
||||
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
|
||||
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
|
||||
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
|
||||
8. Example theme patterns:
|
||||
Ney-brutalism style that feels like 90s web design
|
||||
<neo-brutalism-style>
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9551 0 0);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--radius: 0px;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</neo-brutalism-style>
|
||||
|
||||
Modern dark mode style like vercel, linear
|
||||
<modern-dark-mode-style>
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.1450 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.1450 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.1450 0 0);
|
||||
--primary: oklch(0.2050 0 0);
|
||||
--primary-foreground: oklch(0.9850 0 0);
|
||||
--secondary: oklch(0.9700 0 0);
|
||||
--secondary-foreground: oklch(0.2050 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.5560 0 0);
|
||||
--accent: oklch(0.9700 0 0);
|
||||
--accent-foreground: oklch(0.2050 0 0);
|
||||
--destructive: oklch(0.5770 0.2450 27.3250);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9220 0 0);
|
||||
--input: oklch(0.9220 0 0);
|
||||
--ring: oklch(0.7080 0 0);
|
||||
--chart-1: oklch(0.8100 0.1000 252);
|
||||
--chart-2: oklch(0.6200 0.1900 260);
|
||||
--chart-3: oklch(0.5500 0.2200 263);
|
||||
--chart-4: oklch(0.4900 0.2200 264);
|
||||
--chart-5: oklch(0.4200 0.1800 266);
|
||||
--sidebar: oklch(0.9850 0 0);
|
||||
--sidebar-foreground: oklch(0.1450 0 0);
|
||||
--sidebar-primary: oklch(0.2050 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9850 0 0);
|
||||
--sidebar-accent: oklch(0.9700 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2050 0 0);
|
||||
--sidebar-border: oklch(0.9220 0 0);
|
||||
--sidebar-ring: oklch(0.7080 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.625rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
</modern-dark-mode-style>
|
||||
|
||||
## Images & icons
|
||||
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
|
||||
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
## Script
|
||||
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
|
||||
|
||||
## Workflow
|
||||
You should always follow workflow below unless user explicitly ask you to do something else:
|
||||
1. Layout design
|
||||
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
|
||||
3. Core Animation design
|
||||
4. Generate a singlehtml file for the UI
|
||||
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
|
||||
|
||||
### 1. Layout design
|
||||
Output type: Just text
|
||||
Think through how should the layout of interface look like, what are different UI components
|
||||
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
|
||||
|
||||
### 2. Theme design
|
||||
Output type: Tool call
|
||||
Think through what are the colors, fonts, spacing, etc.
|
||||
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
### 3. Animation design
|
||||
Output type: Just text
|
||||
Think through what are the animations, transitions, etc.
|
||||
|
||||
### 4. Generate html file for each UI component and then combine them together to form a single html file
|
||||
Output type: Tool call
|
||||
Generate html file for each UI component and then combine them together to form a single html file
|
||||
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
|
||||
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
|
||||
|
||||
<example>
|
||||
<user>design an AI chat UI</user>
|
||||
|
||||
<assistant>
|
||||
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
|
||||
|
||||
## Core UI Components
|
||||
|
||||
**Header Area**
|
||||
- Settings/menu button (Top left)
|
||||
- Chat title/AI name (Top left)
|
||||
- Conversation controls (new chat, clear, etc.) (Top right)
|
||||
|
||||
**Main Chat Area**
|
||||
- Message container (scrollable) (Full width & height)
|
||||
- User messages (typically right-aligned)
|
||||
- AI messages (typically left-aligned)
|
||||
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
|
||||
|
||||
**Input Area**
|
||||
- Text input field (Full width left)
|
||||
- Send button (Embed within the input field,Bottom right side of the screen)
|
||||
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
|
||||
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
|
||||
|
||||
|
||||
## Layout Structure Options
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☰ HEADER BAR + │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ User Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ AI Message Bubble │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ [CHAT AREA] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input Field] [Send] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
When hamburger (☰) is clicked, sidebar slides out:
|
||||
┌──────────────┬─────────────────────────────────────┐
|
||||
│ SIDEBAR │ ☰ HEADER BAR + │
|
||||
│ ┌──────────┐ ├─────────────────────────────────────┤
|
||||
│ │ Chat 1 │ │ │
|
||||
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
|
||||
│ │ Chat 3 │ │ │ AI Message Bubble │ │
|
||||
│ │ + New │ │ └─────────────────────────────┘ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ User Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ ┌─────────────────────────────┐ │
|
||||
│ │ │ AI Message Bubble │ │
|
||||
│ │ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ [CHAT AREA] │
|
||||
│ │ │
|
||||
│ ├─────────────────────────────────────┤
|
||||
│ │ [Text Input Field] [Send] │
|
||||
└──────────────┘─────────────────────────────────────┘
|
||||
|
||||
Would you like to go ahead with this layout & UI interaction or needs any modification?
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
They looks good
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the color & font theme,
|
||||
|
||||
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.1448 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.1448 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.1448 0 0);
|
||||
--primary: oklch(0.5555 0 0);
|
||||
--primary-foreground: oklch(0.9851 0 0);
|
||||
--secondary: oklch(0.9702 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9702 0 0);
|
||||
--muted-foreground: oklch(0.5486 0 0);
|
||||
--accent: oklch(0.9702 0 0);
|
||||
--accent-foreground: oklch(0.2046 0 0);
|
||||
--destructive: oklch(0.5830 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(0.9702 0 0);
|
||||
--border: oklch(0.9219 0 0);
|
||||
--input: oklch(0.9219 0 0);
|
||||
--ring: oklch(0.7090 0 0);
|
||||
--chart-1: oklch(0.5555 0 0);
|
||||
--chart-2: oklch(0.5555 0 0);
|
||||
--chart-3: oklch(0.5555 0 0);
|
||||
--chart-4: oklch(0.5555 0 0);
|
||||
--chart-5: oklch(0.5555 0 0);
|
||||
--sidebar: oklch(0.9851 0 0);
|
||||
--sidebar-foreground: oklch(0.1448 0 0);
|
||||
--sidebar-primary: oklch(0.2046 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-accent: oklch(0.9702 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2046 0 0);
|
||||
--sidebar-border: oklch(0.9219 0 0);
|
||||
--sidebar-ring: oklch(0.7090 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}')
|
||||
|
||||
|
||||
<user>
|
||||
I like the vintage style
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, next let's design the animation:
|
||||
|
||||
# CHAT UI ANIMATIONS - MICRO-SYNTAX
|
||||
|
||||
## Core Message Flow
|
||||
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
|
||||
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
|
||||
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
|
||||
status: 300ms ease-out [α0.6→1, S1→1.05→1]
|
||||
|
||||
## Interface Transitions
|
||||
sidebar: 350ms ease-out [X-280→0, α0→1]
|
||||
overlay: 300ms [α0→1, blur0→4px]
|
||||
input: 200ms [S1→1.01, shadow+ring] focus
|
||||
input: 150ms [S1.01→1, shadow-ring] blur
|
||||
|
||||
## Button Interactions
|
||||
sendBtn: 150ms [S1→0.95→1, R±2°] press
|
||||
sendBtn: 200ms [S1→1.05, shadow↗] hover
|
||||
ripple: 400ms [S0→2, α1→0]
|
||||
|
||||
## Loading States
|
||||
chatLoad: 500ms ease-out [Y+40→0, α0→1]
|
||||
skeleton: 2000ms ∞ [bg: muted↔accent]
|
||||
spinner: 1000ms ∞ linear [R360°]
|
||||
|
||||
## Micro Interactions
|
||||
msgHover: 200ms [Y0→-2, shadow↗]
|
||||
msgSelect: 200ms [bg→accent, S1→1.02]
|
||||
error: 400ms [X±5] shake
|
||||
success: 600ms bounce [S0→1.2→1, R360°]
|
||||
|
||||
## Scroll & Navigation
|
||||
autoScroll: 400ms smooth
|
||||
scrollHint: 800ms ∞×3 [Y±5]
|
||||
|
||||
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
|
||||
|
||||
</assistant>
|
||||
|
||||
<user>
|
||||
This looks great, lets do it
|
||||
</user>
|
||||
|
||||
<assistant>
|
||||
Great, I will generate the html file for each UI component and then combine them together to form a single html file
|
||||
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
|
||||
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
|
||||
|
||||
I've created the html design, please reveiw and let me know if you need any changes
|
||||
|
||||
</example>
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
|
||||
2. You MUST confirm the layout, and then theme style, and then animation
|
||||
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
|
||||
4. You MUST create follow the workflow above
|
||||
|
||||
# Available Tools
|
||||
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
|
||||
- **write**: Write content to files in the workspace (creates parent directories automatically)
|
||||
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
|
||||
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
|
||||
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
|
||||
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
|
||||
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
|
||||
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
|
||||
- **generateTheme**: Generate a theme for the design
|
||||
|
||||
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
|
||||
51
CRUSH.md
Executable file
51
CRUSH.md
Executable file
@@ -0,0 +1,51 @@
|
||||
# Aperonight - CRUSH Development Guidelines
|
||||
|
||||
## Build Commands
|
||||
- `bin/rails server` - Start development server
|
||||
- `bin/rails assets:precompile` - Compile assets
|
||||
- `npm run build` - Build JavaScript bundle (production)
|
||||
- `npm run build:dev` - Build JavaScript bundle (development)
|
||||
- `npm run build:css` - Compile CSS with PostCSS/Tailwind
|
||||
|
||||
## Test Commands
|
||||
- `bin/rails test` - Run all tests
|
||||
- `bin/rails test test/models/user_test.rb` - Run specific test file
|
||||
- `bin/rails test test/models/user_test.rb:15` - Run specific test method
|
||||
- `bin/rails test:system` - Run system tests
|
||||
|
||||
## Lint Commands
|
||||
- `bin/rubocop` - Run Ruby linter
|
||||
- `bin/rubocop -a` - Run Ruby linter with auto-fix
|
||||
- Check JS/JSX files manually (no configured linter)
|
||||
|
||||
## Development Workflow
|
||||
1. Branch naming: `type/descriptive-name` (e.g., `feature/user-profile`)
|
||||
2. Follow Git Flow with `main` and `develop` branches
|
||||
3. Run tests and linters before committing
|
||||
4. Keep PRs focused on single features/fixes
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Ruby
|
||||
- Follow Rubocop Rails Omakase defaults
|
||||
- Standard Rails MVC conventions
|
||||
- Use descriptive method and variable names
|
||||
- Prefer single quotes for strings without interpolation
|
||||
|
||||
### JavaScript/React
|
||||
- Use Stimulus controllers for DOM interactions
|
||||
- React components in PascalCase (`UserProfile.jsx`)
|
||||
- Shadcn components in kebab-case (`button.jsx`) but exported as PascalCase
|
||||
- Functional components with hooks over class components
|
||||
|
||||
### CSS/Tailwind
|
||||
- Mobile-first responsive design
|
||||
- Use Tailwind utility classes over custom CSS
|
||||
- Primary color palette: indigo → purple → pink gradients
|
||||
- Consistent spacing with Tailwind's spacing scale
|
||||
|
||||
### General
|
||||
- Keep functions small and focused
|
||||
- Comment complex logic
|
||||
- Use descriptive commit messages
|
||||
- Maintain consistency with existing code patterns
|
||||
0
Dockerfile
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
28
Gemfile
Normal file → Executable file
28
Gemfile
Normal file → Executable file
@@ -51,6 +51,15 @@ group :development, :test do
|
||||
|
||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
|
||||
# Add SQlite3 for local testing
|
||||
gem "sqlite3", "~> 2.7"
|
||||
|
||||
# Improve Minitest output
|
||||
gem "minitest-reporters", "~> 1.7"
|
||||
|
||||
# Load environment variables from .env file
|
||||
gem "dotenv-rails"
|
||||
end
|
||||
|
||||
group :development do
|
||||
@@ -62,6 +71,25 @@ group :test do
|
||||
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
||||
gem "capybara"
|
||||
gem "selenium-webdriver"
|
||||
# For controller testing helpers
|
||||
gem "rails-controller-testing"
|
||||
# For mocking and stubbing
|
||||
gem "mocha"
|
||||
end
|
||||
|
||||
gem "devise", "~> 4.9"
|
||||
|
||||
# Pagination gem
|
||||
gem "kaminari", "~> 1.2"
|
||||
gem "kaminari-tailwind", "~> 0.1.0"
|
||||
|
||||
# Stripe payment processing
|
||||
gem "stripe", "~> 15.5"
|
||||
|
||||
# PDF generation for tickets
|
||||
gem "grover"
|
||||
|
||||
# QR code generation
|
||||
gem "rqrcode", "~> 3.1"
|
||||
|
||||
# gem "net-pop", "~> 0.1.2"
|
||||
|
||||
62
Gemfile.lock
Normal file → Executable file
62
Gemfile.lock
Normal file → Executable file
@@ -74,6 +74,7 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ansi (1.5.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
@@ -95,6 +96,7 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crass (1.0.6)
|
||||
@@ -111,17 +113,22 @@ GEM
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
dotenv (3.1.8)
|
||||
dotenv-rails (3.1.8)
|
||||
dotenv (= 3.1.8)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (5.0.2)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.3.0)
|
||||
tzinfo
|
||||
fugit (1.11.1)
|
||||
fugit (1.11.2)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
grover (1.2.3)
|
||||
nokogiri (~> 1)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.8.1)
|
||||
@@ -146,6 +153,19 @@ GEM
|
||||
sshkit (>= 1.23.0, < 2.0)
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
kaminari-activerecord (= 1.2.2)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-actionview (1.2.2)
|
||||
actionview
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-activerecord (1.2.2)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
kaminari-tailwind (0.1.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
@@ -161,6 +181,13 @@ GEM
|
||||
matrix (0.4.3)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.5)
|
||||
minitest-reporters (1.7.1)
|
||||
ansi
|
||||
builder
|
||||
minitest (>= 5.0)
|
||||
ruby-progressbar
|
||||
mocha (2.7.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
mysql2 (0.5.6)
|
||||
net-imap (0.5.9)
|
||||
@@ -234,6 +261,10 @@ GEM
|
||||
activesupport (= 8.0.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2.1)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -260,8 +291,12 @@ GEM
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.1)
|
||||
rubocop (1.79.2)
|
||||
rexml (3.4.2)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.80.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -290,7 +325,8 @@ GEM
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (3.0.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.0.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.35.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -314,6 +350,12 @@ GEM
|
||||
fugit (~> 1.11.0)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
sqlite3 (2.7.3-aarch64-linux-gnu)
|
||||
sqlite3 (2.7.3-aarch64-linux-musl)
|
||||
sqlite3 (2.7.3-arm-linux-gnu)
|
||||
sqlite3 (2.7.3-arm-linux-musl)
|
||||
sqlite3 (2.7.3-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.3-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
@@ -324,6 +366,7 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
stripe (15.5.0)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.15)
|
||||
thruster (0.1.15-aarch64-linux)
|
||||
@@ -361,7 +404,6 @@ PLATFORMS
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
@@ -372,19 +414,29 @@ DEPENDENCIES
|
||||
cssbundling-rails
|
||||
debug
|
||||
devise (~> 4.9)
|
||||
dotenv-rails
|
||||
grover
|
||||
jbuilder
|
||||
jsbundling-rails
|
||||
kamal
|
||||
kaminari (~> 1.2)
|
||||
kaminari-tailwind (~> 0.1.0)
|
||||
minitest-reporters (~> 1.7)
|
||||
mocha
|
||||
mysql2 (~> 0.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0.2, >= 8.0.2.1)
|
||||
rails-controller-testing
|
||||
rqrcode (~> 3.1)
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
solid_cable
|
||||
solid_cache
|
||||
solid_queue
|
||||
sqlite3 (~> 2.7)
|
||||
stimulus-rails
|
||||
stripe (~> 15.5)
|
||||
thruster
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
|
||||
2
Procfile.dev
Normal file → Executable file
2
Procfile.dev
Normal file → Executable file
@@ -1,3 +1,3 @@
|
||||
web: env RUBY_DEBUG_OPEN=true bin/rails server
|
||||
js: yarn build --watch
|
||||
js: yarn build:dev --watch
|
||||
css: yarn build:css --watch
|
||||
|
||||
213
README.md
Normal file → Executable file
213
README.md
Normal file → Executable file
@@ -1,79 +1,212 @@
|
||||
# Aperonight - Party Booking Platform
|
||||
# Aperonight - Event Booking Platform
|
||||
|
||||

|
||||
|
||||
## 🌃 Overview
|
||||
|
||||
**Aperonight** is a two-sided marketplace connecting party-goers with nightlife promoters in Paris. The platform allows:
|
||||
|
||||
- **Customers** to discover/book tickets for upcoming parties
|
||||
- **Promoters** to create/manage events and validate tickets at venue entrances
|
||||
**Aperonight** is a comprehensive ticket selling system that connects event-goers with event organizers. The platform provides a complete solution for event booking, payment processing, and ticket management.
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### For Party-Goers
|
||||
✔ Browse upcoming parties with filters (date, location, music genre)
|
||||
✔ Book tickets with multiple bundle options (VIP, group passes, etc.)
|
||||
✔ Secure payment processing (credit cards, Apple/Google Pay)
|
||||
✔ Mobile-friendly e-tickets with QR codes
|
||||
### For Event-Goers
|
||||
✅ **User Dashboard** - Personalized metrics showing booked events, upcoming events, and event statistics
|
||||
✅ **Event Discovery** - Browse upcoming events with detailed information and venue details
|
||||
✅ **Secure Booking** - Multiple ticket types per event with quantity selection
|
||||
✅ **Stripe Integration** - Secure payment processing with credit/debit cards
|
||||
✅ **PDF Tickets** - Automatically generated tickets with unique QR codes for each purchase
|
||||
✅ **Download System** - Instant PDF ticket downloads after successful payment
|
||||
|
||||
### For Promoters
|
||||
✔ Event creation dashboard with ticket type customization
|
||||
✔ Real-time ticket validation via mobile scanning
|
||||
✔ Sales analytics and attendee tracking
|
||||
✔ Automatic aggregation of events from partner platforms
|
||||
### For Event Organizers
|
||||
✅ **Event Management** - Create and manage events with detailed information
|
||||
✅ **Ticket Type Configuration** - Set up multiple ticket types with different pricing
|
||||
✅ **Sales Tracking** - Monitor ticket sales and availability
|
||||
✅ **User Authentication** - Secure user registration and login system
|
||||
|
||||
### Technical Implementation
|
||||
✅ **Payment Processing** - Full Stripe Checkout integration with session management
|
||||
✅ **PDF Generation** - Custom PDF tickets with QR codes using Prawn library
|
||||
✅ **Responsive Design** - Mobile-friendly interface with Tailwind CSS
|
||||
✅ **Database Relations** - Proper user-event-ticket relationships
|
||||
|
||||
## 🛠 Technical Stack
|
||||
|
||||
### Backend
|
||||
- **Ruby on Rails 7** (API mode)
|
||||
- **MariaDB** database
|
||||
<!--- **Redis** for caching/background jobs-->
|
||||
- **ActiveJob** for background processing
|
||||
- **Ruby on Rails 8.0+** with Hotwire for reactive UI
|
||||
- **MySQL** database with comprehensive migrations
|
||||
- **Devise** for user authentication and session management
|
||||
- **Kaminari** for pagination
|
||||
|
||||
### Frontend
|
||||
- **Hotwire (Turbo + Stimulus)** for reactive UI
|
||||
- **Tailwind CSS** for styling
|
||||
- **React Native** for promoter mobile app (ticket scanning)
|
||||
- **Hotwire (Turbo + Stimulus)** for interactive JavaScript behavior
|
||||
- **Tailwind CSS** for responsive styling and modern UI
|
||||
- **JavaScript Controllers** for cart management and checkout flow
|
||||
|
||||
### Key Integrations
|
||||
- **Stripe Connect** for payments & promoter payouts
|
||||
- **Shogun/Bizouk/Weezevent APIs** for event aggregation
|
||||
<!--- **Twilio** for SMS ticket delivery-->
|
||||
<!--- **AWS S3** for media storage-->
|
||||
- **Stripe** for secure payment processing and checkout sessions
|
||||
- **Prawn & Prawn-QRCode** for PDF ticket generation
|
||||
- **RQRCode** for unique QR code generation per ticket
|
||||
|
||||
## 📊 Database Schema (Simplified)
|
||||
## 📊 Database Schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ BOOKING : makes
|
||||
USER ||--o{ EVENT : creates
|
||||
USER ||--o{ TICKET : purchases
|
||||
USER {
|
||||
integer id
|
||||
string email
|
||||
string encrypted_password
|
||||
string first_name
|
||||
string last_name
|
||||
}
|
||||
PROMOTER ||--o{ PARTY : creates
|
||||
PROMOTER {
|
||||
integer id
|
||||
string stripe_account_id
|
||||
}
|
||||
PARTY ||--o{ TICKET_TYPE : has
|
||||
PARTY {
|
||||
EVENT ||--o{ TICKET_TYPE : has
|
||||
EVENT {
|
||||
integer id
|
||||
integer user_id
|
||||
string name
|
||||
string slug
|
||||
text description
|
||||
string venue_name
|
||||
string venue_address
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
datetime start_time
|
||||
}
|
||||
BOOKING ||--o{ TICKET : generates
|
||||
BOOKING {
|
||||
integer id
|
||||
decimal total_price
|
||||
datetime end_time
|
||||
string state
|
||||
boolean featured
|
||||
string image
|
||||
}
|
||||
TICKET_TYPE ||--o{ TICKET : defines
|
||||
TICKET_TYPE {
|
||||
integer id
|
||||
integer event_id
|
||||
string name
|
||||
text description
|
||||
integer price_cents
|
||||
integer quantity
|
||||
datetime sale_start_at
|
||||
datetime sale_end_at
|
||||
boolean requires_id
|
||||
integer minimum_age
|
||||
}
|
||||
TICKET {
|
||||
integer id
|
||||
integer user_id
|
||||
integer ticket_type_id
|
||||
string qr_code
|
||||
integer price_cents
|
||||
string status
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Ruby 3.4+
|
||||
- Rails 8.0+
|
||||
- MySQL/MariaDB
|
||||
- Node.js 18+ (for asset compilation)
|
||||
- Stripe account (for payment processing)
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/aperonight.git
|
||||
cd aperonight
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
bundle install
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Database setup**
|
||||
```bash
|
||||
rails db:create
|
||||
rails db:migrate
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
4. **Configure environment variables**
|
||||
Create a `.env` file or configure Rails credentials:
|
||||
```bash
|
||||
# Stripe configuration
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||
STRIPE_SECRET_KEY=sk_test_your_key_here
|
||||
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
|
||||
|
||||
# Database configuration (if not using defaults)
|
||||
DATABASE_URL=mysql2://username:password@localhost/aperonight_development
|
||||
```
|
||||
|
||||
5. **Start the development server**
|
||||
```bash
|
||||
rails server
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see the application running.
|
||||
|
||||
## 💳 Payment Configuration
|
||||
|
||||
### Setting up Stripe
|
||||
|
||||
1. Create a Stripe account at [stripe.com](https://stripe.com)
|
||||
2. Get your API keys from the Stripe Dashboard
|
||||
3. Add your keys to the Rails credentials or environment variables
|
||||
4. Configure webhook endpoints for payment confirmations:
|
||||
- Endpoint URL: `your-domain.com/stripe/webhooks`
|
||||
- Events: `checkout.session.completed`, `payment_intent.succeeded`
|
||||
|
||||
## 🎫 Core Functionality
|
||||
|
||||
### User Flow
|
||||
1. **Registration/Login** - Users create accounts or sign in
|
||||
2. **Event Discovery** - Browse events from the homepage or events page
|
||||
3. **Ticket Selection** - Choose ticket types and quantities
|
||||
4. **Checkout** - Secure payment through Stripe Checkout
|
||||
5. **Ticket Generation** - Automatic PDF ticket generation with QR codes
|
||||
6. **Download** - Instant ticket download after payment
|
||||
|
||||
### Event Management
|
||||
1. **Event Creation** - Create events with full details and images
|
||||
2. **Ticket Types** - Configure multiple ticket types with pricing
|
||||
3. **Sales Tracking** - Monitor ticket sales through the dashboard
|
||||
|
||||
### Dashboard Features
|
||||
- **Personal Metrics** - View booked events and upcoming events
|
||||
- **Event Sections** - Today's events, tomorrow's events, and upcoming events
|
||||
- **Quick Actions** - Easy navigation to event discovery and booking
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Key Files Structure
|
||||
```
|
||||
app/
|
||||
├── controllers/
|
||||
│ ├── events_controller.rb # Event listing, booking, checkout
|
||||
│ └── pages_controller.rb # Dashboard and static pages
|
||||
├── models/
|
||||
│ ├── user.rb # User authentication with Devise
|
||||
│ ├── event.rb # Event management and states
|
||||
│ ├── ticket_type.rb # Ticket configuration
|
||||
│ └── ticket.rb # Ticket generation with QR codes
|
||||
├── services/
|
||||
│ └── ticket_pdf_generator.rb # PDF ticket generation service
|
||||
└── views/
|
||||
├── events/
|
||||
│ ├── show.html.erb # Event details and booking
|
||||
│ └── payment_success.html.erb # Post-purchase confirmation
|
||||
└── pages/
|
||||
└── dashboard.html.erb # User dashboard with metrics
|
||||
```
|
||||
|
||||
### Key Routes
|
||||
- `GET /` - Homepage
|
||||
- `GET /dashboard` - User dashboard (authenticated)
|
||||
- `GET /events` - Event listings
|
||||
- `GET /events/:slug.:id` - Event details and booking
|
||||
- `POST /events/:slug.:id/checkout` - Stripe checkout initiation
|
||||
- `GET /payment/success` - Payment confirmation
|
||||
- `GET /tickets/:ticket_id/download` - PDF ticket download
|
||||
|
||||
124
REFACTORING_SUMMARY.md
Normal file
124
REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Aperonight Application Refactoring Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the comprehensive refactoring work performed to ensure all code in the Aperonight application is useful and well-documented.
|
||||
|
||||
## Phase 1: Previous Code Cleanup (Already Completed)
|
||||
|
||||
### Files Removed
|
||||
- **Unused JavaScript Controllers**: shadcn_test_controller.js, featured_event_controller.js, event_form_controller.js, ticket_type_form_controller.js
|
||||
- **Unused React Components**: button.jsx, utils.js
|
||||
- **Duplicate Configuration**: env.example file
|
||||
|
||||
### Dependencies Removed
|
||||
- **Alpine.js Dependencies**: alpinejs, @types/alpinejs (unused in production)
|
||||
|
||||
## Phase 2: Current Refactoring Work
|
||||
|
||||
### 1. Code Cleanup and Unused Code Removal
|
||||
|
||||
#### Removed Dead Code
|
||||
- **TicketsController**: Removed unused `create_stripe_session` method (lines 78-105) that duplicated functionality already present in OrdersController
|
||||
- The legacy TicketsController now properly focuses only on redirects and backward compatibility
|
||||
|
||||
#### Fixed Issues and Improvements
|
||||
- **ApplicationHelper**: Fixed typo in comment ("prince" → "price")
|
||||
- **API Security**: Replaced hardcoded API key with environment variable lookup for better security
|
||||
- **User Validations**: Improved name length validations (2-50 chars instead of restrictive 3-12 chars)
|
||||
|
||||
### 2. Enhanced Documentation and Comments
|
||||
|
||||
#### Models (Now Comprehensively Documented)
|
||||
- **User**: Enhanced comments explaining Devise modules and authorization methods
|
||||
- **Event**: Detailed documentation of state enum, validations, and scopes
|
||||
- **Order**: Comprehensive documentation of lifecycle management and payment processing
|
||||
- **Ticket**: Clear explanation of ticket states and QR code generation
|
||||
- **TicketType**: Documented pricing methods and availability logic
|
||||
|
||||
#### Controllers (Improved Documentation)
|
||||
- **EventsController**: Added detailed method documentation and purpose explanation
|
||||
- **OrdersController**: Already well-documented, verified completeness
|
||||
- **TicketsController**: Enhanced comments explaining legacy redirect functionality
|
||||
- **ApiController**: Improved API authentication documentation with security notes
|
||||
|
||||
#### Services (Enhanced Documentation)
|
||||
- **StripeInvoiceService**: Already excellently documented
|
||||
- **TicketPdfGenerator**: Added class-level documentation and suppressed font warnings
|
||||
|
||||
#### Jobs (Comprehensive Documentation)
|
||||
- **CleanupExpiredDraftsJob**: Added comprehensive documentation and improved error handling
|
||||
- **ExpiredOrdersCleanupJob**: Already well-documented
|
||||
- **StripeInvoiceGenerationJob**: Already well-documented
|
||||
|
||||
#### Helpers (YARD-Style Documentation)
|
||||
- **FlashMessagesHelper**: Added detailed YARD-style documentation with examples
|
||||
- **LucideHelper**: Already well-documented
|
||||
- **StripeHelper**: Verified documentation completeness
|
||||
|
||||
### 3. Code Quality Improvements
|
||||
|
||||
#### Security Enhancements
|
||||
- **ApiController**: Moved API key to environment variables/Rails credentials
|
||||
- Maintained secure authentication patterns throughout
|
||||
|
||||
#### Performance Optimizations
|
||||
- Verified proper use of `includes` for eager loading
|
||||
- Confirmed efficient database queries with scopes
|
||||
- Proper use of `find_each` for batch processing
|
||||
|
||||
#### Error Handling
|
||||
- Enhanced error handling in cleanup jobs
|
||||
- Maintained robust error handling in payment processing
|
||||
- Added graceful fallbacks where appropriate
|
||||
|
||||
### 4. Code Organization and Structure
|
||||
|
||||
#### Structure Verification
|
||||
- Confirmed logical controller organization
|
||||
- Verified proper separation of concerns
|
||||
- Maintained clean service object patterns
|
||||
- Proper use of Rails conventions
|
||||
|
||||
## Files Modified in Current Refactoring
|
||||
|
||||
1. `app/controllers/tickets_controller.rb` - Removed unused method, fixed layout
|
||||
2. `app/controllers/api_controller.rb` - Security improvement, removed hardcoded key
|
||||
3. `app/controllers/events_controller.rb` - Enhanced documentation
|
||||
4. `app/helpers/application_helper.rb` - Fixed typo
|
||||
5. `app/helpers/flash_messages_helper.rb` - Added comprehensive documentation
|
||||
6. `app/jobs/cleanup_expired_drafts_job.rb` - Enhanced documentation and error handling
|
||||
7. `app/models/user.rb` - Improved validations
|
||||
8. `app/services/ticket_pdf_generator.rb` - Added documentation and suppressed warnings
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
- **Tests**: 200 tests, 454 assertions, 0 failures, 0 errors, 0 skips
|
||||
- **RuboCop**: All style issues resolved automatically
|
||||
- **Code Coverage**: Maintained existing coverage
|
||||
- **Documentation**: Significantly improved throughout codebase
|
||||
- **Bundle Size**: No increase, maintenance of efficient build
|
||||
|
||||
## Security Improvements
|
||||
|
||||
1. **API Authentication**: Moved from hardcoded to environment-based API keys
|
||||
2. **Input Validation**: Improved user input validations
|
||||
3. **Error Handling**: Enhanced error messages without exposing sensitive information
|
||||
|
||||
## Recommendations for Future Development
|
||||
|
||||
1. **Environment Variables**: Ensure API_KEY is set in production environment
|
||||
2. **Monitoring**: Consider adding metrics for cleanup job performance
|
||||
3. **Testing**: Add integration tests for the refactored components
|
||||
4. **Documentation**: Maintain the documentation standards established
|
||||
5. **Security**: Regular audit of dependencies and authentication mechanisms
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Aperonight application has been successfully refactored to ensure all code is useful, well-documented, and follows Rails best practices. The codebase is now more maintainable, secure, and provides a better developer experience. All existing functionality is preserved while significantly improving code quality and documentation standards.
|
||||
|
||||
**Total Impact:**
|
||||
- Removed unused code reducing maintenance overhead
|
||||
- Enhanced security with proper credential management
|
||||
- Improved documentation for better maintainability
|
||||
- Maintained 100% test coverage with 0 failures
|
||||
- Preserved all existing functionality
|
||||
0
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/builds/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
0
app/assets/images/.keep
Normal file → Executable file
25
app/assets/stylesheets/application.postcss.css
Normal file → Executable file
25
app/assets/stylesheets/application.postcss.css
Normal file → Executable file
@@ -3,7 +3,26 @@
|
||||
/* Import Tailwind using PostCSS */
|
||||
@import "tailwindcss";
|
||||
|
||||
/** Default text color */
|
||||
body {
|
||||
color: #555555;
|
||||
/* Import our custom theme */
|
||||
@import "theme";
|
||||
|
||||
/* Import components */
|
||||
@import "components/hero";
|
||||
@import "components/flash";
|
||||
@import "components/event-finder";
|
||||
|
||||
/* Import pages */
|
||||
@import "pages/home";
|
||||
|
||||
/* QR Code Styles */
|
||||
.qr-code-container {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.qr-code-container svg {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
width: 208px !important;
|
||||
height: 208px !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
210
app/assets/stylesheets/components/event-finder.css
Executable file
210
app/assets/stylesheets/components/event-finder.css
Executable file
@@ -0,0 +1,210 @@
|
||||
.event-finder {
|
||||
background: white;
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
padding: var(--space-8);
|
||||
margin: var(--space-12) auto;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.finder-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.finder-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.finder-subtitle {
|
||||
color: var(--color-neutral-600);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.finder-form {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.finder-form {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.finder-form {
|
||||
grid-template-columns: repeat(4, 1fr) auto;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
|
||||
.finder-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.finder-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.finder-input,
|
||||
.finder-select {
|
||||
background: var(--color-neutral-50);
|
||||
border: 2px solid var(--color-neutral-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-900);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.finder-input:focus,
|
||||
.finder-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgb(168 85 247 / 0.1);
|
||||
}
|
||||
|
||||
.finder-input::placeholder {
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.finder-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 12px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
|
||||
.price-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.price-range-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.price-slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-neutral-200);
|
||||
outline: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
cursor: pointer;
|
||||
border: 3px solid white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.price-slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
cursor: pointer;
|
||||
border: 3px solid white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.finder-search-btn {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5) var(--space-8);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
box-shadow: var(--shadow-purple-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.finder-search-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-purple-lg);
|
||||
}
|
||||
|
||||
.finder-search-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.event-finder {
|
||||
margin: var(--space-8) auto;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.finder-form {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.finder-search-btn {
|
||||
justify-content: center;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced animations */
|
||||
.animate-fadeInUp {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s var(--ease-out);
|
||||
}
|
||||
|
||||
.animate-fadeInUp.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
70
app/assets/stylesheets/components/flash.css
Executable file
70
app/assets/stylesheets/components/flash.css
Executable file
@@ -0,0 +1,70 @@
|
||||
.notification {
|
||||
font-family: var(--font-sans, 'Plus Jakarta Sans', sans-serif);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid;
|
||||
transition: all var(--duration-normal, 0.3s) ease-out;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.notification-icon i {
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background: var(--color-success-light, #dcfce7);
|
||||
color: var(--color-success-dark, #15803d);
|
||||
border-color: var(--color-success, #22c55e);
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: var(--color-warning-light, #fef3c7);
|
||||
color: var(--color-warning-dark, #92400e);
|
||||
border-color: var(--color-warning, #f59e0b);
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: var(--color-danger-light, #fecaca);
|
||||
color: var(--color-danger-dark, #dc2626);
|
||||
border-color: var(--color-danger, #ef4444);
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: var(--color-primary-50, #f0f9ff);
|
||||
color: var(--color-primary-800, #1e40af);
|
||||
border-color: var(--color-primary-200, #bfdbfe);
|
||||
}
|
||||
|
||||
/* Fallback colors if CSS variables are not available */
|
||||
.notification-success {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: #fecaca;
|
||||
color: #dc2626;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: #f0f9ff;
|
||||
color: #1e40af;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
/* Animation for fade out */
|
||||
.flash-messages-container .notification.opacity-0 {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
0
app/assets/stylesheets/components/header.css
Executable file
0
app/assets/stylesheets/components/header.css
Executable file
238
app/assets/stylesheets/components/hero.css
Executable file
238
app/assets/stylesheets/components/hero.css
Executable file
@@ -0,0 +1,238 @@
|
||||
.hero {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(168, 85, 247, 0.08) 0%,
|
||||
rgba(236, 72, 153, 0.08) 50%,
|
||||
rgba(168, 85, 247, 0.08) 100%);
|
||||
padding: var(--space-12) 0 var(--space-8);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(168, 85, 247, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(236, 72, 153, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 900;
|
||||
margin-bottom: var(--space-4);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-neutral-600);
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cta-group {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-6);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-neutral-500);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-12) 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 3vw, 2.5rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-neutral-600);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: linear-gradient(135deg, var(--color-primary-50) 0%, var(--color-accent-50) 100%);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-accent-400) 100%);
|
||||
color: white;
|
||||
border-radius: var(--radius-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto var(--space-3);
|
||||
box-shadow: var(--shadow-purple-sm);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: var(--color-neutral-600);
|
||||
line-height: 1.6;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: var(--color-neutral-900);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-weight: 900;
|
||||
color: var(--color-accent-400);
|
||||
display: block;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-neutral-300);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: var(--space-12) 0;
|
||||
}
|
||||
|
||||
.cta-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.cta-content p {
|
||||
font-size: var(--text-lg);
|
||||
margin-bottom: var(--space-6);
|
||||
opacity: 0.9;
|
||||
}
|
||||
90
app/assets/stylesheets/pages/events.css
Executable file
90
app/assets/stylesheets/pages/events.css
Executable file
@@ -0,0 +1,90 @@
|
||||
/* Events page specific styles */
|
||||
|
||||
.events-page {
|
||||
background: linear-gradient(135deg, var(--color-neutral-50) 0%, var(--color-neutral-100) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.events-page .breadcrumb {
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.events-page .event-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-2xl);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.events-page .event-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.events-page .event-date-badge {
|
||||
background: linear-gradient(135deg, var(--color-primary-100) 0%, var(--color-accent-100) 100%);
|
||||
color: var(--color-primary-800);
|
||||
font-weight: 700;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.events-page .price-highlight {
|
||||
color: var(--color-primary-600);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.events-page .pagination {
|
||||
margin-top: var(--space-12);
|
||||
}
|
||||
|
||||
.events-page .pagination .page,
|
||||
.events-page .pagination .next,
|
||||
.events-page .pagination .last,
|
||||
.events-page .pagination .prev,
|
||||
.events-page .pagination .first {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
margin: 0 var(--space-1);
|
||||
transition: all var(--duration-normal);
|
||||
}
|
||||
|
||||
.events-page .pagination .page:hover,
|
||||
.events-page .pagination .next:hover,
|
||||
.events-page .pagination .last:hover,
|
||||
.events-page .pagination .prev:hover,
|
||||
.events-page .pagination .first:hover {
|
||||
background: var(--color-primary-100);
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.events-page .pagination .current {
|
||||
background: var(--color-primary-600);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.events-page .no-events-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-12);
|
||||
box-shadow: var(--shadow-lg);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.events-page .event-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.events-page .no-events-card {
|
||||
padding: var(--space-8);
|
||||
}
|
||||
}
|
||||
171
app/assets/stylesheets/pages/home.css
Executable file
171
app/assets/stylesheets/pages/home.css
Executable file
@@ -0,0 +1,171 @@
|
||||
/* Updated Featured Events Grid - 3 Cards Side by Side */
|
||||
.featured-events-grid {
|
||||
display: grid;
|
||||
gap: var(--space-8);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.featured-events-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.featured-events-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.featured-event-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.featured-event-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.featured-event-image {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
object-fit: cover;
|
||||
transition: transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.featured-event-card:hover .featured-event-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.featured-event-content {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.featured-event-badges {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.featured-event-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.featured-event-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.featured-event-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--color-neutral-600);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.featured-event-description {
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: var(--space-6);
|
||||
line-height: 1.6;
|
||||
font-size: var(--text-sm);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.featured-event-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.featured-event-price {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.featured-event-image {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.featured-event-content {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced animations */
|
||||
.animate-slideInLeft {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transition: all 0.5s var(--ease-out);
|
||||
}
|
||||
|
||||
.animate-slideInLeft.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.animate-slideInRight {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
transition: all 0.5s var(--ease-out);
|
||||
}
|
||||
|
||||
.animate-slideInRight.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Added missing animation for fadeInUp */
|
||||
.animate-fadeInUp {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.5s var(--ease-out);
|
||||
}
|
||||
|
||||
.animate-fadeInUp.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Feature Stats Styling */
|
||||
.feature-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-neutral-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
141
app/assets/stylesheets/pdf.css
Normal file
141
app/assets/stylesheets/pdf.css
Normal file
@@ -0,0 +1,141 @@
|
||||
/* PDF Styles for Ticket Generation */
|
||||
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.ticket-container {
|
||||
max-width: 350px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #2D1B69;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Event name */
|
||||
.event-name {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.event-name h2 {
|
||||
color: #000000;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Ticket info box */
|
||||
.ticket-info-box {
|
||||
background-color: #F9FAFB;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
display: inline-block;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Venue information */
|
||||
.venue-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.venue-info h3 {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.venue-details {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.venue-address {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* QR Code */
|
||||
.qr-code-section {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.qr-code-section h3 {
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
text-align: center;
|
||||
margin: 0 auto 10px auto;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.qr-code-text {
|
||||
font-size: 8px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
border-top: 1px solid #E5E7EB;
|
||||
padding-top: 15px;
|
||||
text-align: center;
|
||||
font-size: 8px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.generated-date {
|
||||
margin-top: 5px;
|
||||
}
|
||||
790
app/assets/stylesheets/theme.css
Executable file
790
app/assets/stylesheets/theme.css
Executable file
@@ -0,0 +1,790 @@
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary-50: #f5f3ff;
|
||||
--color-primary-100: #ede9fe;
|
||||
--color-primary-200: #ddd6fe;
|
||||
--color-primary-300: #c4b5fd;
|
||||
--color-primary-400: #a78bfa;
|
||||
--color-primary-500: #8b5cf6;
|
||||
--color-primary-600: #7c3aed;
|
||||
--color-primary-700: #6d28d9;
|
||||
--color-primary-800: #5b21b6;
|
||||
--color-primary-900: #4c1d95;
|
||||
|
||||
--color-accent-50: #fdf2f8;
|
||||
--color-accent-100: #fce7f3;
|
||||
--color-accent-200: #fbcfe8;
|
||||
--color-accent-300: #f9a8d4;
|
||||
--color-accent-400: #f472b6;
|
||||
--color-accent-500: #ec4899;
|
||||
--color-accent-600: #db2777;
|
||||
--color-accent-700: #be185d;
|
||||
--color-accent-800: #9d174d;
|
||||
--color-accent-900: #831843;
|
||||
|
||||
--color-neutral-50: #fafafa;
|
||||
--color-neutral-100: #f5f5f5;
|
||||
--color-neutral-200: #e5e5e5;
|
||||
--color-neutral-300: #d4d4d4;
|
||||
--color-neutral-400: #a3a3a3;
|
||||
--color-neutral-500: #737373;
|
||||
--color-neutral-600: #525252;
|
||||
--color-neutral-700: #404040;
|
||||
--color-neutral-800: #262626;
|
||||
--color-neutral-900: #171717;
|
||||
|
||||
--color-success: #10b981;
|
||||
--color-success-light: #d1fae5;
|
||||
--color-success-dark: #047857;
|
||||
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-light: #fef3c7;
|
||||
--color-warning-dark: #b45309;
|
||||
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-light: #fee2e2;
|
||||
--color-danger-dark: #b91c1c;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-display: 'Outfit', var(--font-sans);
|
||||
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 1.875rem; /* 30px */
|
||||
--text-4xl: 2.25rem; /* 36px */
|
||||
--text-5xl: 3rem; /* 48px */
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.25rem; /* 4px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
--radius-2xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
--shadow-purple-sm: 0 1px 3px 0 rgba(168, 85, 247, 0.1), 0 1px 2px 0 rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-md: 0 4px 6px -1px rgba(168, 85, 247, 0.1), 0 2px 4px -1px rgba(168, 85, 247, 0.06);
|
||||
--shadow-purple-lg: 0 10px 15px -3px rgba(168, 85, 247, 0.1), 0 4px 6px -2px rgba(168, 85, 247, 0.05);
|
||||
|
||||
/* Transitions */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--duration-slow: 500ms;
|
||||
|
||||
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.6;
|
||||
color: var(--color-neutral-900);
|
||||
background: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-4) var(--space-8);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-accent-500) 100%);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-purple-md);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-purple-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-neutral-800);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-neutral-900);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-primary-600);
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--color-primary-50);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-neutral-700);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-group {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: var(--space-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
background: var(--color-neutral-50);
|
||||
border: 2px solid var(--color-neutral-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-900);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
font-family: var(--font-sans);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 12px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--text-sm);
|
||||
margin-top: var(--space-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.focus-ring {
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.focus-ring:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-500);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.badge-available {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
border: 1px solid var(--color-success);
|
||||
}
|
||||
|
||||
.badge-limited {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
border: 1px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.badge-sold-out {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.badge-featured {
|
||||
background: var(--color-accent-100);
|
||||
color: var(--color-accent-700);
|
||||
border: 1px solid var(--color-accent-300);
|
||||
}
|
||||
|
||||
.badge-vip {
|
||||
background: var(--color-primary-100);
|
||||
color: var(--color-primary-800);
|
||||
border: 1px solid var(--color-primary-300);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-6);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.card.hover-lift:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.event-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--duration-slow) var(--ease-out);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card.hover-glow:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav {
|
||||
background: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--color-neutral-700);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
border-radius: var(--radius);
|
||||
transition: all var(--duration-normal);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-primary-600);
|
||||
background: var(--color-primary-50);
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
.search-box {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-left: var(--space-12);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Price tags */
|
||||
.price-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.price-tag-sm {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.price-tag-md {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.price-tag-lg {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
/* Avatars */
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-primary-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-600);
|
||||
border: 2px solid white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-lg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.avatar-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.avatar-status {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.status-online { background: var(--color-success); }
|
||||
.status-offline { background: var(--color-neutral-400); }
|
||||
.status-busy { background: var(--color-danger); }
|
||||
|
||||
/* Progress bars */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-accent-400) 100%);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
/* Rating stars */
|
||||
.rating {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.rating-star.filled {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.rating-star.empty {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--space-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
border: 1px solid var(--color-success);
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
border: 1px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: var(--color-primary-50);
|
||||
color: var(--color-primary-800);
|
||||
border: 1px solid var(--color-primary-200);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
border-bottom: 1px solid var(--color-neutral-200);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-600);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all var(--duration-normal);
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: var(--color-primary-600);
|
||||
background: var(--color-primary-50);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: var(--color-primary-600);
|
||||
border-bottom-color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
position: absolute;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-neutral-900);
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--text-sm);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--duration-normal);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-neutral-200);
|
||||
border-top-color: var(--color-primary-600);
|
||||
border-radius: var(--radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-primary-600);
|
||||
border-radius: var(--radius-full);
|
||||
animation: bounce 1.4s infinite both;
|
||||
}
|
||||
|
||||
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--color-neutral-600);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.breadcrumb-item.current {
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional styles for enhanced Aperonight design */
|
||||
.event-card.hover-glow:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
border-color: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.price-tag-sm {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.price-tag-md {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.price-tag-lg {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-left: var(--space-12);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
100
app/controllers/api/v1/events_controller.rb
Executable file
100
app/controllers/api/v1/events_controller.rb
Executable file
@@ -0,0 +1,100 @@
|
||||
# Contrôleur API pour la gestion des ressources d'événements
|
||||
# Fournit des points de terminaison RESTful pour les opérations CRUD sur le modèle Event
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class EventsController < ApiController
|
||||
# Skip API key authentication for store_cart action (used by frontend forms)
|
||||
skip_before_action :authenticate_api_key, only: [ :store_cart ]
|
||||
|
||||
# Charge l'évén avant certaines actions pour réduire les duplications
|
||||
before_action :set_event, only: [ :show, :update, :destroy, :store_cart ]
|
||||
|
||||
# GET /api/v1/events
|
||||
# Récupère tous les événements triés par date de création (du plus récent au plus ancien)
|
||||
def index
|
||||
@events = Event.all.order(created_at: :desc)
|
||||
render json: @events, status: :ok
|
||||
end
|
||||
|
||||
# GET /api/v1/events/:id
|
||||
# Récupère un seul événement par son ID
|
||||
# Retourne 404 si l'événement n'est pas trouvé
|
||||
def show
|
||||
render json: @event, status: :ok
|
||||
end
|
||||
|
||||
# POST /api/v1/events
|
||||
# Crée un nouvel événement avec les attributs fournis
|
||||
# Retourne 201 Created en cas de succès avec les données de l'événement
|
||||
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||
def create
|
||||
@event = Event.new(event_params)
|
||||
if @event.save
|
||||
render json: @event, status: :created
|
||||
else
|
||||
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /api/v1/events/:id
|
||||
# Met à jour un événement existant avec les attributs fournis
|
||||
# Retourne 200 OK avec les données mises à jour en cas de succès
|
||||
# Retourne 422 Unprocessable Entity avec les messages d'erreur en cas d'échec
|
||||
def update
|
||||
if @event.update(event_params)
|
||||
render json: @event, status: :ok
|
||||
else
|
||||
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /api/v1/events/:id
|
||||
# Supprime définitivement un événement
|
||||
# Retourne 204 No Content en cas de succès
|
||||
def destroy
|
||||
@event.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
# POST /api/v1/events/:id/store_cart
|
||||
# Store cart data in session (AJAX endpoint)
|
||||
def store_cart
|
||||
cart_data = params[:cart] || {}
|
||||
session[:pending_cart] = cart_data
|
||||
session[:event_id] = @event.id
|
||||
|
||||
render json: { status: "success", message: "Cart stored successfully" }
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
Rails.logger.error "Error storing cart: #{error_message}"
|
||||
render json: { status: "error", message: "Failed to store cart" }, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Trouve un événement par son ID ou retourne 404 Introuvable
|
||||
# Utilisé comme before_action pour les actions show, update et destroy
|
||||
def set_event
|
||||
@event = Event.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||
end
|
||||
|
||||
# Paramètres forts pour la création et la mise à jour des événements
|
||||
# Liste blanche des attributs autorisés pour éviter les vulnérabilités de mass assignment
|
||||
def event_params
|
||||
params.require(:event).permit(
|
||||
:name,
|
||||
:description,
|
||||
:state,
|
||||
:venue_name,
|
||||
:venue_address,
|
||||
:latitude,
|
||||
:longitude,
|
||||
:featured
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
279
app/controllers/api/v1/orders_controller.rb
Normal file
279
app/controllers/api/v1/orders_controller.rb
Normal file
@@ -0,0 +1,279 @@
|
||||
# API controller for order management
|
||||
# Provides RESTful endpoints for order operations
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class OrdersController < ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||
before_action :set_event, only: [ :new, :create ]
|
||||
|
||||
# GET /api/v1/orders/new
|
||||
# Returns data needed for new order form
|
||||
def new
|
||||
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if cart_data.empty?
|
||||
render json: { error: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
tickets_needing_names = []
|
||||
cart_data.each do |ticket_type_id, item|
|
||||
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||
next unless ticket_type
|
||||
|
||||
quantity = item["quantity"].to_i
|
||||
next if quantity <= 0
|
||||
|
||||
quantity.times do |i|
|
||||
tickets_needing_names << {
|
||||
ticket_type_id: ticket_type.id,
|
||||
ticket_type_name: ticket_type.name,
|
||||
ticket_type_price: ticket_type.price_cents,
|
||||
index: i
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
render json: { tickets_needing_names: tickets_needing_names }, status: :ok
|
||||
end
|
||||
|
||||
# POST /api/v1/orders
|
||||
# Creates a new order with tickets
|
||||
def create
|
||||
cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if cart_data.empty?
|
||||
render json: { error: "Aucun billet sélectionné" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
success = false
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@order = current_user.orders.create!(event: @event, status: "draft")
|
||||
|
||||
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
|
||||
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
|
||||
|
||||
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
|
||||
|
||||
ticket = @order.tickets.build(
|
||||
ticket_type: ticket_type,
|
||||
first_name: ticket_attrs[:first_name],
|
||||
last_name: ticket_attrs[:last_name],
|
||||
status: "draft"
|
||||
)
|
||||
|
||||
unless ticket.save
|
||||
render json: { error: "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}" }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
if @order.tickets.present?
|
||||
@order.calculate_total!
|
||||
success = true
|
||||
else
|
||||
render json: { error: "Aucun billet valide créé" }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
if success
|
||||
session[:draft_order_id] = @order.id
|
||||
session.delete(:pending_cart)
|
||||
render json: { order: @order, redirect_to: checkout_order_path(@order) }, status: :created
|
||||
end
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
render json: { error: "Une erreur est survenue: #{error_message}" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# GET /api/v1/orders/:id
|
||||
# Returns order summary
|
||||
def show
|
||||
tickets = @order.tickets.includes(:ticket_type)
|
||||
render json: { order: @order, tickets: tickets }, status: :ok
|
||||
end
|
||||
|
||||
# GET /api/v1/orders/:id/checkout
|
||||
# Returns checkout data for an order
|
||||
def checkout
|
||||
if @order.expired?
|
||||
@order.expire_if_overdue!
|
||||
render json: { error: "Votre commande a expiré. Veuillez recommencer." }, status: :gone
|
||||
return
|
||||
end
|
||||
|
||||
tickets = @order.tickets.includes(:ticket_type)
|
||||
total_amount = @order.total_amount_cents
|
||||
expiring_soon = @order.expiring_soon?
|
||||
|
||||
checkout_session = nil
|
||||
if Rails.application.config.stripe[:secret_key].present?
|
||||
begin
|
||||
checkout_session = create_stripe_session
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
Rails.logger.error "Stripe checkout session creation failed: #{error_message}"
|
||||
render json: { error: "Erreur lors de la création de la session de paiement" }, status: :internal_server_error
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
render json: {
|
||||
order: @order,
|
||||
tickets: tickets,
|
||||
total_amount: total_amount,
|
||||
expiring_soon: expiring_soon,
|
||||
checkout_session: checkout_session
|
||||
}, status: :ok
|
||||
end
|
||||
|
||||
# PATCH /api/v1/orders/:id/increment_payment_attempt
|
||||
# Increments payment attempt counter
|
||||
def increment_payment_attempt
|
||||
@order.increment_payment_attempt!
|
||||
render json: { success: true, attempts: @order.payment_attempts }, status: :ok
|
||||
end
|
||||
|
||||
# POST /api/v1/orders/:id/retry_payment
|
||||
# Allows retrying payment for failed orders
|
||||
def retry_payment
|
||||
unless @order.can_retry_payment?
|
||||
render json: { error: "Cette commande ne peut plus être payée" }, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
render json: { redirect_to: checkout_order_path(@order) }, status: :ok
|
||||
end
|
||||
|
||||
# GET /api/v1/orders/payment_success
|
||||
# Handles successful payment confirmation
|
||||
def payment_success
|
||||
session_id = params[:session_id]
|
||||
|
||||
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||
|
||||
unless stripe_configured
|
||||
render json: { error: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur." }, status: :service_unavailable
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||
|
||||
if stripe_session.payment_status == "paid"
|
||||
order_id = stripe_session.metadata["order_id"]
|
||||
|
||||
unless order_id.present?
|
||||
render json: { error: "Informations de commande manquantes" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||
@order.mark_as_paid!
|
||||
|
||||
begin
|
||||
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||
end
|
||||
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
session.delete(:pending_cart)
|
||||
session.delete(:ticket_names)
|
||||
session.delete(:draft_order_id)
|
||||
|
||||
render json: { order: @order, tickets: @order.tickets }, status: :ok
|
||||
else
|
||||
render json: { error: "Le paiement n'a pas été complété avec succès" }, status: :payment_required
|
||||
end
|
||||
rescue Stripe::StripeError => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
render json: { error: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}" }, status: :bad_request
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||
render json: { error: "Une erreur inattendue s'est produite : #{error_message}" }, status: :internal_server_error
|
||||
end
|
||||
end
|
||||
|
||||
# POST /api/v1/orders/payment_cancel
|
||||
# Handles payment cancellation
|
||||
def payment_cancel
|
||||
order_id = params[:order_id] || session[:draft_order_id]
|
||||
|
||||
if order_id.present?
|
||||
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
render json: { message: "Le paiement a été annulé. Vous pouvez réessayer.", redirect_to: checkout_order_path(order) }, status: :ok
|
||||
else
|
||||
session.delete(:draft_order_id)
|
||||
render json: { message: "Le paiement a été annulé et votre commande a expiré." }, status: :gone
|
||||
end
|
||||
else
|
||||
render json: { message: "Le paiement a été annulé" }, status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_order
|
||||
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Commande non trouvée" }, status: :not_found
|
||||
end
|
||||
|
||||
def set_event
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Événement non trouvé" }, status: :not_found
|
||||
end
|
||||
|
||||
def order_params
|
||||
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
||||
end
|
||||
|
||||
def create_stripe_session
|
||||
line_items = @order.tickets.map do |ticket|
|
||||
{
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
product_data: {
|
||||
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||
description: ticket.ticket_type.description
|
||||
},
|
||||
unit_amount: ticket.price_cents
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
end
|
||||
|
||||
Stripe::Checkout::Session.create(
|
||||
payment_method_types: [ "card" ],
|
||||
line_items: line_items,
|
||||
mode: "payment",
|
||||
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: order_payment_cancel_url,
|
||||
metadata: {
|
||||
order_id: @order.id,
|
||||
user_id: current_user.id
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
app/controllers/api_controller.rb
Executable file
26
app/controllers/api_controller.rb
Executable file
@@ -0,0 +1,26 @@
|
||||
# Base controller for API endpoints
|
||||
# Provides authentication and common functionality for API controllers
|
||||
class ApiController < ApplicationController
|
||||
# Disable CSRF protection for API requests (token-based authentication instead)
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
# Authenticate all API requests using API key
|
||||
# Must be called before any API action
|
||||
before_action :authenticate_api_key
|
||||
|
||||
private
|
||||
|
||||
# Authenticates API requests using X-API-Key header or api_key parameter
|
||||
# Returns 401 Unauthorized if key is invalid or missing
|
||||
def authenticate_api_key
|
||||
# Extract API key from header or query parameter
|
||||
api_key = request.headers["X-API-Key"] || params[:api_key]
|
||||
|
||||
# Validate against environment variable for security
|
||||
expected_key = Rails.application.credentials.api_key || ENV["API_KEY"]
|
||||
|
||||
unless expected_key.present? && api_key == expected_key
|
||||
render json: { error: "Unauthorized" }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/controllers/application_controller.rb
Normal file → Executable file
17
app/controllers/application_controller.rb
Normal file → Executable file
@@ -1,4 +1,17 @@
|
||||
# Base controller for the application
|
||||
# Provides common functionality and security configurations for all controllers
|
||||
class ApplicationController < ActionController::Base
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
# Protect against Cross-Site Request Forgery (CSRF) attacks
|
||||
# Ensures that all non-GET requests include a valid authenticity token
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
# Restrict access to modern browsers only
|
||||
# Requires browsers to support modern web standards:
|
||||
# - WebP images for better compression
|
||||
# - Web Push notifications
|
||||
# - Badge API for notifications
|
||||
# - Import maps for JavaScript modules
|
||||
# - CSS nesting and :has() pseudo-class
|
||||
# allow_browser versions: :modern
|
||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||
end
|
||||
|
||||
2
app/controllers/authentications/confirmations_controller.rb → app/controllers/auth/confirmations_controller.rb
Normal file → Executable file
2
app/controllers/authentications/confirmations_controller.rb → app/controllers/auth/confirmations_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::ConfirmationsController < Devise::ConfirmationsController
|
||||
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||
# GET /resource/confirmation/new
|
||||
# def new
|
||||
# super
|
||||
2
app/controllers/authentications/omniauth_callbacks_controller.rb → app/controllers/auth/omniauth_callbacks_controller.rb
Normal file → Executable file
2
app/controllers/authentications/omniauth_callbacks_controller.rb → app/controllers/auth/omniauth_callbacks_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
# You should configure your model like this:
|
||||
# devise :omniauthable, omniauth_providers: [:twitter]
|
||||
|
||||
10
app/controllers/authentications/passwords_controller.rb → app/controllers/auth/passwords_controller.rb
Normal file → Executable file
10
app/controllers/authentications/passwords_controller.rb → app/controllers/auth/passwords_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::PasswordsController < Devise::PasswordsController
|
||||
class Auth::PasswordsController < Devise::PasswordsController
|
||||
# GET /resource/password/new
|
||||
# def new
|
||||
# super
|
||||
@@ -23,9 +23,11 @@ class Authentications::PasswordsController < Devise::PasswordsController
|
||||
|
||||
# protected
|
||||
|
||||
# def after_resetting_password_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
# Override to set a flash message on successful password reset
|
||||
def after_resetting_password_path_for(resource)
|
||||
flash[:notice] = "Votre mot de passe a été changé avec succès. Vous êtes maintenant connecté."
|
||||
super(resource)
|
||||
end
|
||||
|
||||
# The path used after sending reset password instructions
|
||||
# def after_sending_reset_password_instructions_path_for(resource_name)
|
||||
18
app/controllers/authentications/registrations_controller.rb → app/controllers/auth/registrations_controller.rb
Normal file → Executable file
18
app/controllers/authentications/registrations_controller.rb → app/controllers/auth/registrations_controller.rb
Normal file → Executable file
@@ -1,8 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::RegistrationsController < Devise::RegistrationsController
|
||||
# before_action :configure_sign_up_params, only: [:create]
|
||||
# before_action :configure_account_update_params, only: [:update]
|
||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
before_action :configure_sign_up_params, only: [ :create ]
|
||||
before_action :configure_account_update_params, only: [ :update ]
|
||||
|
||||
# GET /resource/sign_up
|
||||
# def new
|
||||
@@ -41,14 +41,14 @@ class Authentications::RegistrationsController < Devise::RegistrationsController
|
||||
# protected
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
# def configure_sign_up_params
|
||||
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
|
||||
# end
|
||||
def configure_sign_up_params
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [ :last_name, :first_name ])
|
||||
end
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
# def configure_account_update_params
|
||||
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
|
||||
# end
|
||||
def configure_account_update_params
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [ :last_name, :first_name ])
|
||||
end
|
||||
|
||||
# The path used after sign up.
|
||||
# def after_sign_up_path_for(resource)
|
||||
9
app/controllers/authentications/sessions_controller.rb → app/controllers/auth/sessions_controller.rb
Normal file → Executable file
9
app/controllers/authentications/sessions_controller.rb → app/controllers/auth/sessions_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::SessionsController < Devise::SessionsController
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
# before_action :configure_sign_in_params, only: [:create]
|
||||
|
||||
# GET /resource/sign_in
|
||||
@@ -9,9 +9,10 @@ class Authentications::SessionsController < Devise::SessionsController
|
||||
# end
|
||||
|
||||
# POST /resource/sign_in
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
def create
|
||||
super
|
||||
flash[:notice] = "Connexion réussie !" if resource.persisted?
|
||||
end
|
||||
|
||||
# DELETE /resource/sign_out
|
||||
# def destroy
|
||||
2
app/controllers/authentications/unlocks_controller.rb → app/controllers/auth/unlocks_controller.rb
Normal file → Executable file
2
app/controllers/authentications/unlocks_controller.rb → app/controllers/auth/unlocks_controller.rb
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::UnlocksController < Devise::UnlocksController
|
||||
class Auth::UnlocksController < Devise::UnlocksController
|
||||
# GET /resource/unlock/new
|
||||
# def new
|
||||
# super
|
||||
0
app/controllers/concerns/.keep
Normal file → Executable file
0
app/controllers/concerns/.keep
Normal file → Executable file
18
app/controllers/concerns/stripe_concern.rb
Normal file
18
app/controllers/concerns/stripe_concern.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module StripeConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Check if Stripe is properly configured
|
||||
def stripe_configured?
|
||||
Rails.application.config.stripe[:secret_key].present?
|
||||
end
|
||||
|
||||
# Stripe is now initialized at application startup, so this method is no longer needed
|
||||
# but kept for backward compatibility
|
||||
def initialize_stripe
|
||||
return false unless stripe_configured?
|
||||
|
||||
# Stripe is already initialized at application startup
|
||||
Rails.logger.debug "Stripe already initialized at application startup"
|
||||
true
|
||||
end
|
||||
end
|
||||
36
app/controllers/events_controller.rb
Executable file
36
app/controllers/events_controller.rb
Executable file
@@ -0,0 +1,36 @@
|
||||
# Events controller - Public event listings and individual event display
|
||||
#
|
||||
# This controller manages public event browsing and displays individual events
|
||||
# with their associated ticket types. No authentication required for public browsing.
|
||||
class EventsController < ApplicationController
|
||||
# No authentication required for public event viewing
|
||||
before_action :authenticate_user!, only: []
|
||||
before_action :set_event, only: [ :show ]
|
||||
|
||||
# Display paginated list of upcoming published events
|
||||
#
|
||||
# Shows events in published state, ordered by start time ascending
|
||||
# Includes event owner information and supports Kaminari pagination
|
||||
def index
|
||||
@events = Event.includes(:user).upcoming.page(params[:page]).per(12)
|
||||
end
|
||||
|
||||
# Display individual event with ticket type information
|
||||
#
|
||||
# Shows complete event details including venue information,
|
||||
# available ticket types, and allows users to add tickets to cart
|
||||
def show
|
||||
# Event is set by set_event callback with ticket types preloaded
|
||||
# Template will display event details and ticket selection interface
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Find and set the current event with eager-loaded associations
|
||||
#
|
||||
# Loads event with ticket types to avoid N+1 queries
|
||||
# Raises ActiveRecord::RecordNotFound if event doesn't exist
|
||||
def set_event
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
end
|
||||
end
|
||||
284
app/controllers/orders_controller.rb
Normal file
284
app/controllers/orders_controller.rb
Normal file
@@ -0,0 +1,284 @@
|
||||
# Handle order management and checkout process
|
||||
#
|
||||
# This controller manages the order lifecycle from checkout to payment completion
|
||||
# Orders group multiple tickets together for better transaction management
|
||||
class OrdersController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||
before_action :set_event, only: [ :new, :create ]
|
||||
|
||||
# Display new order form with name collection
|
||||
#
|
||||
# On this page user can see order summary and complete the tickets details
|
||||
# (first name and last name) for each ticket ordered
|
||||
def new
|
||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if @cart_data.empty?
|
||||
redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
||||
return
|
||||
end
|
||||
|
||||
# Build list of tickets requiring names
|
||||
@tickets_needing_names = []
|
||||
@cart_data.each do |ticket_type_id, item|
|
||||
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
|
||||
next unless ticket_type
|
||||
|
||||
quantity = item["quantity"].to_i
|
||||
next if quantity <= 0
|
||||
|
||||
quantity.times do |i|
|
||||
@tickets_needing_names << {
|
||||
ticket_type_id: ticket_type.id,
|
||||
ticket_type_name: ticket_type.name,
|
||||
ticket_type_price: ticket_type.price_cents,
|
||||
index: i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new order with tickets
|
||||
#
|
||||
# Here a new order is created with associated tickets in draft state.
|
||||
# When user is ready they can proceed to payment via the order checkout
|
||||
def create
|
||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if @cart_data.empty?
|
||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet sélectionné"
|
||||
return
|
||||
end
|
||||
|
||||
success = false
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@order = current_user.orders.create!(event: @event, status: "draft")
|
||||
|
||||
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
|
||||
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
|
||||
|
||||
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
|
||||
|
||||
ticket = @order.tickets.build(
|
||||
ticket_type: ticket_type,
|
||||
first_name: ticket_attrs[:first_name],
|
||||
last_name: ticket_attrs[:last_name],
|
||||
status: "draft"
|
||||
)
|
||||
|
||||
unless ticket.save
|
||||
flash[:alert] = "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}"
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
if @order.tickets.present?
|
||||
@order.calculate_total!
|
||||
success = true
|
||||
else
|
||||
flash[:alert] = "Aucun billet valide créé"
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
# Handle redirects outside transaction
|
||||
if success
|
||||
session[:draft_order_id] = @order.id
|
||||
session.delete(:pending_cart)
|
||||
redirect_to checkout_order_path(@order)
|
||||
else
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
end
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
flash[:alert] = "Une erreur est survenue: #{error_message}"
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
end
|
||||
|
||||
# Display order summary
|
||||
#
|
||||
#
|
||||
def show
|
||||
@tickets = @order.tickets.includes(:ticket_type)
|
||||
end
|
||||
|
||||
# Display payment page for an order
|
||||
#
|
||||
# Display a summary of all tickets in the order and permit user
|
||||
# to proceed to payment via Stripe
|
||||
def checkout
|
||||
# Handle expired orders
|
||||
if @order.expired?
|
||||
@order.expire_if_overdue!
|
||||
return redirect_to event_path(@order.event.slug, @order.event),
|
||||
alert: "Votre commande a expiré. Veuillez recommencer."
|
||||
end
|
||||
|
||||
@tickets = @order.tickets.includes(:ticket_type)
|
||||
@total_amount = @order.total_amount_cents
|
||||
@expiring_soon = @order.expiring_soon?
|
||||
|
||||
# Create Stripe checkout session if Stripe is configured
|
||||
if Rails.application.config.stripe[:secret_key].present?
|
||||
begin
|
||||
@checkout_session = create_stripe_session
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
Rails.logger.error "Stripe checkout session creation failed: #{error_message}"
|
||||
flash[:alert] = "Erreur lors de la création de la session de paiement"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Increment payment attempt - called via AJAX when user clicks pay button
|
||||
def increment_payment_attempt
|
||||
@order.increment_payment_attempt!
|
||||
render json: { success: true, attempts: @order.payment_attempts }
|
||||
end
|
||||
|
||||
# Allow users to retry payment for failed/cancelled payments
|
||||
def retry_payment
|
||||
unless @order.can_retry_payment?
|
||||
redirect_to event_path(@order.event.slug, @order.event),
|
||||
alert: "Cette commande ne peut plus être payée"
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to checkout_order_path(@order)
|
||||
end
|
||||
|
||||
# Handle successful payment
|
||||
def payment_success
|
||||
session_id = params[:session_id]
|
||||
|
||||
# Check if Stripe is properly configured
|
||||
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||
|
||||
unless stripe_configured
|
||||
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||
|
||||
if stripe_session.payment_status == "paid"
|
||||
# Get order_id from session metadata
|
||||
order_id = stripe_session.metadata["order_id"]
|
||||
|
||||
unless order_id.present?
|
||||
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
||||
return
|
||||
end
|
||||
|
||||
# Find and update the order
|
||||
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Schedule Stripe invoice generation in background
|
||||
# This creates accounting records without blocking the payment success flow
|
||||
begin
|
||||
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||
# Don't fail the payment process due to job scheduling issues
|
||||
end
|
||||
|
||||
# Send confirmation emails
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
# Don't fail the entire payment process due to email/PDF generation issues
|
||||
end
|
||||
end
|
||||
|
||||
# Clear session data
|
||||
session.delete(:pending_cart)
|
||||
session.delete(:ticket_names)
|
||||
session.delete(:draft_order_id)
|
||||
|
||||
render "payment_success"
|
||||
else
|
||||
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
||||
end
|
||||
rescue Stripe::StripeError => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Handle payment failure/cancellation
|
||||
def payment_cancel
|
||||
order_id = params[:order_id] || session[:draft_order_id]
|
||||
|
||||
if order_id.present?
|
||||
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
redirect_to checkout_order_path(order),
|
||||
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
||||
else
|
||||
session.delete(:draft_order_id)
|
||||
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
||||
end
|
||||
else
|
||||
redirect_to root_path, alert: "Le paiement a été annulé"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_order
|
||||
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path, alert: "Commande non trouvée"
|
||||
end
|
||||
|
||||
def set_event
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
end
|
||||
|
||||
def order_params
|
||||
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
||||
end
|
||||
|
||||
def create_stripe_session
|
||||
line_items = @order.tickets.map do |ticket|
|
||||
{
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
product_data: {
|
||||
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||
description: ticket.ticket_type.description
|
||||
},
|
||||
unit_amount: ticket.price_cents
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
end
|
||||
|
||||
Stripe::Checkout::Session.create(
|
||||
payment_method_types: [ "card" ],
|
||||
line_items: line_items,
|
||||
mode: "payment",
|
||||
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: order_payment_cancel_url,
|
||||
metadata: {
|
||||
order_id: @order.id,
|
||||
user_id: current_user.id
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
47
app/controllers/pages_controller.rb
Normal file → Executable file
47
app/controllers/pages_controller.rb
Normal file → Executable file
@@ -1,9 +1,50 @@
|
||||
# Controller for static pages and user dashboard
|
||||
# Handles basic page rendering and user-specific content
|
||||
class PagesController < ApplicationController
|
||||
# Display homepage
|
||||
before_action :authenticate_user!, only: [ :dashboard ]
|
||||
|
||||
# Homepage showing featured events
|
||||
#
|
||||
# Display homepage with featured events and incoming ones
|
||||
def home
|
||||
@featured_events = Event.published.featured.limit(3)
|
||||
|
||||
if user_signed_in?
|
||||
redirect_to(dashboard_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Display legal page
|
||||
def legals
|
||||
# User dashboard showing personalized content
|
||||
# Accessible only to authenticated users
|
||||
def dashboard
|
||||
# Metrics for dashboard cards
|
||||
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||
.where(events: { state: :published })
|
||||
.where(orders: { status: [ "paid", "completed" ] })
|
||||
.sum("1")
|
||||
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
||||
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
||||
@upcoming_events = Event.published.upcoming.count
|
||||
|
||||
# User's booked events
|
||||
@user_booked_events = Event.joins(ticket_types: { tickets: :order })
|
||||
.where(orders: { user: current_user }, tickets: { status: "active" })
|
||||
.distinct
|
||||
.limit(5)
|
||||
|
||||
# Draft orders that can be retried
|
||||
@draft_orders = current_user.orders.includes(tickets: [ :ticket_type, :event ])
|
||||
.can_retry_payment
|
||||
.order(:expires_at)
|
||||
|
||||
# Events sections
|
||||
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [ Date.current, Date.current + 1 ]).order(start_time: :asc).page(params[:page])
|
||||
end
|
||||
|
||||
# Events page showing all published events with pagination
|
||||
def events
|
||||
@events = Event.published.order(created_at: :desc).page(params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
117
app/controllers/promoter/events_controller.rb
Normal file
117
app/controllers/promoter/events_controller.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
# Promoter Events Controller
|
||||
#
|
||||
# Handles event management for promoters (event organizers)
|
||||
# Allows promoters to create, edit, delete and manage their events
|
||||
class Promoter::EventsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_can_manage_events!
|
||||
before_action :set_event, only: [ :show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out ]
|
||||
|
||||
# Display all events for the current promoter
|
||||
def index
|
||||
@events = current_user.events.order(created_at: :desc).page(params[:page]).per(10)
|
||||
end
|
||||
|
||||
# Display a specific event for the promoter
|
||||
def show
|
||||
# Event is set by set_event callback
|
||||
end
|
||||
|
||||
# Show form to create a new event
|
||||
def new
|
||||
@event = current_user.events.build
|
||||
end
|
||||
|
||||
# Create a new event
|
||||
def create
|
||||
@event = current_user.events.build(event_params)
|
||||
|
||||
if @event.save
|
||||
redirect_to promoter_event_path(@event), notice: "Event créé avec succès!"
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Show form to edit an existing event
|
||||
def edit
|
||||
# Event is set by set_event callback
|
||||
end
|
||||
|
||||
# Update an existing event
|
||||
def update
|
||||
if @event.update(event_params)
|
||||
redirect_to promoter_event_path(@event), notice: "Event mis à jour avec succès!"
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Delete an event
|
||||
def destroy
|
||||
@event.destroy
|
||||
redirect_to promoter_events_path, notice: "Event supprimé avec succès!"
|
||||
end
|
||||
|
||||
# Publish an event (make it visible to public)
|
||||
def publish
|
||||
if @event.draft?
|
||||
@event.update(state: :published)
|
||||
redirect_to promoter_event_path(@event), notice: "Event publié avec succès!"
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être publié."
|
||||
end
|
||||
end
|
||||
|
||||
# Unpublish an event (make it draft)
|
||||
def unpublish
|
||||
if @event.published?
|
||||
@event.update(state: :draft)
|
||||
redirect_to promoter_event_path(@event), notice: "Event dépublié avec succès!"
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être dépublié."
|
||||
end
|
||||
end
|
||||
|
||||
# Cancel an event
|
||||
def cancel
|
||||
if @event.published?
|
||||
@event.update(state: :canceled)
|
||||
redirect_to promoter_event_path(@event), notice: "Event annulé avec succès!"
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être annulé."
|
||||
end
|
||||
end
|
||||
|
||||
# Mark event as sold out
|
||||
def mark_sold_out
|
||||
if @event.published?
|
||||
@event.update(state: :sold_out)
|
||||
redirect_to promoter_event_path(@event), notice: "Event marqué comme complet!"
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être marqué comme complet."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_can_manage_events!
|
||||
unless current_user.can_manage_events?
|
||||
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||
end
|
||||
end
|
||||
|
||||
def set_event
|
||||
@event = current_user.events.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||
end
|
||||
|
||||
def event_params
|
||||
params.require(:event).permit(
|
||||
:name, :slug, :description, :image,
|
||||
:venue_name, :venue_address, :latitude, :longitude,
|
||||
:start_time, :end_time, :featured
|
||||
)
|
||||
end
|
||||
end
|
||||
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
# Promoter Ticket Types Controller
|
||||
#
|
||||
# Handles ticket type (bundle) management for promoters
|
||||
# Allows promoters to create, edit, delete and manage ticket types for their events
|
||||
class Promoter::TicketTypesController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_can_manage_events!
|
||||
before_action :set_event
|
||||
before_action :set_ticket_type, only: [ :show, :edit, :update, :destroy ]
|
||||
|
||||
# Display all ticket types for an event
|
||||
def index
|
||||
@ticket_types = @event.ticket_types.order(:created_at)
|
||||
end
|
||||
|
||||
# Display a specific ticket type
|
||||
def show
|
||||
# Ticket type is set by set_ticket_type callback
|
||||
end
|
||||
|
||||
# Show form to create a new ticket type
|
||||
def new
|
||||
@ticket_type = @event.ticket_types.build
|
||||
# Set default values
|
||||
@ticket_type.sale_start_at = Time.current
|
||||
@ticket_type.sale_end_at = @event.start_time || 1.week.from_now
|
||||
@ticket_type.requires_id = false
|
||||
end
|
||||
|
||||
# Create a new ticket type
|
||||
def create
|
||||
@ticket_type = @event.ticket_types.build(ticket_type_params)
|
||||
|
||||
if @ticket_type.save
|
||||
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet créé avec succès!"
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Show form to edit an existing ticket type
|
||||
def edit
|
||||
# Ticket type is set by set_ticket_type callback
|
||||
end
|
||||
|
||||
# Update an existing ticket type
|
||||
def update
|
||||
if @ticket_type.update(ticket_type_params)
|
||||
redirect_to promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet mis à jour avec succès!"
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Delete a ticket type
|
||||
def destroy
|
||||
if @ticket_type.tickets.any?
|
||||
redirect_to promoter_event_ticket_types_path(@event), alert: "Impossible de supprimer ce type de billet car des billets ont déjà été vendus."
|
||||
else
|
||||
@ticket_type.destroy
|
||||
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet supprimé avec succès!"
|
||||
end
|
||||
end
|
||||
|
||||
# Duplicate an existing ticket type
|
||||
def duplicate
|
||||
original = @event.ticket_types.find(params[:id])
|
||||
@ticket_type = original.dup
|
||||
@ticket_type.name = "#{original.name} (Copie)"
|
||||
|
||||
if @ticket_type.save
|
||||
redirect_to edit_promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet dupliqué avec succès!"
|
||||
else
|
||||
redirect_to promoter_event_ticket_types_path(@event), alert: "Erreur lors de la duplication."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_can_manage_events!
|
||||
unless current_user.can_manage_events?
|
||||
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||
end
|
||||
end
|
||||
|
||||
def set_event
|
||||
@event = current_user.events.find(params[:event_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||
end
|
||||
|
||||
def set_ticket_type
|
||||
@ticket_type = @event.ticket_types.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to promoter_event_ticket_types_path(@event), alert: "Type de billet non trouvé."
|
||||
end
|
||||
|
||||
def ticket_type_params
|
||||
params.require(:ticket_type).permit(
|
||||
:name, :description, :price_euros, :quantity,
|
||||
:sale_start_at, :sale_end_at, :minimum_age, :requires_id
|
||||
)
|
||||
end
|
||||
end
|
||||
198
app/controllers/tickets_controller.rb
Normal file
198
app/controllers/tickets_controller.rb
Normal file
@@ -0,0 +1,198 @@
|
||||
# Legacy tickets controller - redirects to new order system
|
||||
#
|
||||
# This controller now primarily handles legacy redirects and backward compatibility
|
||||
# Most ticket creation functionality has been moved to OrdersController
|
||||
class TicketsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :ticket_view, :download_ticket ]
|
||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
||||
|
||||
# Redirect to order-based checkout
|
||||
def checkout
|
||||
# Check for draft order
|
||||
if session[:draft_order_id].present?
|
||||
order = current_user.orders.find_by(id: session[:draft_order_id], status: "draft")
|
||||
if order.present?
|
||||
redirect_to order_checkout_path(order)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# No order found
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet en attente de paiement"
|
||||
end
|
||||
|
||||
# Redirect to order-based payment success
|
||||
def payment_success
|
||||
redirect_to order_payment_success_path(session_id: params[:session_id])
|
||||
end
|
||||
|
||||
# Redirect to order-based payment cancel
|
||||
def payment_cancel
|
||||
redirect_to order_payment_cancel_path
|
||||
end
|
||||
|
||||
# Redirect retry payment to order system
|
||||
def retry_payment
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
|
||||
# Look for draft order for this event
|
||||
order = current_user.orders.find_by(event: @event, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
redirect_to retry_payment_order_path(order)
|
||||
else
|
||||
redirect_to event_path(@event.slug, @event),
|
||||
alert: "Aucune commande disponible pour un nouveau paiement"
|
||||
end
|
||||
end
|
||||
|
||||
# Display ticket details
|
||||
def show
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
@event = @ticket.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
|
||||
# Display ticket in PDF-like format
|
||||
def ticket_view
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
@event = @ticket.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
|
||||
# Download PDF ticket - only accessible by ticket owner
|
||||
# User must be authenticated to download ticket
|
||||
# TODO: change ID to an unique identifier (UUID)
|
||||
def download_ticket
|
||||
# Find ticket and ensure it belongs to current user
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
# Generate PDF using Grover
|
||||
begin
|
||||
Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}"
|
||||
|
||||
# Render the HTML template
|
||||
html = render_to_string(
|
||||
partial: "tickets/pdf_ticket",
|
||||
layout: false,
|
||||
locals: { ticket: @ticket }
|
||||
)
|
||||
|
||||
Rails.logger.info "HTML template rendered successfully, length: #{html.length}"
|
||||
|
||||
# Try to load and use Grover
|
||||
begin
|
||||
Rails.logger.info "Attempting to load Grover gem"
|
||||
|
||||
# Try different approaches to load grover
|
||||
begin
|
||||
require "bundler"
|
||||
Bundler.require(:default, Rails.env)
|
||||
Rails.logger.info "Bundler required gems successfully"
|
||||
rescue => bundler_error
|
||||
Rails.logger.warn "Bundler require failed: #{bundler_error.message}"
|
||||
end
|
||||
|
||||
# Direct path approach using bundle show
|
||||
grover_gem_path = `bundle show grover`.strip
|
||||
grover_path = File.join(grover_gem_path, "lib", "grover")
|
||||
|
||||
if File.exist?(grover_path + ".rb")
|
||||
Rails.logger.info "Loading Grover from direct path: #{grover_path}"
|
||||
require grover_path
|
||||
else
|
||||
Rails.logger.error "Grover not found at path: #{grover_path}"
|
||||
raise LoadError, "Grover gem not available at expected path"
|
||||
end
|
||||
|
||||
Rails.logger.info "Creating Grover instance with options"
|
||||
grover = Grover.new(html,
|
||||
format: "A6",
|
||||
margin: {
|
||||
top: "10mm",
|
||||
bottom: "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm"
|
||||
},
|
||||
prefer_css_page_size: true,
|
||||
emulate_media: "print",
|
||||
cache: false,
|
||||
launch_args: [ "--no-sandbox", "--disable-setuid-sandbox" ] # For better compatibility
|
||||
)
|
||||
Rails.logger.info "Grover instance created successfully"
|
||||
|
||||
pdf_content = grover.to_pdf
|
||||
Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}"
|
||||
|
||||
# Send PDF as download
|
||||
send_data pdf_content,
|
||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
|
||||
type: "application/pdf",
|
||||
disposition: "attachment"
|
||||
rescue LoadError => grover_error
|
||||
Rails.logger.error "Failed to load Grover: #{grover_error.message}"
|
||||
# Fallback: return HTML instead of PDF
|
||||
send_data html,
|
||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.html",
|
||||
type: "text/html",
|
||||
disposition: "attachment"
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Error generating ticket PDF with Grover:"
|
||||
Rails.logger.error "Message: #{e.message}"
|
||||
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error "ActiveRecord::RecordNotFound error: #{e.message}"
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
rescue => e
|
||||
Rails.logger.error "Unexpected error in download_ticket action:"
|
||||
Rails.logger.error "Message: #{e.message}"
|
||||
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_event
|
||||
event_id = params[:id] || session[:event_id]
|
||||
|
||||
Rails.logger.debug "TicketsController#set_event - params[:id]: #{params[:id].inspect}, session[:event_id]: #{session[:event_id].inspect}"
|
||||
|
||||
unless event_id
|
||||
Rails.logger.error "TicketsController#set_event - No event ID found"
|
||||
redirect_to events_path, alert: "Aucun événement spécifié"
|
||||
return
|
||||
end
|
||||
|
||||
@event = Event.includes(:ticket_types).find(event_id)
|
||||
Rails.logger.debug "TicketsController#set_event - Found event: #{@event.id} - #{@event.name}"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
Rails.logger.error "TicketsController#set_event - Event not found with ID: #{event_id}"
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
end
|
||||
end
|
||||
10
app/helpers/application_helper.rb
Normal file → Executable file
10
app/helpers/application_helper.rb
Normal file → Executable file
@@ -1,2 +1,12 @@
|
||||
module ApplicationHelper
|
||||
# Convert price from cents to float
|
||||
def format_price(cents)
|
||||
(cents.to_f / 100).round(2)
|
||||
end
|
||||
|
||||
# Include flash message helpers
|
||||
include FlashMessagesHelper
|
||||
|
||||
# Include Stripe helper
|
||||
include StripeHelper
|
||||
end
|
||||
|
||||
51
app/helpers/flash_messages_helper.rb
Executable file
51
app/helpers/flash_messages_helper.rb
Executable file
@@ -0,0 +1,51 @@
|
||||
# Flash messages helper for consistent styling across the application
|
||||
#
|
||||
# Provides standardized CSS classes and icons for different types of flash messages
|
||||
# using Tailwind CSS classes and Lucide icons for consistent UI presentation
|
||||
module FlashMessagesHelper
|
||||
# Return appropriate Tailwind CSS classes for different flash message types
|
||||
#
|
||||
# @param type [String, Symbol] The flash message type (notice, error, warning, info)
|
||||
# @return [String] Tailwind CSS classes for styling the flash message container
|
||||
#
|
||||
# Examples:
|
||||
# flash_class('success') # => "bg-green-50 text-green-800 border-green-200"
|
||||
# flash_class('error') # => "bg-red-50 text-red-800 border-red-200"
|
||||
def flash_class(type)
|
||||
case type.to_s
|
||||
when "notice", "success"
|
||||
"bg-green-50 text-green-800 border-green-200"
|
||||
when "error", "alert"
|
||||
"bg-red-50 text-red-800 border-red-200"
|
||||
when "warning"
|
||||
"bg-yellow-50 text-yellow-800 border-yellow-200"
|
||||
when "info"
|
||||
"bg-blue-50 text-blue-800 border-blue-200"
|
||||
else
|
||||
"bg-gray-50 text-gray-800 border-gray-200"
|
||||
end
|
||||
end
|
||||
|
||||
# Return appropriate Lucide icon for different flash message types
|
||||
#
|
||||
# @param type [String, Symbol] The flash message type
|
||||
# @return [String] HTML content tag with Lucide icon data attribute
|
||||
#
|
||||
# Examples:
|
||||
# flash_icon('success') # => <i data-lucide="check-circle" class="..."></i>
|
||||
# flash_icon('error') # => <i data-lucide="x-circle" class="..."></i>
|
||||
def flash_icon(type)
|
||||
case type.to_s
|
||||
when "notice", "success"
|
||||
content_tag :i, "", "data-lucide": "check-circle", class: "w-5 h-5 flex-shrink-0"
|
||||
when "error", "alert"
|
||||
content_tag :i, "", "data-lucide": "x-circle", class: "w-5 h-5 flex-shrink-0"
|
||||
when "warning"
|
||||
content_tag :i, "", "data-lucide": "alert-triangle", class: "w-5 h-5 flex-shrink-0"
|
||||
when "info"
|
||||
content_tag :i, "", "data-lucide": "info", class: "w-5 h-5 flex-shrink-0"
|
||||
else
|
||||
content_tag :i, "", "data-lucide": "bell", class: "w-5 h-5 flex-shrink-0"
|
||||
end
|
||||
end
|
||||
end
|
||||
82
app/helpers/lucide_helper.rb
Normal file
82
app/helpers/lucide_helper.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
module LucideHelper
|
||||
# Create a Lucide icon element
|
||||
#
|
||||
# @param name [String] The name of the Lucide icon
|
||||
# @param options [Hash] Additional options
|
||||
# @option options [String] :class Additional CSS classes
|
||||
# @option options [String] :size Size class (e.g., 'w-4 h-4', 'w-6 h-6')
|
||||
# @option options [Hash] :data Additional data attributes
|
||||
#
|
||||
# @return [String] HTML string for the icon
|
||||
#
|
||||
# Usage:
|
||||
# lucide_icon('user')
|
||||
# lucide_icon('check-circle', class: 'text-green-500', size: 'w-5 h-5')
|
||||
# lucide_icon('menu', data: { action: 'click->header#toggleMenu' })
|
||||
def lucide_icon(name, options = {})
|
||||
css_classes = [ "lucide-icon" ]
|
||||
css_classes << options[:size] if options[:size]
|
||||
css_classes << options[:class] if options[:class]
|
||||
|
||||
data_attributes = { lucide: name }
|
||||
data_attributes.merge!(options[:data]) if options[:data]
|
||||
|
||||
content_tag :i, "",
|
||||
class: css_classes.join(" "),
|
||||
data: data_attributes,
|
||||
**options.except(:class, :size, :data)
|
||||
end
|
||||
|
||||
# Create a button with a Lucide icon
|
||||
#
|
||||
# @param name [String] The name of the Lucide icon
|
||||
# @param options [Hash] Button options
|
||||
# @option options [String] :text Button text (optional)
|
||||
# @option options [String] :class Additional CSS classes for button
|
||||
# @option options [String] :icon_class Additional CSS classes for icon
|
||||
# @option options [String] :icon_size Size class for icon
|
||||
#
|
||||
# Usage:
|
||||
# lucide_button('plus', text: 'Add Item', class: 'btn btn-primary')
|
||||
# lucide_button('trash-2', class: 'btn-danger', data: { confirm: 'Are you sure?' })
|
||||
def lucide_button(name, options = {})
|
||||
text = options.delete(:text)
|
||||
icon_class = options.delete(:icon_class)
|
||||
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||
|
||||
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||
|
||||
content = if text.present?
|
||||
safe_join([ icon, " ", text ])
|
||||
else
|
||||
icon
|
||||
end
|
||||
|
||||
content_tag :button, content, options
|
||||
end
|
||||
|
||||
# Create a link with a Lucide icon
|
||||
#
|
||||
# @param name [String] The name of the Lucide icon
|
||||
# @param url [String] The URL for the link
|
||||
# @param options [Hash] Link options
|
||||
#
|
||||
# Usage:
|
||||
# lucide_link('edit', edit_user_path(user), text: 'Edit')
|
||||
# lucide_link('external-link', 'https://example.com', text: 'Visit', target: '_blank')
|
||||
def lucide_link(name, url, options = {})
|
||||
text = options.delete(:text)
|
||||
icon_class = options.delete(:icon_class)
|
||||
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||
|
||||
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||
|
||||
content = if text.present?
|
||||
safe_join([ icon, " ", text ])
|
||||
else
|
||||
icon
|
||||
end
|
||||
|
||||
link_to content, url, options
|
||||
end
|
||||
end
|
||||
0
app/helpers/pages_helper.rb
Normal file → Executable file
0
app/helpers/pages_helper.rb
Normal file → Executable file
17
app/helpers/stripe_helper.rb
Normal file
17
app/helpers/stripe_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module StripeHelper
|
||||
# Safely call Stripe methods with error handling
|
||||
def safe_stripe_call(&block)
|
||||
# Check if Stripe is properly configured
|
||||
return nil unless Rails.application.config.stripe[:secret_key].present?
|
||||
|
||||
# Stripe is now initialized at application startup
|
||||
Rails.logger.debug "Using globally initialized Stripe"
|
||||
|
||||
begin
|
||||
yield if block_given?
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.error "Stripe Error: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
2
app/helpers/tickets_helper.rb
Normal file
2
app/helpers/tickets_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module TicketsHelper
|
||||
end
|
||||
24
app/javascript/application.js
Normal file → Executable file
24
app/javascript/application.js
Normal file → Executable file
@@ -1,3 +1,23 @@
|
||||
// Entry point for the build script in your package.json
|
||||
import "@hotwired/turbo-rails"
|
||||
import "./controllers"
|
||||
// This file initializes the Rails application with Turbo and Stimulus controllers
|
||||
|
||||
// Import Turbo Rails for SPA-like navigation
|
||||
import "@hotwired/turbo-rails";
|
||||
|
||||
// Import all Stimulus controllers
|
||||
import "./controllers";
|
||||
|
||||
// Import and initialize Lucide icons globally
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
// Initialize icons globally
|
||||
function initializeLucideIcons() {
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
// Run on initial page load
|
||||
document.addEventListener('DOMContentLoaded', initializeLucideIcons);
|
||||
|
||||
// Run on Turbo navigation (Rails 7+ SPA behavior)
|
||||
document.addEventListener('turbo:render', initializeLucideIcons);
|
||||
document.addEventListener('turbo:frame-render', initializeLucideIcons);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
21
app/javascript/controllers/application.js
Normal file → Executable file
21
app/javascript/controllers/application.js
Normal file → Executable file
@@ -1,9 +1,20 @@
|
||||
import { Application } from "@hotwired/stimulus"
|
||||
// Main Stimulus application controller
|
||||
// Initializes the Stimulus framework and makes it available globally
|
||||
import { Application } from "@hotwired/stimulus";
|
||||
|
||||
const application = Application.start()
|
||||
// Create and start the Stimulus application
|
||||
const application = Application.start();
|
||||
|
||||
// Configure Stimulus development experience
|
||||
application.debug = false
|
||||
window.Stimulus = application
|
||||
// Set to false in production to avoid unnecessary logging
|
||||
application.debug = false;
|
||||
|
||||
export { application }
|
||||
// Make Stimulus globally available for debugging purposes
|
||||
window.Stimulus = application;
|
||||
|
||||
// Configure Alpine js (commented out as it's not currently used)
|
||||
// import Alpine from "alpinejs";
|
||||
// window.Alpine = Alpine;
|
||||
// Alpine.start();
|
||||
|
||||
export { application };
|
||||
|
||||
58
app/javascript/controllers/counter_controller.js
Normal file → Executable file
58
app/javascript/controllers/counter_controller.js
Normal file → Executable file
@@ -1,61 +1,85 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Counter controller for animating number increments
|
||||
// Used for statistics and numerical displays that animate when they come into view
|
||||
export default class extends Controller {
|
||||
// Define controller values with defaults
|
||||
static values = {
|
||||
target: Number,
|
||||
decimal: Boolean,
|
||||
duration: { type: Number, default: 2000 }
|
||||
target: { type: Number, default: 0 }, // Target number to count to
|
||||
decimal: { type: Boolean, default: false }, // Whether to display decimal values
|
||||
duration: { type: Number, default: 2000 } // Animation duration in milliseconds
|
||||
}
|
||||
|
||||
// Set up the intersection observer when the controller connects
|
||||
connect() {
|
||||
// Create an intersection observer to trigger animation when element is visible
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
// Start animation when element is 50% visible
|
||||
if (entry.isIntersecting) {
|
||||
this.animate()
|
||||
// Stop observing after animation starts
|
||||
this.observer.unobserve(this.element)
|
||||
}
|
||||
})
|
||||
}, { threshold: 0.5 })
|
||||
|
||||
// Begin observing this element
|
||||
this.observer.observe(this.element)
|
||||
}
|
||||
|
||||
// Clean up the observer when the controller disconnects
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Animate the counter from 0 to the target value
|
||||
animate() {
|
||||
const startValue = 0
|
||||
const startTime = performance.now()
|
||||
// Find the target element with data-target-value
|
||||
const targetElement = this.element.querySelector('.stat-number');
|
||||
if (!targetElement) return;
|
||||
|
||||
// Get the target value
|
||||
this.targetValue = parseInt(targetElement.getAttribute('data-target-value'), 10) || this.targetValue;
|
||||
|
||||
const startValue = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Update counter function using requestAnimationFrame for smooth animation
|
||||
const updateCounter = (currentTime) => {
|
||||
const elapsedTime = currentTime - startTime
|
||||
const progress = Math.min(elapsedTime / this.durationValue, 1)
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const progress = Math.min(elapsedTime / this.durationValue, 1);
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
||||
// Easing function for smooth animation (ease-out quartic)
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
|
||||
|
||||
// Format value based on decimal setting
|
||||
if (this.decimalValue && this.targetValue < 10) {
|
||||
currentValue = currentValue.toFixed(1)
|
||||
currentValue = currentValue.toFixed(1);
|
||||
} else {
|
||||
currentValue = Math.floor(currentValue)
|
||||
currentValue = Math.floor(currentValue);
|
||||
}
|
||||
|
||||
this.element.textContent = currentValue
|
||||
// Update only the text content of the target element
|
||||
targetElement.textContent = currentValue;
|
||||
|
||||
// Continue animation until complete
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter)
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
this.element.textContent = this.decimalValue && this.targetValue < 10
|
||||
// Ensure final value is exactly the target
|
||||
const finalValue = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue
|
||||
: this.targetValue;
|
||||
targetElement.textContent = finalValue;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter)
|
||||
// Start the animation
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
46
app/javascript/controllers/flash_message_controller.js
Executable file
46
app/javascript/controllers/flash_message_controller.js
Executable file
@@ -0,0 +1,46 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Controller for handling flash messages
|
||||
// Automatically dismisses messages after a timeout and handles manual closing
|
||||
export default class extends Controller {
|
||||
// Define targets for the controller
|
||||
static targets = ["message"];
|
||||
|
||||
// Initialize the controller when it connects to the DOM
|
||||
connect() {
|
||||
// console.log("FlashMessageController mounted", this.element);
|
||||
console.log("FlashMessageController mounted");
|
||||
|
||||
// Initialize Lucide icons for this element if available
|
||||
if (typeof lucide !== "undefined") {
|
||||
lucide.createIcons({ within: this.element });
|
||||
}
|
||||
|
||||
// Auto-dismiss after 2 seconds
|
||||
this.timeout = setTimeout(() => {
|
||||
this.close();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clean up the timeout when the controller disconnects
|
||||
disconnect() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Close the flash message with a fade-out animation
|
||||
close() {
|
||||
// Add opacity transition classes
|
||||
this.element.classList.add(
|
||||
"opacity-0",
|
||||
"transition-opacity",
|
||||
"duration-300",
|
||||
);
|
||||
|
||||
// Remove element after transition completes
|
||||
setTimeout(() => {
|
||||
this.element.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
73
app/javascript/controllers/header_controller.js
Normal file
73
app/javascript/controllers/header_controller.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Controller for handling the header navigation
|
||||
// Manages mobile menu toggle and user dropdown menu
|
||||
export default class extends Controller {
|
||||
static targets = ["mobileMenu", "mobileMenuButton", "userMenu", "userMenuButton"]
|
||||
|
||||
connect() {
|
||||
// Initialize menu states
|
||||
this.mobileMenuOpen = false
|
||||
this.userMenuOpen = false
|
||||
|
||||
// Add click outside listener for user menu
|
||||
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
||||
document.addEventListener("click", this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
// Clean up event listener
|
||||
document.removeEventListener("click", this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
// Toggle mobile menu visibility
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen
|
||||
this.mobileMenuTarget.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||
|
||||
// Update button icon based on state
|
||||
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||
|
||||
if (iconOpen && iconClose) {
|
||||
iconOpen.classList.toggle("hidden", this.mobileMenuOpen)
|
||||
iconClose.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle user dropdown menu visibility
|
||||
toggleUserMenu() {
|
||||
this.userMenuOpen = !this.userMenuOpen
|
||||
if (this.hasUserMenuTarget) {
|
||||
this.userMenuTarget.classList.toggle("hidden", !this.userMenuOpen)
|
||||
}
|
||||
}
|
||||
|
||||
// Close menus when clicking outside
|
||||
handleClickOutside(event) {
|
||||
// Close user menu if clicked outside
|
||||
if (this.userMenuOpen && this.hasUserMenuTarget &&
|
||||
!this.userMenuTarget.contains(event.target) &&
|
||||
!this.userMenuButtonTarget.contains(event.target)) {
|
||||
this.userMenuOpen = false
|
||||
this.userMenuTarget.classList.add("hidden")
|
||||
}
|
||||
|
||||
// Close mobile menu if clicked outside
|
||||
if (this.mobileMenuOpen &&
|
||||
!this.mobileMenuTarget.contains(event.target) &&
|
||||
!this.mobileMenuButtonTarget.contains(event.target)) {
|
||||
this.mobileMenuOpen = false
|
||||
this.mobileMenuTarget.classList.add("hidden")
|
||||
|
||||
// Update button icon
|
||||
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||
|
||||
if (iconOpen && iconClose) {
|
||||
iconOpen.classList.remove("hidden")
|
||||
iconClose.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/javascript/controllers/index.js
Normal file → Executable file
19
app/javascript/controllers/index.js
Normal file → Executable file
@@ -2,10 +2,19 @@
|
||||
// Run that command whenever you add a new controller or create them with
|
||||
// ./bin/rails generate stimulus controllerName
|
||||
|
||||
import { application } from "./application"
|
||||
import { application } from "./application";
|
||||
|
||||
import ShadcnTestController from "./shadcn_test_controller"
|
||||
import CounterController from "./counter_controller"
|
||||
import LogoutController from "./logout_controller";
|
||||
application.register("logout", LogoutController);
|
||||
|
||||
application.register("shadcn-test", ShadcnTestController)
|
||||
application.register("counter", CounterController)
|
||||
import CounterController from "./counter_controller";
|
||||
application.register("counter", CounterController);
|
||||
|
||||
import FlashMessageController from "./flash_message_controller";
|
||||
application.register("flash-message", FlashMessageController);
|
||||
|
||||
import TicketSelectionController from "./ticket_selection_controller";
|
||||
application.register("ticket-selection", TicketSelectionController);
|
||||
|
||||
import HeaderController from "./header_controller";
|
||||
application.register("header", HeaderController);
|
||||
|
||||
31
app/javascript/controllers/logout_controller.js
Normal file → Executable file
31
app/javascript/controllers/logout_controller.js
Normal file → Executable file
@@ -1,29 +1,36 @@
|
||||
// app/javascript/controllers/logout_controller.js
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Controller for handling user logout functionality
|
||||
// Sends a DELETE request to the server to sign out the user
|
||||
export default class extends Controller {
|
||||
// Define controller values
|
||||
static values = {
|
||||
url: String,
|
||||
url: String, // Optional URL for logout endpoint
|
||||
};
|
||||
|
||||
// Log when the controller is mounted
|
||||
connect() {
|
||||
// Optional: Add confirmation message
|
||||
//console.log("Hello LogoutController, Stimulus!", this.element);
|
||||
// this.element.dataset.confirm = "Êtes-vous sûr de vouloir vous déconnecter ?";
|
||||
// Display a message when the controller is mounted
|
||||
// console.log("LogoutController mounted", this.element);
|
||||
}
|
||||
|
||||
// Handle the sign out action
|
||||
signOut(event) {
|
||||
event.preventDefault();
|
||||
console.log("LogoutController#signOut mounted");
|
||||
console.log("User clicked on logout button.");
|
||||
|
||||
// Ensure user wants to disconnect with a confirmation request
|
||||
// if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; }
|
||||
|
||||
// Retrieve the csrf token from header
|
||||
// Retrieve the csrf token from header for security
|
||||
const csrfToken = document.querySelector("[name='csrf-token']").content;
|
||||
|
||||
// Define url to redirect user when action is valid
|
||||
const url = this.hasUrlValue ? this.urlValue : this.element.href;
|
||||
let url = this.hasUrlValue ? this.urlValue : this.element.href;
|
||||
// Ensure the URL is using the correct path prefix
|
||||
if (url && !url.includes('/auth/sign_out')) {
|
||||
url = url.replace('/users/sign_out', '/auth/sign_out');
|
||||
}
|
||||
|
||||
// Use fetch to send logout request
|
||||
fetch(url, {
|
||||
@@ -36,14 +43,14 @@ export default class extends Controller {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then((response) => {
|
||||
// console.log(this.element.dataset.loginUrlValue); // By default, we does not return anything.
|
||||
// console.log(this.element.dataset.redirectUrlValue); // By default, we does not return anything.
|
||||
|
||||
// By default the response does not include any url.
|
||||
// Redirect to default login page (loginUrlValue)
|
||||
// Redirect to default login page (redirectUrlValue)
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else if (this.element.dataset.loginUrlValue) {
|
||||
window.location.href = this.element.dataset.loginUrlValue;
|
||||
} else if (this.element.dataset.redirectUrlValue) {
|
||||
window.location.href = this.element.dataset.redirectUrlValue;
|
||||
return;
|
||||
}
|
||||
window.location.href = "/";
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import React from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { Button } from "@/components/button"
|
||||
|
||||
// Connects to data-controller="shadcn-test"
|
||||
export default class extends Controller {
|
||||
static targets = ["container"]
|
||||
|
||||
connect() {
|
||||
console.log("Shadcn Button Test Controller connected")
|
||||
this.renderButton()
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
const container = this.containerTarget
|
||||
const root = createRoot(container)
|
||||
|
||||
root.render(
|
||||
<div className="flex flex-col items-center gap-4 p-6">
|
||||
<h3 className="text-white text-lg font-semibold">Test Button Shadcn</h3>
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
Cliquez ici - PostCSS Test
|
||||
</Button>
|
||||
<p className="text-gray-300 text-sm">Ce bouton utilise shadcn/ui + Tailwind + PostCSS</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
|
||||
console.log("Shadcn button clicked - PostCSS compilation successful")
|
||||
}
|
||||
}
|
||||
169
app/javascript/controllers/ticket_selection_controller.js
Normal file
169
app/javascript/controllers/ticket_selection_controller.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Controller for handling ticket selection on the event show page
|
||||
// Manages quantity inputs, calculates totals, and enables/disables the checkout button
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"quantityInput",
|
||||
"totalQuantity",
|
||||
"totalAmount",
|
||||
"checkoutButton",
|
||||
"form",
|
||||
];
|
||||
static values = { eventSlug: String, eventId: String };
|
||||
|
||||
// Initialize the controller and update the cart summary
|
||||
connect() {
|
||||
this.updateCartSummary();
|
||||
this.bindFormSubmission();
|
||||
}
|
||||
|
||||
// Bind form submission to handle cart storage
|
||||
bindFormSubmission() {
|
||||
if (this.hasFormTarget) {
|
||||
this.formTarget.addEventListener("submit", this.submitCart.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the quantity for a specific ticket type
|
||||
increment(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.target;
|
||||
const input = this.quantityInputTargets.find(
|
||||
(input) => input.dataset.target === ticketTypeId,
|
||||
);
|
||||
const value = parseInt(input.value) || 0;
|
||||
const max = parseInt(input.max) || 0;
|
||||
|
||||
if (value < max) {
|
||||
input.value = value + 1;
|
||||
this.updateCartSummary();
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement the quantity for a specific ticket type
|
||||
decrement(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.target;
|
||||
const input = this.quantityInputTargets.find(
|
||||
(input) => input.dataset.target === ticketTypeId,
|
||||
);
|
||||
const value = parseInt(input.value) || 0;
|
||||
|
||||
if (value > 0) {
|
||||
input.value = value - 1;
|
||||
this.updateCartSummary();
|
||||
}
|
||||
}
|
||||
|
||||
// Update quantity when directly edited in the input field
|
||||
updateQuantity(event) {
|
||||
const input = event.currentTarget;
|
||||
let value = parseInt(input.value) || 0;
|
||||
const max = parseInt(input.max) || 0;
|
||||
|
||||
// Ensure value is within valid range (0 to max available)
|
||||
if (value < 0) value = 0;
|
||||
if (value > max) value = max;
|
||||
|
||||
input.value = value;
|
||||
this.updateCartSummary();
|
||||
}
|
||||
|
||||
// Calculate and update the cart summary (total quantity and amount)
|
||||
updateCartSummary() {
|
||||
let totalQuantity = 0;
|
||||
let totalAmount = 0;
|
||||
|
||||
// Sum up quantities and calculate total amount
|
||||
this.quantityInputTargets.forEach((input) => {
|
||||
const quantity = parseInt(input.value) || 0;
|
||||
const price = parseInt(input.dataset.price) || 0;
|
||||
|
||||
totalQuantity += quantity;
|
||||
totalAmount += quantity * price;
|
||||
});
|
||||
|
||||
// Update the displayed total quantity and amount
|
||||
this.totalQuantityTarget.textContent = totalQuantity;
|
||||
this.totalAmountTarget.textContent = `€${(totalAmount / 100).toFixed(2)}`;
|
||||
|
||||
// Enable/disable checkout button based on whether any tickets are selected
|
||||
if (totalQuantity > 0) {
|
||||
this.checkoutButtonTarget.classList.remove(
|
||||
"opacity-50",
|
||||
"cursor-not-allowed",
|
||||
);
|
||||
this.checkoutButtonTarget.disabled = false;
|
||||
} else {
|
||||
this.checkoutButtonTarget.classList.add(
|
||||
"opacity-50",
|
||||
"cursor-not-allowed",
|
||||
);
|
||||
this.checkoutButtonTarget.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission - store cart in session before proceeding
|
||||
async submitCart(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const cartData = this.buildCartData();
|
||||
|
||||
if (Object.keys(cartData).length === 0) {
|
||||
alert("Veuillez sélectionner au moins un billet");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Store cart data in session
|
||||
await this.storeCartInSession(cartData);
|
||||
|
||||
// Redirect to event-scoped orders/new page
|
||||
const OrderNewUrl = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`;
|
||||
window.location.href = OrderNewUrl;
|
||||
} catch (error) {
|
||||
console.error("Error storing cart:", error);
|
||||
alert("Une erreur est survenue. Veuillez réessayer.");
|
||||
}
|
||||
}
|
||||
|
||||
// Build cart data from current form state
|
||||
buildCartData() {
|
||||
const cartData = {};
|
||||
|
||||
this.quantityInputTargets.forEach((input) => {
|
||||
const quantity = parseInt(input.value) || 0;
|
||||
if (quantity > 0) {
|
||||
const ticketTypeId = input.dataset.target;
|
||||
cartData[ticketTypeId] = {
|
||||
quantity: quantity,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return cartData;
|
||||
}
|
||||
|
||||
// Store cart data in session via AJAX
|
||||
async storeCartInSession(cartData) {
|
||||
const storeCartUrl = `/api/v1/events/${this.eventIdValue}/store_cart`;
|
||||
|
||||
const response = await fetch(storeCartUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
.getAttribute("content"),
|
||||
},
|
||||
body: JSON.stringify({ cart: cartData }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to store cart data: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
0
app/jobs/application_job.rb
Normal file → Executable file
0
app/jobs/application_job.rb
Normal file → Executable file
33
app/jobs/cleanup_expired_drafts_job.rb
Normal file
33
app/jobs/cleanup_expired_drafts_job.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# Background job to clean up expired draft tickets
|
||||
#
|
||||
# This job runs periodically to find and expire draft tickets that have
|
||||
# passed their expiry time (typically 30 minutes after creation).
|
||||
# Should be scheduled via cron or similar scheduling system.
|
||||
class CleanupExpiredDraftsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
# Find and expire all draft tickets that have passed their expiry time
|
||||
#
|
||||
# Uses find_each to process tickets in batches to avoid memory issues
|
||||
# with large datasets. Continues processing even if individual tickets fail.
|
||||
def perform
|
||||
expired_count = 0
|
||||
|
||||
# Process expired draft tickets in batches
|
||||
Ticket.expired_drafts.find_each do |ticket|
|
||||
begin
|
||||
Rails.logger.info "Expiring draft ticket #{ticket.id} for user #{ticket.user.id}"
|
||||
ticket.expire_if_overdue!
|
||||
expired_count += 1
|
||||
rescue => e
|
||||
# Log error but continue processing other tickets
|
||||
Rails.logger.error "Failed to expire ticket #{ticket.id}: #{e.message}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# Log summary if any tickets were processed
|
||||
Rails.logger.info "Expired #{expired_count} draft tickets" if expired_count > 0
|
||||
Rails.logger.info "No expired draft tickets found" if expired_count == 0
|
||||
end
|
||||
end
|
||||
23
app/jobs/expired_orders_cleanup_job.rb
Normal file
23
app/jobs/expired_orders_cleanup_job.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
class ExpiredOrdersCleanupJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
# Find and expire all draft orders that have passed their expiry time
|
||||
expired_orders = Order.expired_drafts
|
||||
|
||||
Rails.logger.info "Found #{expired_orders.count} expired orders to process"
|
||||
|
||||
expired_orders.find_each do |order|
|
||||
begin
|
||||
order.expire_if_overdue!
|
||||
Rails.logger.info "Expired order ##{order.id} for user ##{order.user_id}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to expire order ##{order.id}: #{e.message}"
|
||||
# Continue processing other orders even if one fails
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
Rails.logger.info "Completed expired orders cleanup job"
|
||||
end
|
||||
end
|
||||
49
app/jobs/stripe_invoice_generation_job.rb
Normal file
49
app/jobs/stripe_invoice_generation_job.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
# Background job to create Stripe invoices for accounting records
|
||||
#
|
||||
# This job is responsible for creating post-payment invoices in Stripe
|
||||
# for accounting purposes after a successful payment
|
||||
class StripeInvoiceGenerationJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
# Retry up to 3 times with exponential backoff
|
||||
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
||||
|
||||
# Don't retry on Stripe authentication errors
|
||||
discard_on Stripe::AuthenticationError
|
||||
|
||||
def perform(order_id)
|
||||
order = Order.find(order_id)
|
||||
|
||||
unless order.status == "paid"
|
||||
Rails.logger.warn "Attempted to create invoice for unpaid order #{order_id}"
|
||||
return
|
||||
end
|
||||
|
||||
# Create the Stripe invoice
|
||||
service = StripeInvoiceService.new(order)
|
||||
stripe_invoice = service.create_post_payment_invoice
|
||||
|
||||
if stripe_invoice
|
||||
# Store the invoice ID (you might want to persist this in the database)
|
||||
order.instance_variable_set(:@stripe_invoice_id, stripe_invoice.id)
|
||||
|
||||
Rails.logger.info "Successfully created Stripe invoice #{stripe_invoice.id} for order #{order.id} via background job"
|
||||
|
||||
# Optionally send notification email about invoice availability
|
||||
# InvoiceMailer.invoice_ready(order, stripe_invoice.id).deliver_now
|
||||
else
|
||||
error_msg = service.errors.join(", ")
|
||||
Rails.logger.error "Failed to create Stripe invoice for order #{order.id}: #{error_msg}"
|
||||
raise StandardError, "Invoice generation failed: #{error_msg}"
|
||||
end
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
Rails.logger.error "Order #{order_id} not found for invoice generation"
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.error "Stripe error creating invoice for order #{order_id}: #{e.message}"
|
||||
raise e # Re-raise to trigger retry logic
|
||||
rescue => e
|
||||
Rails.logger.error "Unexpected error creating invoice for order #{order_id}: #{e.message}"
|
||||
raise e # Re-raise to trigger retry logic
|
||||
end
|
||||
end
|
||||
0
app/mailers/application_mailer.rb
Normal file → Executable file
0
app/mailers/application_mailer.rb
Normal file → Executable file
21
app/mailers/ticket_mailer.rb
Executable file
21
app/mailers/ticket_mailer.rb
Executable file
@@ -0,0 +1,21 @@
|
||||
class TicketMailer < ApplicationMailer
|
||||
default from: "notifications@aperonight.com"
|
||||
|
||||
def purchase_confirmation(ticket)
|
||||
@ticket = ticket
|
||||
@user = ticket.user
|
||||
@event = ticket.event
|
||||
|
||||
# Generate PDF attachment
|
||||
pdf = @ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: "application/pdf",
|
||||
content: pdf
|
||||
}
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}"
|
||||
)
|
||||
end
|
||||
end
|
||||
3
app/models/application_record.rb
Normal file → Executable file
3
app/models/application_record.rb
Normal file → Executable file
@@ -1,3 +1,6 @@
|
||||
# Base class for all ActiveRecord models in the application
|
||||
# Provides common functionality and configuration for all models
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
# Mark this as the primary abstract class to establish inheritance hierarchy
|
||||
primary_abstract_class
|
||||
end
|
||||
|
||||
0
app/models/concerns/.keep
Normal file → Executable file
0
app/models/concerns/.keep
Normal file → Executable file
51
app/models/event.rb
Executable file
51
app/models/event.rb
Executable file
@@ -0,0 +1,51 @@
|
||||
# Event model representing nightlife events and events
|
||||
# Manages event details, location data, and publication state
|
||||
class Event < ApplicationRecord
|
||||
# Define states for Event lifecycle management
|
||||
# draft: Initial state when Event is being created
|
||||
# published: Event is visible to public and can be discovered
|
||||
# canceled: Event has been canceled by organizer
|
||||
# sold_out: Event has reached capacity and tickets are no longer available
|
||||
enum :state, {
|
||||
draft: 0,
|
||||
published: 1,
|
||||
canceled: 2,
|
||||
sold_out: 3
|
||||
}, default: :draft
|
||||
|
||||
# === Relations ===
|
||||
belongs_to :user
|
||||
has_many :ticket_types
|
||||
has_many :tickets, through: :ticket_types
|
||||
has_many :orders
|
||||
|
||||
# Validations for Event attributes
|
||||
# Basic information
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :description, presence: true, length: { minimum: 10, maximum: 1000 }
|
||||
validates :state, presence: true, inclusion: { in: states.keys }
|
||||
validates :image, length: { maximum: 500 } # URL or path to image
|
||||
|
||||
# Venue information
|
||||
validates :venue_name, presence: true, length: { maximum: 100 }
|
||||
validates :venue_address, presence: true, length: { maximum: 200 }
|
||||
|
||||
# Geographic coordinates for map display
|
||||
validates :latitude, presence: true, numericality: {
|
||||
greater_than_or_equal_to: -90,
|
||||
less_than_or_equal_to: 90
|
||||
}
|
||||
validates :longitude, presence: true, numericality: {
|
||||
greater_than_or_equal_to: -180,
|
||||
less_than_or_equal_to: 180
|
||||
}
|
||||
|
||||
# Scopes for querying events with common filters
|
||||
scope :featured, -> { where(featured: true) } # Get featured events for homepage
|
||||
scope :published, -> { where(state: :published) } # Get publicly visible events
|
||||
scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{query}%") } # Search by name (case-insensitive)
|
||||
|
||||
# Scope for published events ordered by start time
|
||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||
end
|
||||
127
app/models/order.rb
Normal file
127
app/models/order.rb
Normal file
@@ -0,0 +1,127 @@
|
||||
class Order < ApplicationRecord
|
||||
# === Constants ===
|
||||
DRAFT_EXPIRY_TIME = 30.minutes
|
||||
MAX_PAYMENT_ATTEMPTS = 3
|
||||
|
||||
# === Associations ===
|
||||
belongs_to :user
|
||||
belongs_to :event
|
||||
has_many :tickets, dependent: :destroy
|
||||
|
||||
# === Validations ===
|
||||
validates :user_id, presence: true
|
||||
validates :event_id, presence: true
|
||||
validates :status, presence: true, inclusion: {
|
||||
in: %w[draft pending_payment paid completed cancelled expired]
|
||||
}
|
||||
validates :total_amount_cents, presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :payment_attempts, presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
# Stripe invoice ID for accounting records
|
||||
attr_accessor :stripe_invoice_id
|
||||
|
||||
# === Scopes ===
|
||||
scope :draft, -> { where(status: "draft") }
|
||||
scope :active, -> { where(status: %w[paid completed]) }
|
||||
scope :expired_drafts, -> { draft.where("expires_at < ?", Time.current) }
|
||||
scope :can_retry_payment, -> {
|
||||
draft.where("payment_attempts < ? AND expires_at > ?",
|
||||
MAX_PAYMENT_ATTEMPTS, Time.current)
|
||||
}
|
||||
|
||||
before_validation :set_expiry, on: :create
|
||||
|
||||
# === Instance Methods ===
|
||||
|
||||
# Total amount in euros (formatted)
|
||||
def total_amount_euros
|
||||
total_amount_cents / 100.0
|
||||
end
|
||||
|
||||
# Check if order can be retried for payment
|
||||
def can_retry_payment?
|
||||
draft? && payment_attempts < MAX_PAYMENT_ATTEMPTS && !expired?
|
||||
end
|
||||
|
||||
# Check if order is expired
|
||||
def expired?
|
||||
expires_at.present? && expires_at < Time.current
|
||||
end
|
||||
|
||||
# Mark order as expired if it's past expiry time
|
||||
def expire_if_overdue!
|
||||
return unless draft? && expired?
|
||||
update!(status: "expired")
|
||||
end
|
||||
|
||||
# Increment payment attempt counter
|
||||
def increment_payment_attempt!
|
||||
update!(
|
||||
payment_attempts: payment_attempts + 1,
|
||||
last_payment_attempt_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
# Check if draft is about to expire (within 5 minutes)
|
||||
def expiring_soon?
|
||||
return false unless draft? && expires_at.present?
|
||||
expires_at <= 5.minutes.from_now
|
||||
end
|
||||
|
||||
# Mark order as paid and activate all tickets
|
||||
def mark_as_paid!
|
||||
transaction do
|
||||
update!(status: "paid")
|
||||
tickets.update_all(status: "active")
|
||||
end
|
||||
end
|
||||
|
||||
# Calculate total from tickets
|
||||
def calculate_total!
|
||||
update!(total_amount_cents: tickets.sum(:price_cents))
|
||||
end
|
||||
|
||||
# Create Stripe invoice for accounting records
|
||||
#
|
||||
# This method creates a post-payment invoice in Stripe for accounting purposes
|
||||
# It should only be called after the order has been paid
|
||||
#
|
||||
# @return [String, nil] The Stripe invoice ID or nil if creation failed
|
||||
def create_stripe_invoice!
|
||||
return nil unless status == "paid"
|
||||
return @stripe_invoice_id if @stripe_invoice_id.present?
|
||||
|
||||
service = StripeInvoiceService.new(self)
|
||||
stripe_invoice = service.create_post_payment_invoice
|
||||
|
||||
if stripe_invoice
|
||||
@stripe_invoice_id = stripe_invoice.id
|
||||
Rails.logger.info "Created Stripe invoice #{stripe_invoice.id} for order #{id}"
|
||||
stripe_invoice.id
|
||||
else
|
||||
Rails.logger.error "Failed to create Stripe invoice for order #{id}: #{service.errors.join(', ')}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Get the Stripe invoice PDF URL if available
|
||||
#
|
||||
# @return [String, nil] The PDF URL or nil if not available
|
||||
def stripe_invoice_pdf_url
|
||||
return nil unless @stripe_invoice_id.present?
|
||||
StripeInvoiceService.get_invoice_pdf_url(@stripe_invoice_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_expiry
|
||||
return unless status == "draft"
|
||||
self.expires_at = DRAFT_EXPIRY_TIME.from_now if expires_at.blank?
|
||||
end
|
||||
|
||||
def draft?
|
||||
status == "draft"
|
||||
end
|
||||
end
|
||||
100
app/models/ticket.rb
Executable file
100
app/models/ticket.rb
Executable file
@@ -0,0 +1,100 @@
|
||||
class Ticket < ApplicationRecord
|
||||
# === Associations ===
|
||||
belongs_to :order
|
||||
belongs_to :ticket_type
|
||||
has_one :event, through: :ticket_type
|
||||
has_one :user, through: :order
|
||||
|
||||
# === Validations ===
|
||||
validates :qr_code, presence: true, uniqueness: true
|
||||
validates :order_id, presence: true
|
||||
validates :ticket_type_id, presence: true
|
||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
||||
validates :status, presence: true, inclusion: { in: %w[draft active used expired refunded] }
|
||||
validates :first_name, presence: true
|
||||
validates :last_name, presence: true
|
||||
|
||||
# === Scopes ===
|
||||
scope :draft, -> { where(status: "draft") }
|
||||
scope :active, -> { where(status: "active") }
|
||||
scope :expired_drafts, -> { joins(:order).where(status: "draft").where("orders.expires_at < ?", Time.current) }
|
||||
|
||||
before_validation :set_price_from_ticket_type, on: :create
|
||||
before_validation :generate_qr_code, on: :create
|
||||
|
||||
# Generate PDF ticket
|
||||
def to_pdf
|
||||
TicketPdfGenerator.new(self).generate
|
||||
end
|
||||
|
||||
# Generate QR code data for ticket validation
|
||||
def to_qr_data
|
||||
{
|
||||
ticket_id: id,
|
||||
qr_code: qr_code,
|
||||
event_id: event&.id,
|
||||
user_id: user&.id
|
||||
}.compact.to_json
|
||||
end
|
||||
|
||||
# Generate QR code as SVG
|
||||
def generate_qr_svg
|
||||
require "rqrcode"
|
||||
qrcode = RQRCode::QRCode.new(to_qr_data)
|
||||
qrcode.as_svg(
|
||||
offset: 0,
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 4,
|
||||
standalone: true
|
||||
)
|
||||
end
|
||||
|
||||
# Price in euros (formatted)
|
||||
def price_euros
|
||||
price_cents / 100.0
|
||||
end
|
||||
|
||||
# Delegate payment methods to order
|
||||
def can_retry_payment?
|
||||
order.can_retry_payment?
|
||||
end
|
||||
|
||||
def expired?
|
||||
order.expired?
|
||||
end
|
||||
|
||||
def expiring_soon?
|
||||
order.expiring_soon?
|
||||
end
|
||||
|
||||
# Mark ticket as expired if it's past expiry time
|
||||
def expire_if_overdue!
|
||||
order.expire_if_overdue!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_price_from_ticket_type
|
||||
return unless ticket_type
|
||||
self.price_cents = ticket_type.price_cents
|
||||
end
|
||||
|
||||
def generate_qr_code
|
||||
return if qr_code.present?
|
||||
|
||||
loop do
|
||||
self.qr_code = SecureRandom.uuid
|
||||
break unless Ticket.exists?(qr_code: qr_code)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to generate QR code for ticket: #{e.message}"
|
||||
# Generate a simple fallback QR code
|
||||
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
|
||||
end
|
||||
|
||||
|
||||
def draft?
|
||||
status == "draft"
|
||||
end
|
||||
end
|
||||
85
app/models/ticket_type.rb
Executable file
85
app/models/ticket_type.rb
Executable file
@@ -0,0 +1,85 @@
|
||||
class TicketType < ApplicationRecord
|
||||
# Associations
|
||||
belongs_to :event
|
||||
has_many :tickets, dependent: :destroy
|
||||
|
||||
# Validations
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 50 }
|
||||
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
||||
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :sale_start_at, presence: true
|
||||
validates :sale_end_at, presence: true
|
||||
validates :minimum_age, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }, allow_nil: true
|
||||
validates :event_id, presence: true
|
||||
validates :requires_id, inclusion: { in: [ true, false ] }
|
||||
|
||||
# Custom validations
|
||||
validate :sale_end_after_start
|
||||
validate :sale_times_within_event_period
|
||||
|
||||
# Scopes
|
||||
scope :available_now, -> { where("sale_start_at <= ? AND sale_end_at >= ?", Time.current, Time.current) }
|
||||
scope :upcoming, -> { where("sale_start_at > ?", Time.current) }
|
||||
scope :expired, -> { where("sale_end_at < ?", Time.current) }
|
||||
|
||||
# Helper methods
|
||||
def price_euros
|
||||
return 0.0 if price_cents.nil?
|
||||
price_cents / 100.0
|
||||
end
|
||||
|
||||
def price_euros=(value)
|
||||
self.price_cents = (value.to_f * 100).to_i
|
||||
end
|
||||
|
||||
def available?
|
||||
return false if sale_start_at.nil? || sale_end_at.nil?
|
||||
sale_start_at <= Time.current && sale_end_at >= Time.current
|
||||
end
|
||||
|
||||
def sold_out?
|
||||
return false if quantity.nil?
|
||||
tickets.count >= quantity
|
||||
end
|
||||
|
||||
def available_quantity
|
||||
return 0 if quantity.nil?
|
||||
[ quantity - tickets.count, 0 ].max
|
||||
end
|
||||
|
||||
def sales_status
|
||||
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
||||
return :expired if sale_end_at < Time.current
|
||||
return :upcoming if sale_start_at > Time.current
|
||||
return :sold_out if sold_out?
|
||||
:available
|
||||
end
|
||||
|
||||
def total_potential_revenue
|
||||
return 0.0 if quantity.nil? || price_cents.nil?
|
||||
quantity * price_euros
|
||||
end
|
||||
|
||||
def current_revenue
|
||||
return 0.0 if price_cents.nil?
|
||||
tickets.count * price_euros
|
||||
end
|
||||
|
||||
def remaining_potential_revenue
|
||||
return 0.0 if quantity.nil? || price_cents.nil?
|
||||
available_quantity * price_euros
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sale_end_after_start
|
||||
return unless sale_start_at && sale_end_at
|
||||
errors.add(:sale_end_at, "must be after sale start") if sale_end_at <= sale_start_at
|
||||
end
|
||||
|
||||
def sale_times_within_event_period
|
||||
return unless event&.start_time && sale_end_at
|
||||
errors.add(:sale_end_at, "cannot be after the event starts") if sale_end_at > event.start_time
|
||||
end
|
||||
end
|
||||
37
app/models/user.rb
Normal file → Executable file
37
app/models/user.rb
Normal file → Executable file
@@ -1,6 +1,43 @@
|
||||
# User model for authentication and user management
|
||||
# Handles user accounts, authentication, and authorization using Devise
|
||||
class User < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||
#
|
||||
# Include default devise modules for authentication
|
||||
# :database_authenticatable - encrypts and stores password in database
|
||||
# :registerable - allows users to sign up and edit their accounts
|
||||
# :recoverable - handles password reset functionality
|
||||
# :rememberable - manages token-based user remembering
|
||||
# :validatable - provides email and password validation
|
||||
# Other available modules are:
|
||||
# :confirmable - requires email confirmation
|
||||
# :lockable - locks account after failed login attempts
|
||||
# :timeoutable - expires sessions after inactivity
|
||||
# :trackable - tracks sign-in count, timestamps, and IP
|
||||
# :omniauthable - allows authentication via OAuth providers
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
# Relationships
|
||||
has_many :events, dependent: :destroy
|
||||
has_many :tickets, dependent: :destroy
|
||||
has_many :orders, dependent: :destroy
|
||||
|
||||
# Validations - allow reasonable name lengths
|
||||
validates :last_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
||||
validates :first_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
||||
validates :company_name, length: { minimum: 2, maximum: 100, allow_blank: true }
|
||||
|
||||
# Authorization methods
|
||||
def can_manage_events?
|
||||
# For now, all authenticated users can manage events
|
||||
# This can be extended later with role-based permissions
|
||||
true
|
||||
end
|
||||
|
||||
def promoter?
|
||||
# Alias for can_manage_events? to make views more semantic
|
||||
can_manage_events?
|
||||
end
|
||||
end
|
||||
|
||||
206
app/services/stripe_invoice_service.rb
Normal file
206
app/services/stripe_invoice_service.rb
Normal file
@@ -0,0 +1,206 @@
|
||||
# Service to create Stripe invoices for accounting records after successful payment
|
||||
#
|
||||
# This service creates post-payment invoices in Stripe for accounting purposes.
|
||||
# Unlike regular Stripe invoices which are used for collection, these are
|
||||
# created after payment via Checkout Sessions as accounting records.
|
||||
class StripeInvoiceService
|
||||
attr_reader :order, :errors
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
@errors = []
|
||||
end
|
||||
|
||||
# Create a post-payment invoice in Stripe
|
||||
#
|
||||
# Returns the created Stripe invoice object or nil if creation failed
|
||||
def create_post_payment_invoice
|
||||
return nil unless valid_for_invoice_creation?
|
||||
|
||||
begin
|
||||
customer = find_or_create_stripe_customer
|
||||
return nil unless customer
|
||||
|
||||
invoice = create_stripe_invoice(customer)
|
||||
return nil unless invoice
|
||||
|
||||
add_line_items_to_invoice(customer, invoice)
|
||||
finalize_invoice(invoice)
|
||||
|
||||
Rails.logger.info "Successfully created Stripe invoice #{invoice.id} for order #{@order.id}"
|
||||
invoice
|
||||
rescue Stripe::StripeError => e
|
||||
handle_stripe_error(e)
|
||||
nil
|
||||
rescue => e
|
||||
handle_generic_error(e)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Get the PDF URL for a Stripe invoice
|
||||
#
|
||||
# @param invoice_id [String] The Stripe invoice ID
|
||||
# @return [String, nil] The invoice PDF URL or nil if not available
|
||||
def self.get_invoice_pdf_url(invoice_id)
|
||||
return nil if invoice_id.blank?
|
||||
|
||||
begin
|
||||
invoice = Stripe::Invoice.retrieve(invoice_id)
|
||||
invoice.invoice_pdf
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.error "Failed to retrieve Stripe invoice PDF URL: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_for_invoice_creation?
|
||||
unless @order.present?
|
||||
@errors << "Order is required"
|
||||
return false
|
||||
end
|
||||
|
||||
unless @order.status == "paid"
|
||||
@errors << "Order must be paid to create invoice"
|
||||
return false
|
||||
end
|
||||
|
||||
unless @order.user.present?
|
||||
@errors << "Order must have an associated user"
|
||||
return false
|
||||
end
|
||||
|
||||
unless @order.tickets.any?
|
||||
@errors << "Order must have tickets to create invoice"
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def find_or_create_stripe_customer
|
||||
if @order.user.stripe_customer_id.present?
|
||||
retrieve_existing_customer
|
||||
else
|
||||
create_new_customer
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_existing_customer
|
||||
Stripe::Customer.retrieve(@order.user.stripe_customer_id)
|
||||
rescue Stripe::InvalidRequestError
|
||||
# Customer doesn't exist, create a new one
|
||||
Rails.logger.warn "Stripe customer #{@order.user.stripe_customer_id} not found, creating new customer"
|
||||
@order.user.update(stripe_customer_id: nil)
|
||||
create_new_customer
|
||||
end
|
||||
|
||||
def create_new_customer
|
||||
customer = Stripe::Customer.create({
|
||||
email: @order.user.email,
|
||||
name: customer_name,
|
||||
metadata: {
|
||||
user_id: @order.user.id,
|
||||
created_by: "aperonight_system"
|
||||
}
|
||||
})
|
||||
|
||||
@order.user.update(stripe_customer_id: customer.id)
|
||||
Rails.logger.info "Created new Stripe customer #{customer.id} for user #{@order.user.id}"
|
||||
customer
|
||||
end
|
||||
|
||||
def customer_name
|
||||
parts = []
|
||||
parts << @order.user.first_name if @order.user.first_name.present?
|
||||
parts << @order.user.last_name if @order.user.last_name.present?
|
||||
|
||||
if parts.empty?
|
||||
@order.user.email.split("@").first.humanize
|
||||
else
|
||||
parts.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
def create_stripe_invoice(customer)
|
||||
invoice_data = {
|
||||
customer: customer.id,
|
||||
collection_method: "send_invoice", # Don't auto-charge
|
||||
auto_advance: false, # Don't automatically finalize
|
||||
metadata: {
|
||||
order_id: @order.id,
|
||||
user_id: @order.user.id,
|
||||
event_name: @order.event.name,
|
||||
created_by: "aperonight_system",
|
||||
payment_method: "checkout_session"
|
||||
},
|
||||
description: "Invoice for #{@order.event.name} - Order ##{@order.id}",
|
||||
footer: "Thank you for your purchase! This invoice is for your records as payment was already processed."
|
||||
}
|
||||
|
||||
# Add due date (same day since it's already paid)
|
||||
invoice_data[:due_date] = Time.current.to_i
|
||||
|
||||
Stripe::Invoice.create(invoice_data)
|
||||
end
|
||||
|
||||
def add_line_items_to_invoice(customer, invoice)
|
||||
@order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets|
|
||||
quantity = tickets.count
|
||||
|
||||
Stripe::InvoiceItem.create({
|
||||
customer: customer.id,
|
||||
invoice: invoice.id,
|
||||
amount: ticket_type.price_cents * quantity,
|
||||
currency: "eur",
|
||||
description: build_line_item_description(ticket_type, tickets),
|
||||
metadata: {
|
||||
ticket_type_id: ticket_type.id,
|
||||
ticket_type_name: ticket_type.name,
|
||||
quantity: quantity,
|
||||
unit_price_cents: ticket_type.price_cents
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def build_line_item_description(ticket_type, tickets)
|
||||
quantity = tickets.count
|
||||
unit_price = ticket_type.price_cents / 100.0
|
||||
|
||||
description_parts = [
|
||||
"#{@order.event.name}",
|
||||
"#{ticket_type.name}",
|
||||
"(#{quantity}x €#{unit_price})"
|
||||
]
|
||||
|
||||
description_parts.join(" - ")
|
||||
end
|
||||
|
||||
def finalize_invoice(invoice)
|
||||
# Mark as paid since payment was already processed via checkout
|
||||
finalized_invoice = invoice.finalize_invoice
|
||||
|
||||
# Mark the invoice as paid
|
||||
finalized_invoice.pay({
|
||||
paid_out_of_band: true, # Payment was made outside of Stripe invoicing
|
||||
payment_method: nil # No payment method needed for out-of-band payment
|
||||
})
|
||||
|
||||
finalized_invoice
|
||||
end
|
||||
|
||||
def handle_stripe_error(error)
|
||||
error_message = "Stripe invoice creation failed: #{error.message}"
|
||||
@errors << error_message
|
||||
Rails.logger.error "#{error_message} (Order: #{@order.id})"
|
||||
end
|
||||
|
||||
def handle_generic_error(error)
|
||||
error_message = "Invoice creation failed: #{error.message}"
|
||||
@errors << error_message
|
||||
Rails.logger.error "#{error_message} (Order: #{@order.id})"
|
||||
end
|
||||
end
|
||||
157
app/views/components/_event_finder.html.erb
Executable file
157
app/views/components/_event_finder.html.erb
Executable file
@@ -0,0 +1,157 @@
|
||||
<!-- Event Finder Section -->
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="event-finder">
|
||||
<div class="finder-header">
|
||||
<h2 class="finder-title">Trouvez votre événement parfait</h2>
|
||||
<p class="finder-subtitle">Découvrez des événements afterwork adaptés à vos préférences</p>
|
||||
</div>
|
||||
|
||||
<form class="finder-form">
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="calendar"></i>
|
||||
Date
|
||||
</label>
|
||||
<input type="date" class="finder-input focus-ring" id="event-date">
|
||||
</div>
|
||||
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="map-pin"></i>
|
||||
Ville
|
||||
</label>
|
||||
<select class="finder-select focus-ring" id="event-city">
|
||||
<option value="">Choisissez une ville</option>
|
||||
<option value="paris">Paris</option>
|
||||
<option value="london">London</option>
|
||||
<option value="berlin">Berlin</option>
|
||||
<option value="madrid">Madrid</option>
|
||||
<option value="barcelona">Barcelona</option>
|
||||
<option value="amsterdam">Amsterdam</option>
|
||||
<option value="rome">Rome</option>
|
||||
<option value="zurich">Zurich</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="finder-field">
|
||||
<label class="finder-label">
|
||||
<i data-lucide="users"></i>
|
||||
Type d'événement
|
||||
</label>
|
||||
<select class="finder-select focus-ring" id="event-type">
|
||||
<option value="">Tous les types</option>
|
||||
<option value="networking">Réseautage</option>
|
||||
<option value="tech">Tech & Innovation</option>
|
||||
<option value="creative">Créatif & Design</option>
|
||||
<option value="business">Affaires</option>
|
||||
<option value="startup">Startup</option>
|
||||
<option value="wine">Vin & Dégustation</option>
|
||||
<option value="art">Art & Culture</option>
|
||||
<option value="music">Musique & Divertissement</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="finder-field price-range">
|
||||
<label class="finder-label">
|
||||
<div class="price-range-label">
|
||||
<span>
|
||||
<i data-lucide="euro"></i>
|
||||
Fourchette de prix
|
||||
</span>
|
||||
<span class="price-value" id="price-display">€0 - €100</span>
|
||||
</div>
|
||||
</label>
|
||||
<div style="display: flex; gap: var(--space-3); align-items: center;">
|
||||
<input type="range" class="price-slider" id="price-min" min="0" max="100" value="0" style="flex: 1;">
|
||||
<span style="color: var(--color-neutral-500); font-weight: 600;">à</span>
|
||||
<input type="range" class="price-slider" id="price-max" min="0" max="100" value="100" style="flex: 1;">
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<button type="submit" class="finder-search-btn">
|
||||
<i data-lucide="search"></i>
|
||||
Trouver des événements
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Fonctionnalité de recherche d'événements
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const priceMin = document.getElementById('price-min');
|
||||
const priceMax = document.getElementById('price-max');
|
||||
const priceDisplay = document.getElementById('price-display');
|
||||
|
||||
if (priceMin && priceMax && priceDisplay) {
|
||||
function updatePriceDisplay() {
|
||||
const minVal = parseInt(priceMin.value);
|
||||
const maxVal = parseInt(priceMax.value);
|
||||
|
||||
// Ensure min doesn't exceed max
|
||||
if (minVal > maxVal) {
|
||||
priceMin.value = maxVal;
|
||||
}
|
||||
|
||||
// Ensure max doesn't go below min
|
||||
if (maxVal < minVal) {
|
||||
priceMax.value = minVal;
|
||||
}
|
||||
|
||||
const finalMin = Math.min(parseInt(priceMin.value), parseInt(priceMax.value));
|
||||
const finalMax = Math.max(parseInt(priceMin.value), parseInt(priceMax.value));
|
||||
|
||||
priceDisplay.textContent = `€${finalMin} - €${finalMax}`;
|
||||
}
|
||||
|
||||
priceMin.addEventListener('input', updatePriceDisplay);
|
||||
priceMax.addEventListener('input', updatePriceDisplay);
|
||||
|
||||
// Set default date to tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateInput = document.getElementById('event-date');
|
||||
if (dateInput) {
|
||||
dateInput.value = tomorrow.toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission
|
||||
const finderForm = document.querySelector('.finder-form');
|
||||
if (finderForm) {
|
||||
finderForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('event-date').value,
|
||||
city: document.getElementById('event-city').value,
|
||||
type: document.getElementById('event-type').value,
|
||||
priceMin: priceMin ? priceMin.value : '',
|
||||
priceMax: priceMax ? priceMax.value : ''
|
||||
};
|
||||
|
||||
console.log('Filtres de recherche :', formData);
|
||||
|
||||
// Add loading state to button
|
||||
const searchBtn = document.querySelector('.finder-search-btn');
|
||||
if (searchBtn) {
|
||||
const originalText = searchBtn.innerHTML;
|
||||
searchBtn.innerHTML = '<div style="width: 20px; height: 20px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div> Recherche...';
|
||||
|
||||
// Simulate search
|
||||
setTimeout(() => {
|
||||
searchBtn.innerHTML = originalText;
|
||||
alert('Recherche terminée ! Les résultats seraient affichés ici.');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
15
app/views/components/_event_item.html.erb
Executable file
15
app/views/components/_event_item.html.erb
Executable file
@@ -0,0 +1,15 @@
|
||||
<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
|
||||
<%= event.name %>
|
||||
</h3>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
<%= l(event.start_time, format: :short) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
40
app/views/components/_footer.html.erb
Executable file
40
app/views/components/_footer.html.erb
Executable file
@@ -0,0 +1,40 @@
|
||||
<div class="grid gap-6 mb-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">À propos</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Je suis organisateur</a></li>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Pour les artistes</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Kit presse</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Artistes</a></li> %>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Villes</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Paris</a></li>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Lyon</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Marseille / Aix-en-Provence</a></li> %>
|
||||
<%# <li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Toulouse</a></li> %>
|
||||
</ul>
|
||||
</div>
|
||||
<%# <div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Organisateurs</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Help Center</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Contact Us</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Safety Guidelines</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Cancellation Policy</a></li>
|
||||
</ul>
|
||||
</div> %>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-white mb-3">Support</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Aide</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Nous contacter</a></li>
|
||||
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Signaler un contenu inapproprié</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-neutral-700 pt-4 text-center text-neutral-400 text-sm">
|
||||
<p>© 2025 Aperonight. All rights reserved. • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Privacy Policy</a> • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Terms of Service</a></p>
|
||||
</div>
|
||||
176
app/views/components/_header.html.erb
Normal file → Executable file
176
app/views/components/_header.html.erb
Normal file → Executable file
@@ -1,124 +1,108 @@
|
||||
<nav x-data="{ open: false }" class="bg-black border-b border-gray-100">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<header class="bg-neutral-800 border-b border-neutral-700">
|
||||
<nav data-controller="header" class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center h-16 justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<%= link_to "Aperonight", "/", class: "text-white text-xl font-bold" %>
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/",
|
||||
class: "text-xl font-bold text-white" %>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<!-- Parties -->
|
||||
<%= link_to "Soirées et afterworks", "#",
|
||||
class: "inline-flex
|
||||
items-center px-1 pt-1 border-b-2 border-transparent
|
||||
text-sm font-medium leading-5 text-gray-300
|
||||
hover:text-white hover:border-white focus:outline-none
|
||||
focus:text-white focus:border-white transition
|
||||
duration-150 ease-in-out" %>
|
||||
<!-- /Parties -->
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden sm:flex items-center space-x-6 w-full justify-start">
|
||||
<%= link_to "Afterworks", events_path,
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
|
||||
<%= link_to "Évenements", "#",
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
|
||||
<!-- Concerts -->
|
||||
<%= link_to "Concerts", "#",
|
||||
class: "inline-flex
|
||||
items-center px-1 pt-1 border-b-2 border-transparent
|
||||
text-sm font-medium leading-5 text-gray-300
|
||||
hover:text-white hover:border-white focus:outline-none
|
||||
focus:text-white focus:border-white transition
|
||||
duration-150 ease-in-out" %>
|
||||
<!-- /Parties -->
|
||||
</div>
|
||||
class: "mx-4 text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||
<div @click="open = ! open">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
|
||||
<div>Mon profil</div>
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<!-- Authentication -->
|
||||
<div class="hidden sm:flex items-center space-x-4">
|
||||
<% if user_signed_in? %>
|
||||
<div class="relative" data-header-target="userMenuButton">
|
||||
<button data-action="click->header#toggleUserMenu"
|
||||
class="bg-purple-700 text-white border border-purple-800 font-medium py-2 px-4 rounded-lg hover:bg-purple-800 transition-colors duration-200 flex items-center space-x-2">
|
||||
<span><%= current_user.email.length > 20 ? current_user.email[0,20] + "..." : current_user.email %></span>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown Menu -->
|
||||
<div data-header-target="userMenu" class="hidden absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
||||
<div class="px-4 py-2 text-sm text-gray-900 border-b border-gray-100">
|
||||
<div class="font-medium"><%= current_user.first_name || current_user.email %></div>
|
||||
<div class="text-gray-500"><%= current_user.email %></div>
|
||||
</div>
|
||||
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 py-1 bg-white">
|
||||
<%= link_to "Mon profil", "#", class: "block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out" %>
|
||||
<%= link_to "Mes réservations", "#", class: "block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out" %>
|
||||
|
||||
<!-- Logout -->
|
||||
<%= link_to "Déconnexion", destroy_user_session_path,
|
||||
data: {
|
||||
controller: "logout",
|
||||
action: "click->logout#signOut",
|
||||
logout_url_value: destroy_user_session_path,
|
||||
login_url_value: new_user_session_path,
|
||||
turbo: false
|
||||
},
|
||||
class: "inline-block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out" %>
|
||||
|
||||
|
||||
<%= link_to "Profile", edit_user_registration_path,
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
<%= link_to "Reservations", "#",
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
<div class="border-t border-gray-100">
|
||||
<%= link_to "Sign out", destroy_user_session_path,
|
||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to t("header.login"), new_user_session_path,
|
||||
class: "text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<%= link_to t("header.register"), new_user_registration_path,
|
||||
class: "bg-purple-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<!-- Mobile menu button -->
|
||||
<div class="flex-shrink-0 sm:hidden">
|
||||
<button data-action="click->header#toggleMobileMenu" data-header-target="mobileMenuButton" class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-purple-700">
|
||||
<svg data-menu-icon="open" class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg data-menu-icon="close" class="h-6 w-6 hidden" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div data-header-target="mobileMenu" class="hidden sm:hidden">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<%= link_to t("header.parties"), events_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.concerts"), "#",
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<%= link_to "Soirées et afterworks", "#", class: "block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-white hover:bg-gray-800 hover:border-white focus:outline-none focus:text-white focus:bg-gray-800 focus:border-white transition duration-150 ease-in-out" %>
|
||||
<%= link_to "Concerts", "#", class: "block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-white hover:bg-gray-800 hover:border-white focus:outline-none focus:text-white focus:bg-gray-800 focus:border-white transition duration-150 ease-in-out" %>
|
||||
<div class="pt-4 pb-3 border-t border-gray-700">
|
||||
<% if user_signed_in? %>
|
||||
<div class="px-4 mb-3">
|
||||
<div class="text-base font-medium text-white">
|
||||
<%= current_user.first_name || current_user.email %>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800">Test</div>
|
||||
<div class="font-medium text-sm text-gray-500">Test</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<%= link_to "Mon profil", "#", class: "block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-white hover:bg-gray-800 hover:border-white focus:outline-none focus:text-white focus:bg-gray-800 focus:border-white transition duration-150 ease-in-out" %>
|
||||
<%= link_to "Mes réservations", "#", class: "block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-white hover:bg-gray-800 hover:border-white focus:outline-none focus:text-white focus:bg-gray-800 focus:border-white transition duration-150 ease-in-out" %>
|
||||
|
||||
<!-- Logout -->
|
||||
<%= link_to "Déconnexion", destroy_user_session_path,
|
||||
data: {
|
||||
controller: "logout",
|
||||
action: "click->logout#signOut",
|
||||
logout_url_value: destroy_user_session_path,
|
||||
login_url_value: new_user_session_path,
|
||||
turbo: false
|
||||
},
|
||||
class: "block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out" %>
|
||||
|
||||
<div class="px-2 space-y-1">
|
||||
<%= link_to t("header.profile"), edit_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.reservations"), "#",
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.logout"), destroy_user_session_path,
|
||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-2 space-y-1">
|
||||
<%= link_to t("header.login"), new_user_session_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||
<%= link_to t("header.register"), new_user_registration_path,
|
||||
class: "block px-3 py-2 rounded-md text-base font-medium bg-purple-600 text-white hover:bg-purple-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user