Compare commits
10 Commits
ed659c423e
...
3c6389c6e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6389c6e2 | ||
|
|
2927a8344f | ||
|
|
ad97a36650 | ||
|
|
07a6694883 | ||
|
|
ba3f36a5e8 | ||
|
|
17e6711299 | ||
|
|
31534b2c0c | ||
|
|
87af76f078 | ||
|
|
9ec13c7d3d | ||
|
|
6a1f1cca9a |
81
README.md
81
README.md
@@ -1,24 +1,79 @@
|
||||
# README
|
||||
# Aperonight - Party Booking Platform
|
||||
|
||||
This README would normally document whatever steps are necessary to get the
|
||||
application up and running.
|
||||

|
||||
|
||||
Things you may want to cover:
|
||||
## 🌃 Overview
|
||||
|
||||
* Ruby version
|
||||
**Aperonight** is a two-sided marketplace connecting party-goers with nightlife promoters in Paris. The platform allows:
|
||||
|
||||
* System dependencies
|
||||
- **Customers** to discover/book tickets for upcoming parties
|
||||
- **Promoters** to create/manage events and validate tickets at venue entrances
|
||||
|
||||
* Configuration
|
||||
## 🎯 Key Features
|
||||
|
||||
* Database creation
|
||||
### 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
|
||||
|
||||
* Database initialization
|
||||
### 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
|
||||
|
||||
* How to run the test suite
|
||||
## 🛠 Technical Stack
|
||||
|
||||
* Services (job queues, cache servers, search engines, etc.)
|
||||
### Backend
|
||||
- **Ruby on Rails 7** (API mode)
|
||||
- **MariaDB** database
|
||||
<!--- **Redis** for caching/background jobs-->
|
||||
- **ActiveJob** for background processing
|
||||
|
||||
* Deployment instructions
|
||||
### Frontend
|
||||
- **Hotwire (Turbo + Stimulus)** for reactive UI
|
||||
- **Tailwind CSS** for styling
|
||||
- **React Native** for promoter mobile app (ticket scanning)
|
||||
|
||||
* ...
|
||||
### 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-->
|
||||
|
||||
## 📊 Database Schema (Simplified)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ BOOKING : makes
|
||||
USER {
|
||||
integer id
|
||||
string email
|
||||
string encrypted_password
|
||||
}
|
||||
PROMOTER ||--o{ PARTY : creates
|
||||
PROMOTER {
|
||||
integer id
|
||||
string stripe_account_id
|
||||
}
|
||||
PARTY ||--o{ TICKET_TYPE : has
|
||||
PARTY {
|
||||
integer id
|
||||
datetime start_time
|
||||
}
|
||||
BOOKING ||--o{ TICKET : generates
|
||||
BOOKING {
|
||||
integer id
|
||||
decimal total_price
|
||||
}
|
||||
TICKET_TYPE ||--o{ TICKET : defines
|
||||
TICKET_TYPE {
|
||||
integer id
|
||||
string name
|
||||
}
|
||||
TICKET {
|
||||
integer id
|
||||
string qr_code
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
/* Entry point for your PostCSS build */
|
||||
|
||||
/* Import Tailwind using PostCSS */
|
||||
@import "tailwindcss";
|
||||
|
||||
/** Default text color */
|
||||
body {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
30
app/controllers/authentications/confirmations_controller.rb
Normal file
30
app/controllers/authentications/confirmations_controller.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::ConfirmationsController < Devise::ConfirmationsController
|
||||
# GET /resource/confirmation/new
|
||||
# def new
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /resource/confirmation
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET /resource/confirmation?confirmation_token=abcdef
|
||||
# def show
|
||||
# super
|
||||
# end
|
||||
|
||||
# protected
|
||||
|
||||
# The path used after resending confirmation instructions.
|
||||
# def after_resending_confirmation_instructions_path_for(resource_name)
|
||||
# super(resource_name)
|
||||
# end
|
||||
|
||||
# The path used after confirmation.
|
||||
# def after_confirmation_path_for(resource_name, resource)
|
||||
# super(resource_name, resource)
|
||||
# end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
# You should configure your model like this:
|
||||
# devise :omniauthable, omniauth_providers: [:twitter]
|
||||
|
||||
# You should also create an action method in this controller like this:
|
||||
# def twitter
|
||||
# end
|
||||
|
||||
# More info at:
|
||||
# https://github.com/heartcombo/devise#omniauth
|
||||
|
||||
# GET|POST /resource/auth/twitter
|
||||
# def passthru
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET|POST /users/auth/twitter/callback
|
||||
# def failure
|
||||
# super
|
||||
# end
|
||||
|
||||
# protected
|
||||
|
||||
# The path used when OmniAuth fails
|
||||
# def after_omniauth_failure_path_for(scope)
|
||||
# super(scope)
|
||||
# end
|
||||
end
|
||||
34
app/controllers/authentications/passwords_controller.rb
Normal file
34
app/controllers/authentications/passwords_controller.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::PasswordsController < Devise::PasswordsController
|
||||
# GET /resource/password/new
|
||||
# def new
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /resource/password
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET /resource/password/edit?reset_password_token=abcdef
|
||||
# def edit
|
||||
# super
|
||||
# end
|
||||
|
||||
# PUT /resource/password
|
||||
# def update
|
||||
# super
|
||||
# end
|
||||
|
||||
# protected
|
||||
|
||||
# def after_resetting_password_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
|
||||
# The path used after sending reset password instructions
|
||||
# def after_sending_reset_password_instructions_path_for(resource_name)
|
||||
# super(resource_name)
|
||||
# end
|
||||
end
|
||||
62
app/controllers/authentications/registrations_controller.rb
Normal file
62
app/controllers/authentications/registrations_controller.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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]
|
||||
|
||||
# GET /resource/sign_up
|
||||
# def new
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /resource
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET /resource/edit
|
||||
# def edit
|
||||
# super
|
||||
# end
|
||||
|
||||
# PUT /resource
|
||||
# def update
|
||||
# super
|
||||
# end
|
||||
|
||||
# DELETE /resource
|
||||
# def destroy
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET /resource/cancel
|
||||
# Forces the session data which is usually expired after sign
|
||||
# in to be expired now. This is useful if the user wants to
|
||||
# cancel oauth signing in/up in the middle of the process,
|
||||
# removing all OAuth session data.
|
||||
# def cancel
|
||||
# super
|
||||
# end
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# The path used after sign up.
|
||||
# def after_sign_up_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
|
||||
# The path used after sign up for inactive accounts.
|
||||
# def after_inactive_sign_up_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
end
|
||||
27
app/controllers/authentications/sessions_controller.rb
Normal file
27
app/controllers/authentications/sessions_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::SessionsController < Devise::SessionsController
|
||||
# before_action :configure_sign_in_params, only: [:create]
|
||||
|
||||
# GET /resource/sign_in
|
||||
# def new
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /resource/sign_in
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# DELETE /resource/sign_out
|
||||
# def destroy
|
||||
# super
|
||||
# end
|
||||
|
||||
# protected
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
# def configure_sign_in_params
|
||||
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
|
||||
# end
|
||||
end
|
||||
30
app/controllers/authentications/unlocks_controller.rb
Normal file
30
app/controllers/authentications/unlocks_controller.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentications::UnlocksController < Devise::UnlocksController
|
||||
# GET /resource/unlock/new
|
||||
# def new
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /resource/unlock
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# GET /resource/unlock?unlock_token=abcdef
|
||||
# def show
|
||||
# super
|
||||
# end
|
||||
|
||||
# protected
|
||||
|
||||
# The path used after sending unlock password instructions
|
||||
# def after_sending_unlock_instructions_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
|
||||
# The path used after unlocking the resource
|
||||
# def after_unlock_path_for(resource)
|
||||
# super(resource)
|
||||
# end
|
||||
end
|
||||
55
app/javascript/components/button.jsx
Normal file
55
app/javascript/components/button.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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 }
|
||||
61
app/javascript/controllers/counter_controller.js
Normal file
61
app/javascript/controllers/counter_controller.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
target: Number,
|
||||
decimal: Boolean,
|
||||
duration: { type: Number, default: 2000 }
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.animate()
|
||||
this.observer.unobserve(this.element)
|
||||
}
|
||||
})
|
||||
}, { threshold: 0.5 })
|
||||
|
||||
this.observer.observe(this.element)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
const startValue = 0
|
||||
const startTime = performance.now()
|
||||
|
||||
const updateCounter = (currentTime) => {
|
||||
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)
|
||||
|
||||
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart
|
||||
|
||||
if (this.decimalValue && this.targetValue < 10) {
|
||||
currentValue = currentValue.toFixed(1)
|
||||
} else {
|
||||
currentValue = Math.floor(currentValue)
|
||||
}
|
||||
|
||||
this.element.textContent = currentValue
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter)
|
||||
} else {
|
||||
this.element.textContent = this.decimalValue && this.targetValue < 10
|
||||
? this.targetValue.toFixed(1)
|
||||
: this.targetValue
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = "Hello World!"
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,8 @@
|
||||
|
||||
import { application } from "./application"
|
||||
|
||||
import HelloController from "./hello_controller"
|
||||
application.register("hello", HelloController)
|
||||
import ShadcnTestController from "./shadcn_test_controller"
|
||||
import CounterController from "./counter_controller"
|
||||
|
||||
application.register("shadcn-test", ShadcnTestController)
|
||||
application.register("counter", CounterController)
|
||||
|
||||
39
app/javascript/controllers/shadcn_test_controller.js
Normal file
39
app/javascript/controllers/shadcn_test_controller.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
6
app/javascript/lib/utils.js
Normal file
6
app/javascript/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
6
app/models/user.rb
Normal file
6
app/models/user.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class User < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable
|
||||
end
|
||||
124
app/views/components/_header.html.erb
Normal file
124
app/views/components/_header.html.erb
Normal file
@@ -0,0 +1,124 @@
|
||||
<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">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<%= link_to Rails.application.config.app_name, "/", class: "text-white text-xl font-bold" %>
|
||||
</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 -->
|
||||
|
||||
<!-- 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>
|
||||
</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">
|
||||
<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>
|
||||
</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" %>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
16
app/views/devise/confirmations/new.html.erb
Normal file
16
app/views/devise/confirmations/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Resend confirmation instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Resend confirmation instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Welcome <%= @email %>!</p>
|
||||
|
||||
<p>You can confirm your account email through the link below:</p>
|
||||
|
||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||
7
app/views/devise/mailer/email_changed.html.erb
Normal file
7
app/views/devise/mailer/email_changed.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @email %>!</p>
|
||||
|
||||
<% if @resource.try(:unconfirmed_email?) %>
|
||||
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
|
||||
<% else %>
|
||||
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
|
||||
<% end %>
|
||||
3
app/views/devise/mailer/password_change.html.erb
Normal file
3
app/views/devise/mailer/password_change.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>We're contacting you to notify you that your password has been changed.</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
||||
|
||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
||||
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
|
||||
|
||||
<p>Click the link below to unlock your account:</p>
|
||||
|
||||
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
|
||||
25
app/views/devise/passwords/edit.html.erb
Normal file
25
app/views/devise/passwords/edit.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<h2>Change your password</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password, "New password" %><br />
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
|
||||
<% end %>
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation, "Confirm new password" %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Change my password" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
16
app/views/devise/passwords/new.html.erb
Normal file
16
app/views/devise/passwords/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Forgot your password?</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Send me reset password instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
43
app/views/devise/registrations/edit.html.erb
Normal file
43
app/views/devise/registrations/edit.html.erb
Normal file
@@ -0,0 +1,43 @@
|
||||
<h2>Edit <%= resource_name.to_s.humanize %></h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
|
||||
<%= f.password_field :password, autocomplete: "new-password" %>
|
||||
<% if @minimum_password_length %>
|
||||
<br />
|
||||
<em><%= @minimum_password_length %> characters minimum</em>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
|
||||
<%= f.password_field :current_password, autocomplete: "current-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Update" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h3>Cancel my account</h3>
|
||||
|
||||
<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div>
|
||||
|
||||
<%= link_to "Back", :back %>
|
||||
29
app/views/devise/registrations/new.html.erb
Normal file
29
app/views/devise/registrations/new.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
<h2>Sign up</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %>
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em>
|
||||
<% end %><br />
|
||||
<%= f.password_field :password, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Sign up" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
26
app/views/devise/sessions/new.html.erb
Normal file
26
app/views/devise/sessions/new.html.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<h2>Log in</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %><br />
|
||||
<%= f.password_field :password, autocomplete: "current-password" %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="field">
|
||||
<%= f.check_box :remember_me %>
|
||||
<%= f.label :remember_me %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Log in" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
15
app/views/devise/shared/_error_messages.html.erb
Normal file
15
app/views/devise/shared/_error_messages.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<% if resource.errors.any? %>
|
||||
<div id="error_explanation" data-turbo-cache="false">
|
||||
<h2>
|
||||
<%= I18n.t("errors.messages.not_saved",
|
||||
count: resource.errors.count,
|
||||
resource: resource.class.model_name.human.downcase)
|
||||
%>
|
||||
</h2>
|
||||
<ul>
|
||||
<% resource.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
25
app/views/devise/shared/_links.html.erb
Normal file
25
app/views/devise/shared/_links.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<%- if controller_name != 'sessions' %>
|
||||
<%= link_to "Log in", new_session_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.omniauthable? %>
|
||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
||||
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br />
|
||||
<% end %>
|
||||
<% end %>
|
||||
16
app/views/devise/unlocks/new.html.erb
Normal file
16
app/views/devise/unlocks/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Resend unlock instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Resend unlock instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
@@ -23,6 +23,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="header">
|
||||
<%= render "components/header" %>
|
||||
</div><!-- /#header -->
|
||||
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,2 +1,288 @@
|
||||
<h1>Pages#home</h1>
|
||||
<p>Find me in app/views/pages/home.html.erb</p>
|
||||
<!-- Hero Section -->
|
||||
<section class="relative bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-700 min-h-[70vh] flex items-center">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-5xl md:text-7xl font-bold text-white mb-6 leading-tight">
|
||||
Découvrez les afterworks et soirée
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400">
|
||||
à Paris
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-gray-200 mb-8 max-w-3xl mx-auto leading-relaxed">
|
||||
Les meilleures soirées, concerts et afterworks de Paris en un clic. Réservez vos places et vivez des expériences uniques.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<%= link_to "Explorer les soirées", "#events", class: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg" %>
|
||||
<%= link_to "Voir les concerts", "#", class: "bg-white bg-opacity-10 backdrop-blur-sm border border-white border-opacity-30 hover:bg-opacity-20 text-black font-semibold py-4 px-8 rounded-full transition-all duration-300" %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metrics -->
|
||||
<section class="bg-gradient-to-br from-gray-900 via-gray-800 to-black py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
Des chiffres qui parlent
|
||||
</h2>
|
||||
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
La plateforme préférée des Parisiens pour vivre la nuit
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
||||
<!-- Total Events -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-gray-800/60 backdrop-blur-sm border border-gray-700/50 hover:border-purple-500/50 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-600/20 to-indigo-600/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-400 via-indigo-400 to-pink-400 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="127">0</span>
|
||||
</div>
|
||||
<p class="text-gray-200 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Événements organisés
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Users -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-gray-800/60 backdrop-blur-sm border border-gray-700/50 hover:border-purple-500/50 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-600/20 to-indigo-600/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-400 via-indigo-400 to-pink-400 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="1433">0</span>+
|
||||
</div>
|
||||
<p class="text-gray-200 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Membres actifs
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Rating -->
|
||||
<div class="group relative">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-gray-800/60 backdrop-blur-sm border border-gray-700/50 hover:border-purple-500/50 transition-all duration-300 p-8">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-600/20 to-indigo-600/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative">
|
||||
<div class="text-5xl md:text-6xl font-light bg-gradient-to-r from-purple-400 via-indigo-400 to-pink-400 bg-clip-text text-transparent mb-3">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="4.4" data-counter-decimal-value="true">0</span>/5
|
||||
</div>
|
||||
<p class="text-gray-200 font-mono uppercase tracking-widest text-sm font-medium">
|
||||
Note moyenne des soirées
|
||||
</p>
|
||||
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Stats Row -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mt-12">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-300">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="79">0</span>%
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm font-mono uppercase tracking-wide font-medium">Taux de remplissage</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-300">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="12">0</span>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm font-mono uppercase tracking-wide font-medium">Arrondissements</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-300">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="156">0</span>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm font-mono uppercase tracking-wide font-medium">Établissements partenaires</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-300">
|
||||
<span class="counter" data-controller="counter" data-counter-target-value="98">0</span>%
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm font-mono uppercase tracking-wide font-medium">Satisfaction client</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Search Section -->
|
||||
<section id="search" class="bg-gray-900 py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl p-8 shadow-2xl">
|
||||
<h2 class="text-3xl font-bold text-white text-center mb-8">Trouvez votre prochaine soirée</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Quand ?</label>
|
||||
<input type="date" class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all" placeholder="Choisir une date">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type d'événement</label>
|
||||
<select class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all">
|
||||
<option value="">Tous les types</option>
|
||||
<option value="club">Soirées club</option>
|
||||
<option value="afterwork">Afterworks</option>
|
||||
<option value="concert">Concerts</option>
|
||||
<option value="vip">Événements VIP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Genre musical</label>
|
||||
<select class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all">
|
||||
<option value="">Tous les genres</option>
|
||||
<option value="house">House/Techno</option>
|
||||
<option value="hiphop">Hip-Hop</option>
|
||||
<option value="pop">Pop</option>
|
||||
<option value="rock">Rock</option>
|
||||
<option value="electro">Électro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105">
|
||||
Rechercher
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Events -->
|
||||
<section id="events" class="bg-gradient-to-b from-gray-900 to-black py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold text-white mb-4">Événements du moment</h2>
|
||||
<p class="text-xl text-gray-400">Les soirées et concerts les plus populaires cette semaine</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Event Card 1 -->
|
||||
<div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-xl">
|
||||
<div class="h-56 bg-gradient-to-br from-purple-500 via-pink-500 to-red-500 relative">
|
||||
<div class="absolute top-4 right-4 bg-black bg-opacity-60 text-white px-3 py-1 rounded-full text-sm font-medium">
|
||||
Club
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h3 class="text-2xl font-bold text-white">TECHNO NIGHT</h3>
|
||||
<span class="text-purple-400 font-semibold">25€</span>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-2">Rex Club, Paris 2ème</p>
|
||||
<p class="text-gray-300 mb-4">Vendredi 22h • Soirée techno underground</p>
|
||||
<button class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 rounded-lg transition-all duration-300">
|
||||
Réserver ma place
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Card 2 -->
|
||||
<div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-xl">
|
||||
<div class="h-56 bg-gradient-to-br from-blue-500 via-cyan-500 to-teal-500 relative">
|
||||
<div class="absolute top-4 right-4 bg-black bg-opacity-60 text-white px-3 py-1 rounded-full text-sm font-medium">
|
||||
Afterwork
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h3 class="text-2xl font-bold text-white">SUNSET APÉRO</h3>
|
||||
<span class="text-blue-400 font-semibold">15€</span>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-2">Nuba, Paris 13ème</p>
|
||||
<p class="text-gray-300 mb-4">Jeudi 18h • Apéro sur les quais</p>
|
||||
<button class="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white font-semibold py-3 rounded-lg transition-all duration-300">
|
||||
Réserver ma place
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Card 3 -->
|
||||
<div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-xl">
|
||||
<div class="h-56 bg-gradient-to-br from-green-500 via-emerald-500 to-teal-500 relative">
|
||||
<div class="absolute top-4 right-4 bg-black bg-opacity-60 text-white px-3 py-1 rounded-full text-sm font-medium">
|
||||
Concert
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h3 class="text-2xl font-bold text-white">LIVE SESSION</h3>
|
||||
<span class="text-green-400 font-semibold">20€</span>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-2">La Bellevilloise, Paris 20ème</p>
|
||||
<p class="text-gray-300 mb-4">Samedi 20h • Concert live acoustique</p>
|
||||
<button class="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-semibold py-3 rounded-lg transition-all duration-300">
|
||||
Réserver ma place
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<button class="text-purple-400 hover:text-purple-300 text-lg font-medium transition-all duration-300 border-b-2 border-purple-400 hover:border-purple-300">
|
||||
Voir tous les événements →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="bg-gray-900 py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl font-bold text-white mb-4">Pourquoi choisir <%= Rails.application.config.app_name %> ?</h2>
|
||||
<p class="text-xl text-gray-400 max-w-2xl mx-auto">La plateforme préférée des Parisiens pour sortir</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-white mb-3">Découverte facile</h3>
|
||||
<p class="text-gray-400 leading-relaxed">Trouvez les meilleures soirées et concerts de Paris en quelques clics grâce à notre algorithme personnalisé</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-white mb-3">Réservation sécurisée</h3>
|
||||
<p class="text-gray-400 leading-relaxed">Paiement 100% sécurisé et billets électroniques avec QR code sur votre mobile</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-8">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-white mb-3">Accès rapide</h3>
|
||||
<p class="text-gray-400 leading-relaxed">Entrée express avec validation mobile de vos billets. Plus besoin d'imprimer !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="bg-gradient-to-r from-purple-900 via-indigo-900 to-pink-900 py-20">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-4xl font-bold text-white mb-6">Prêt à vivre la nuit parisienne ?</h2>
|
||||
<p class="text-xl text-gray-300 mb-8">Rejoignez des milliers de party-goers qui utilisent Aperonight chaque semaine</p>
|
||||
<button class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full text-lg transition-all duration-300 transform hover:scale-105 shadow-xl">
|
||||
S'inscrire gratuitement
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
17
components.json
Normal file
17
components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/assets/stylesheets/application.postcss.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
1
config/initializers/app_config.rb
Normal file
1
config/initializers/app_config.rb
Normal file
@@ -0,0 +1 @@
|
||||
Rails.application.config.app_name = ENV.fetch("APP_NAME", "Aperonight")
|
||||
313
config/initializers/devise.rb
Normal file
313
config/initializers/devise.rb
Normal file
@@ -0,0 +1,313 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Assuming you have not yet modified this file, each configuration option below
|
||||
# is set to its default value. Note that some are commented out while others
|
||||
# are not: uncommented lines are intended to protect your configuration from
|
||||
# breaking changes in upgrades (i.e., in the event that future versions of
|
||||
# Devise change the default values for those options).
|
||||
#
|
||||
# Use this hook to configure devise mailer, warden hooks and so forth.
|
||||
# Many of these configuration options can be set straight in your model.
|
||||
Devise.setup do |config|
|
||||
# The secret key used by Devise. Devise uses this key to generate
|
||||
# random tokens. Changing this key will render invalid all existing
|
||||
# confirmation, reset password and unlock tokens in the database.
|
||||
# Devise will use the `secret_key_base` as its `secret_key`
|
||||
# by default. You can change it below and use your own secret key.
|
||||
# config.secret_key = '6288061ff5c4779088cf3375201dddbb983dbfb0373937db933c188ba8044e035d822b5280d11d135eea86edb16189ea9ed0cee915709af1590cf94f68ad814d'
|
||||
|
||||
# ==> Controller configuration
|
||||
# Configure the parent class to the devise controllers.
|
||||
# config.parent_controller = 'DeviseController'
|
||||
|
||||
# ==> Mailer Configuration
|
||||
# Configure the e-mail address which will be shown in Devise::Mailer,
|
||||
# note that it will be overwritten if you use your own mailer class
|
||||
# with default "from" parameter.
|
||||
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
|
||||
|
||||
# Configure the class responsible to send e-mails.
|
||||
# config.mailer = 'Devise::Mailer'
|
||||
|
||||
# Configure the parent class responsible to send e-mails.
|
||||
# config.parent_mailer = 'ActionMailer::Base'
|
||||
|
||||
# ==> ORM configuration
|
||||
# Load and configure the ORM. Supports :active_record (default) and
|
||||
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
||||
# available as additional gems.
|
||||
require 'devise/orm/active_record'
|
||||
|
||||
# ==> Configuration for any authentication mechanism
|
||||
# Configure which keys are used when authenticating a user. The default is
|
||||
# just :email. You can configure it to use [:username, :subdomain], so for
|
||||
# authenticating a user, both parameters are required. Remember that those
|
||||
# parameters are used only when authenticating and not when retrieving from
|
||||
# session. If you need permissions, you should implement that in a before filter.
|
||||
# You can also supply a hash where the value is a boolean determining whether
|
||||
# or not authentication should be aborted when the value is not present.
|
||||
# config.authentication_keys = [:email]
|
||||
|
||||
# Configure parameters from the request object used for authentication. Each entry
|
||||
# given should be a request method and it will automatically be passed to the
|
||||
# find_for_authentication method and considered in your model lookup. For instance,
|
||||
# if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
|
||||
# The same considerations mentioned for authentication_keys also apply to request_keys.
|
||||
# config.request_keys = []
|
||||
|
||||
# Configure which authentication keys should be case-insensitive.
|
||||
# These keys will be downcased upon creating or modifying a user and when used
|
||||
# to authenticate or find a user. Default is :email.
|
||||
config.case_insensitive_keys = [:email]
|
||||
|
||||
# Configure which authentication keys should have whitespace stripped.
|
||||
# These keys will have whitespace before and after removed upon creating or
|
||||
# modifying a user and when used to authenticate or find a user. Default is :email.
|
||||
config.strip_whitespace_keys = [:email]
|
||||
|
||||
# Tell if authentication through request.params is enabled. True by default.
|
||||
# It can be set to an array that will enable params authentication only for the
|
||||
# given strategies, for example, `config.params_authenticatable = [:database]` will
|
||||
# enable it only for database (email + password) authentication.
|
||||
# config.params_authenticatable = true
|
||||
|
||||
# Tell if authentication through HTTP Auth is enabled. False by default.
|
||||
# It can be set to an array that will enable http authentication only for the
|
||||
# given strategies, for example, `config.http_authenticatable = [:database]` will
|
||||
# enable it only for database authentication.
|
||||
# For API-only applications to support authentication "out-of-the-box", you will likely want to
|
||||
# enable this with :database unless you are using a custom strategy.
|
||||
# The supported strategies are:
|
||||
# :database = Support basic authentication with authentication key + password
|
||||
# config.http_authenticatable = false
|
||||
|
||||
# If 401 status code should be returned for AJAX requests. True by default.
|
||||
# config.http_authenticatable_on_xhr = true
|
||||
|
||||
# The realm used in Http Basic Authentication. 'Application' by default.
|
||||
# config.http_authentication_realm = 'Application'
|
||||
|
||||
# It will change confirmation, password recovery and other workflows
|
||||
# to behave the same regardless if the e-mail provided was right or wrong.
|
||||
# Does not affect registerable.
|
||||
# config.paranoid = true
|
||||
|
||||
# By default Devise will store the user in session. You can skip storage for
|
||||
# particular strategies by setting this option.
|
||||
# Notice that if you are skipping storage for all authentication paths, you
|
||||
# may want to disable generating routes to Devise's sessions controller by
|
||||
# passing skip: :sessions to `devise_for` in your config/routes.rb
|
||||
config.skip_session_storage = [:http_auth]
|
||||
|
||||
# By default, Devise cleans up the CSRF token on authentication to
|
||||
# avoid CSRF token fixation attacks. This means that, when using AJAX
|
||||
# requests for sign in and sign up, you need to get a new CSRF token
|
||||
# from the server. You can disable this option at your own risk.
|
||||
# config.clean_up_csrf_token_on_authentication = true
|
||||
|
||||
# When false, Devise will not attempt to reload routes on eager load.
|
||||
# This can reduce the time taken to boot the app but if your application
|
||||
# requires the Devise mappings to be loaded during boot time the application
|
||||
# won't boot properly.
|
||||
# config.reload_routes = true
|
||||
|
||||
# ==> Configuration for :database_authenticatable
|
||||
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
||||
# using other algorithms, it sets how many times you want the password to be hashed.
|
||||
# The number of stretches used for generating the hashed password are stored
|
||||
# with the hashed password. This allows you to change the stretches without
|
||||
# invalidating existing passwords.
|
||||
#
|
||||
# Limiting the stretches to just one in testing will increase the performance of
|
||||
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
|
||||
# a value less than 10 in other environments. Note that, for bcrypt (the default
|
||||
# algorithm), the cost increases exponentially with the number of stretches (e.g.
|
||||
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
|
||||
config.stretches = Rails.env.test? ? 1 : 12
|
||||
|
||||
# Set up a pepper to generate the hashed password.
|
||||
# config.pepper = 'f3f90e184a65dc680c9b177c6728d07f011476e10404ecaca8d93ef13b82fb43dc2cc119b5bd4330ec27a3a3b8cdea6f21f0ac21d0dfafff33d3be8b6e5d45da'
|
||||
|
||||
# Send a notification to the original email when the user's email is changed.
|
||||
# config.send_email_changed_notification = false
|
||||
|
||||
# Send a notification email when the user's password is changed.
|
||||
# config.send_password_change_notification = false
|
||||
|
||||
# ==> Configuration for :confirmable
|
||||
# A period that the user is allowed to access the website even without
|
||||
# confirming their account. For instance, if set to 2.days, the user will be
|
||||
# able to access the website for two days without confirming their account,
|
||||
# access will be blocked just in the third day.
|
||||
# You can also set it to nil, which will allow the user to access the website
|
||||
# without confirming their account.
|
||||
# Default is 0.days, meaning the user cannot access the website without
|
||||
# confirming their account.
|
||||
# config.allow_unconfirmed_access_for = 2.days
|
||||
|
||||
# A period that the user is allowed to confirm their account before their
|
||||
# token becomes invalid. For example, if set to 3.days, the user can confirm
|
||||
# their account within 3 days after the mail was sent, but on the fourth day
|
||||
# their account can't be confirmed with the token any more.
|
||||
# Default is nil, meaning there is no restriction on how long a user can take
|
||||
# before confirming their account.
|
||||
# config.confirm_within = 3.days
|
||||
|
||||
# If true, requires any email changes to be confirmed (exactly the same way as
|
||||
# initial account confirmation) to be applied. Requires additional unconfirmed_email
|
||||
# db field (see migrations). Until confirmed, new email is stored in
|
||||
# unconfirmed_email column, and copied to email column on successful confirmation.
|
||||
config.reconfirmable = true
|
||||
|
||||
# Defines which key will be used when confirming an account
|
||||
# config.confirmation_keys = [:email]
|
||||
|
||||
# ==> Configuration for :rememberable
|
||||
# The time the user will be remembered without asking for credentials again.
|
||||
# config.remember_for = 2.weeks
|
||||
|
||||
# Invalidates all the remember me tokens when the user signs out.
|
||||
config.expire_all_remember_me_on_sign_out = true
|
||||
|
||||
# If true, extends the user's remember period when remembered via cookie.
|
||||
# config.extend_remember_period = false
|
||||
|
||||
# Options to be passed to the created cookie. For instance, you can set
|
||||
# secure: true in order to force SSL only cookies.
|
||||
# config.rememberable_options = {}
|
||||
|
||||
# ==> Configuration for :validatable
|
||||
# Range for password length.
|
||||
config.password_length = 6..128
|
||||
|
||||
# Email regex used to validate email formats. It simply asserts that
|
||||
# one (and only one) @ exists in the given string. This is mainly
|
||||
# to give user feedback and not to assert the e-mail validity.
|
||||
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
|
||||
|
||||
# ==> Configuration for :timeoutable
|
||||
# The time you want to timeout the user session without activity. After this
|
||||
# time the user will be asked for credentials again. Default is 30 minutes.
|
||||
# config.timeout_in = 30.minutes
|
||||
|
||||
# ==> Configuration for :lockable
|
||||
# Defines which strategy will be used to lock an account.
|
||||
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
|
||||
# :none = No lock strategy. You should handle locking by yourself.
|
||||
# config.lock_strategy = :failed_attempts
|
||||
|
||||
# Defines which key will be used when locking and unlocking an account
|
||||
# config.unlock_keys = [:email]
|
||||
|
||||
# Defines which strategy will be used to unlock an account.
|
||||
# :email = Sends an unlock link to the user email
|
||||
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
|
||||
# :both = Enables both strategies
|
||||
# :none = No unlock strategy. You should handle unlocking by yourself.
|
||||
# config.unlock_strategy = :both
|
||||
|
||||
# Number of authentication tries before locking an account if lock_strategy
|
||||
# is failed attempts.
|
||||
# config.maximum_attempts = 20
|
||||
|
||||
# Time interval to unlock the account if :time is enabled as unlock_strategy.
|
||||
# config.unlock_in = 1.hour
|
||||
|
||||
# Warn on the last attempt before the account is locked.
|
||||
# config.last_attempt_warning = true
|
||||
|
||||
# ==> Configuration for :recoverable
|
||||
#
|
||||
# Defines which key will be used when recovering the password for an account
|
||||
# config.reset_password_keys = [:email]
|
||||
|
||||
# Time interval you can reset your password with a reset password key.
|
||||
# Don't put a too small interval or your users won't have the time to
|
||||
# change their passwords.
|
||||
config.reset_password_within = 6.hours
|
||||
|
||||
# When set to false, does not sign a user in automatically after their password is
|
||||
# reset. Defaults to true, so a user is signed in automatically after a reset.
|
||||
# config.sign_in_after_reset_password = true
|
||||
|
||||
# ==> Configuration for :encryptable
|
||||
# Allow you to use another hashing or encryption algorithm besides bcrypt (default).
|
||||
# You can use :sha1, :sha512 or algorithms from others authentication tools as
|
||||
# :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
|
||||
# for default behavior) and :restful_authentication_sha1 (then you should set
|
||||
# stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
|
||||
#
|
||||
# Require the `devise-encryptable` gem when using anything other than bcrypt
|
||||
# config.encryptor = :sha512
|
||||
|
||||
# ==> Scopes configuration
|
||||
# Turn scoped views on. Before rendering "sessions/new", it will first check for
|
||||
# "users/sessions/new". It's turned off by default because it's slower if you
|
||||
# are using only default views.
|
||||
# config.scoped_views = false
|
||||
|
||||
# Configure the default scope given to Warden. By default it's the first
|
||||
# devise role declared in your routes (usually :user).
|
||||
# config.default_scope = :user
|
||||
|
||||
# Set this configuration to false if you want /users/sign_out to sign out
|
||||
# only the current scope. By default, Devise signs out all scopes.
|
||||
# config.sign_out_all_scopes = true
|
||||
|
||||
# ==> Navigation configuration
|
||||
# Lists the formats that should be treated as navigational. Formats like
|
||||
# :html should redirect to the sign in page when the user does not have
|
||||
# access, but formats like :xml or :json, should return 401.
|
||||
#
|
||||
# If you have any extra navigational formats, like :iphone or :mobile, you
|
||||
# should add them to the navigational formats lists.
|
||||
#
|
||||
# The "*/*" below is required to match Internet Explorer requests.
|
||||
# config.navigational_formats = ['*/*', :html, :turbo_stream]
|
||||
|
||||
# The default HTTP method used to sign out a resource. Default is :delete.
|
||||
config.sign_out_via = :delete
|
||||
|
||||
# ==> OmniAuth
|
||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
||||
# up on your models and hooks.
|
||||
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
|
||||
|
||||
# ==> Warden configuration
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
# change the failure app, you can configure them inside the config.warden block.
|
||||
#
|
||||
# config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
||||
# end
|
||||
|
||||
# ==> Mountable engine configurations
|
||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||
# is mountable, there are some extra configurations to be taken into account.
|
||||
# The following options are available, assuming the engine is mounted as:
|
||||
#
|
||||
# mount MyEngine, at: '/my_engine'
|
||||
#
|
||||
# The router that invoked `devise_for`, in the example above, would be:
|
||||
# config.router_name = :my_engine
|
||||
#
|
||||
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
|
||||
# so you need to do it manually. For the users scope, it would be:
|
||||
# config.omniauth_path_prefix = '/my_engine/users/auth'
|
||||
|
||||
# ==> Hotwire/Turbo configuration
|
||||
# When using Devise with Hotwire/Turbo, the http status for error responses
|
||||
# and some redirects must match the following. The default in Devise for existing
|
||||
# apps is `200 OK` and `302 Found` respectively, but new apps are generated with
|
||||
# these new defaults that match Hotwire/Turbo behavior.
|
||||
# Note: These might become the new default in future versions of Devise.
|
||||
config.responder.error_status = :unprocessable_entity
|
||||
config.responder.redirect_status = :see_other
|
||||
|
||||
# ==> Configuration for :registerable
|
||||
|
||||
# When set to false, does not sign a user in automatically after their password is
|
||||
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
||||
# config.sign_in_after_change_password = true
|
||||
end
|
||||
65
config/locales/devise.en.yml
Normal file
65
config/locales/devise.en.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
# Additional translations at https://github.com/heartcombo/devise/wiki/I18n
|
||||
|
||||
en:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: "Your email address has been successfully confirmed."
|
||||
send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
|
||||
send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
|
||||
failure:
|
||||
already_authenticated: "You are already signed in."
|
||||
inactive: "Your account is not activated yet."
|
||||
invalid: "Invalid %{authentication_keys} or password."
|
||||
locked: "Your account is locked."
|
||||
last_attempt: "You have one more attempt before your account is locked."
|
||||
not_found_in_database: "Invalid %{authentication_keys} or password."
|
||||
timeout: "Your session expired. Please sign in again to continue."
|
||||
unauthenticated: "You need to sign in or sign up before continuing."
|
||||
unconfirmed: "You have to confirm your email address before continuing."
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Confirmation instructions"
|
||||
reset_password_instructions:
|
||||
subject: "Reset password instructions"
|
||||
unlock_instructions:
|
||||
subject: "Unlock instructions"
|
||||
email_changed:
|
||||
subject: "Email Changed"
|
||||
password_change:
|
||||
subject: "Password Changed"
|
||||
omniauth_callbacks:
|
||||
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
|
||||
success: "Successfully authenticated from %{kind} account."
|
||||
passwords:
|
||||
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
|
||||
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
|
||||
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
|
||||
updated: "Your password has been changed successfully. You are now signed in."
|
||||
updated_not_active: "Your password has been changed successfully."
|
||||
registrations:
|
||||
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
|
||||
signed_up: "Welcome! You have signed up successfully."
|
||||
signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
|
||||
signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
|
||||
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
|
||||
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
|
||||
updated: "Your account has been updated successfully."
|
||||
updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again."
|
||||
sessions:
|
||||
signed_in: "Signed in successfully."
|
||||
signed_out: "Signed out successfully."
|
||||
already_signed_out: "Signed out successfully."
|
||||
unlocks:
|
||||
send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
|
||||
send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
|
||||
unlocked: "Your account has been unlocked successfully. Please sign in to continue."
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: "was already confirmed, please try signing in"
|
||||
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
|
||||
expired: "has expired, please request a new one"
|
||||
not_found: "not found"
|
||||
not_locked: "was not locked"
|
||||
not_saved:
|
||||
one: "1 error prohibited this %{resource} from being saved:"
|
||||
other: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
@@ -11,4 +11,24 @@ Rails.application.routes.draw do
|
||||
|
||||
# Defines the root path route ("/")
|
||||
root "pages#home"
|
||||
|
||||
# Routes for devise authentication Gem
|
||||
# Bind devise to user
|
||||
# devise_for :users
|
||||
devise_for :users, path: "authentications", path_names: {
|
||||
sign_up: "register", # Route for user registration
|
||||
sign_in: "login", # Route for user login
|
||||
sign_out: "logout", # Route for user logout
|
||||
password: "reset-password", # Route for changing password
|
||||
confirmation: "verification", # Route for account confirmation
|
||||
unlock: "unblock", # Route for account unlock
|
||||
registration: "register" # Route for user registration (redundant with sign_up)
|
||||
},
|
||||
controllers: {
|
||||
sessions: "authentications/sessions", # Custom controller for sessions
|
||||
registrations: "authentications/registrations", # Custom controller for registrations
|
||||
passwords: "authentications/passwords", # Custom controller for passwords
|
||||
confirmation: "authentications/confirmations" # Custom controller for confirmations
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
44
db/migrate/20250816145933_devise_create_users.rb
Normal file
44
db/migrate/20250816145933_devise_create_users.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DeviseCreateUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :users do |t|
|
||||
## Database authenticatable
|
||||
t.string :email, null: false, default: ""
|
||||
t.string :encrypted_password, null: false, default: ""
|
||||
|
||||
## Recoverable
|
||||
t.string :reset_password_token
|
||||
t.datetime :reset_password_sent_at
|
||||
|
||||
## Rememberable
|
||||
t.datetime :remember_created_at
|
||||
|
||||
## Trackable
|
||||
# t.integer :sign_in_count, default: 0, null: false
|
||||
# t.datetime :current_sign_in_at
|
||||
# t.datetime :last_sign_in_at
|
||||
# t.string :current_sign_in_ip
|
||||
# t.string :last_sign_in_ip
|
||||
|
||||
## Confirmable
|
||||
# t.string :confirmation_token
|
||||
# t.datetime :confirmed_at
|
||||
# t.datetime :confirmation_sent_at
|
||||
# t.string :unconfirmed_email # Only if using reconfirmable
|
||||
|
||||
## Lockable
|
||||
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
|
||||
# t.string :unlock_token # Only if unlock strategy is :email or :both
|
||||
# t.datetime :locked_at
|
||||
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_index :users, :email, unique: true
|
||||
add_index :users, :reset_password_token, unique: true
|
||||
# add_index :users, :confirmation_token, unique: true
|
||||
# add_index :users, :unlock_token, unique: true
|
||||
end
|
||||
end
|
||||
25
db/schema.rb
generated
Normal file
25
db/schema.rb
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_08_16_145933) do
|
||||
create_table "users", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "reset_password_token"
|
||||
t.datetime "reset_password_sent_at"
|
||||
t.datetime "remember_created_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
end
|
||||
end
|
||||
157
docs/application-optimization.md
Normal file
157
docs/application-optimization.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Application.js Size Optimization Guide
|
||||
|
||||
## Current Issue
|
||||
The `application.js` bundle is 1.4MB (2.3MB with source maps), which is significantly larger than recommended.
|
||||
|
||||
## Root Causes
|
||||
1. **Single bundle includes everything**: All dependencies, React, controllers, and components
|
||||
2. **No code splitting**: Everything is bundled into one file
|
||||
3. **Development dependencies**: Alpine.js and other dev tools included
|
||||
4. **No minification/optimization**: Source maps and uncompressed code
|
||||
|
||||
## Optimization Strategies
|
||||
|
||||
### 1. Split Bundles (Recommended)
|
||||
Create separate bundles for different parts of the application:
|
||||
|
||||
**Update package.json build scripts:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "npm run build:main && npm run build:components",
|
||||
"build:main": "esbuild app/javascript/application.js --bundle --minify --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
|
||||
"build:components": "esbuild app/javascript/components/*.* --bundle --minify --format=esm --outdir=app/assets/builds/components --public-path=/assets --loader:.js=jsx",
|
||||
"build:css": "postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Remove Unused Dependencies
|
||||
**package.json optimization:**
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"esbuild": "^0.25.4",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Remove these from devDependencies:**
|
||||
- `alpinejs` - if not used
|
||||
- `@types/alpinejs` - if Alpine.js removed
|
||||
- `cssnano` - if using Tailwind's built-in minification
|
||||
- `pm2` - production deployment tool
|
||||
|
||||
### 3. Dynamic Imports (Code Splitting)
|
||||
**Update application.js:**
|
||||
```javascript
|
||||
// Instead of importing everything statically
|
||||
import "@hotwired/turbo-rails"
|
||||
import "./controllers"
|
||||
|
||||
// Use dynamic imports for heavy components
|
||||
const loadComponent = async (componentName) => {
|
||||
const { default: component } = await import(`./components/${componentName}`)
|
||||
return component
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Tree Shaking & Minification
|
||||
**Enhanced build command:**
|
||||
```json
|
||||
{
|
||||
"build": "esbuild app/javascript/application.js --bundle --minify --tree-shaking --drop:console --drop:debugger --sourcemap=external --format=esm --outdir=app/assets/builds --public-path=/assets"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Separate Vendor Bundle
|
||||
**Create vendor.js:**
|
||||
```javascript
|
||||
// app/javascript/vendor.js
|
||||
import "react"
|
||||
import "react-dom"
|
||||
import "@radix-ui/react-slot"
|
||||
```
|
||||
|
||||
**Update build to create vendor bundle:**
|
||||
```json
|
||||
{
|
||||
"build:vendor": "esbuild app/javascript/vendor.js --bundle --minify --format=esm --outdir=app/assets/builds --public-path=/assets",
|
||||
"build:app": "esbuild app/javascript/application.js --bundle --minify --external:react --external:react-dom --format=esm --outdir=app/assets/builds --public-path=/assets"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Conditional Loading
|
||||
**Lazy load heavy components:**
|
||||
```javascript
|
||||
// app/javascript/application.js
|
||||
if (document.querySelector('[data-controller="shadcn-test"]')) {
|
||||
import('./controllers/shadcn_test_controller')
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Production Optimization Checklist
|
||||
|
||||
**Step 1: Analyze bundle size**
|
||||
```bash
|
||||
npm install --save-dev webpack-bundle-analyzer
|
||||
npx esbuild app/javascript/application.js --bundle --analyze
|
||||
```
|
||||
|
||||
**Step 2: Implement optimizations**
|
||||
```bash
|
||||
# Remove unused dependencies
|
||||
npm uninstall alpinejs @types/alpinejs cssnano pm2
|
||||
|
||||
# Update build scripts
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Step 3: Verify size reduction**
|
||||
Should reduce from 1.4MB to ~200-400KB
|
||||
|
||||
## Quick Fix Commands
|
||||
|
||||
```bash
|
||||
# 1. Remove Alpine.js (if unused)
|
||||
npm uninstall alpinejs @types/alpinejs
|
||||
|
||||
# 2. Update build with optimization
|
||||
npm install --save-dev esbuild@latest
|
||||
|
||||
# 3. Modify package.json scripts
|
||||
# (Copy the optimized scripts above)
|
||||
|
||||
# 4. Build with optimization
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Expected Results
|
||||
- **Before**: 1.4MB application.js
|
||||
- **After**: 200-400KB with code splitting
|
||||
- **Vendor bundle**: ~100KB (cached)
|
||||
- **App bundle**: ~100-300KB (dynamic)
|
||||
|
||||
## Monitoring
|
||||
Add bundle size monitoring to CI/CD:
|
||||
```json
|
||||
{
|
||||
"size-limits": {
|
||||
"app/assets/builds/application.js": "500kb",
|
||||
"app/assets/builds/application.css": "50kb"
|
||||
}
|
||||
}
|
||||
1055
docs/architecture.md
Normal file
1055
docs/architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
142
docs/branch-naming.md
Normal file
142
docs/branch-naming.md
Normal file
@@ -0,0 +1,142 @@
|
||||
Cette conversation a été générée avec Duck.ai (https://duck.ai) à l'aide du modèle Mistral Small 3 d'Mistral AI. Les chats IA peuvent afficher des informations inexactes ou offensantes (voir https://duckduckgo.com/duckai/privacy-terms pour en savoir plus).
|
||||
|
||||
====================
|
||||
|
||||
Message 1 sur 2 de l'utilisateur - 16/08/2025, 08:06:58:
|
||||
In a git Ruby on Rails web project, how to name branches?
|
||||
|
||||
Mistral Small 3:
|
||||
Naming branches in a Git Ruby on Rails web project (or any Git project) is an important aspect of maintaining a clean and understandable repository. Here are some best practices for naming branches:
|
||||
|
||||
1. **Use Descriptive Names**: The branch name should clearly describe the purpose or the feature being worked on. This helps other developers understand the context of the branch at a glance.
|
||||
|
||||
2. **Use Hyphens or Underscores**: Use hyphens (`-`) or underscores (`_`) to separate words in the branch name. This makes the name more readable.
|
||||
|
||||
3. **Prefix with the Type of Change**: Prefix the branch name with a type indicator such as `feature`, `bugfix`, `hotfix`, `release`, `chore`, `refactor`, `docs`, etc. This helps in categorizing the branches.
|
||||
|
||||
4. **Keep It Short but Meaningful**: While being descriptive, try to keep the branch name short to avoid clutter.
|
||||
|
||||
5. **Avoid Special Characters**: Stick to alphanumeric characters and hyphens/underscores. Avoid using special characters like `@`, `#`, `$`, etc.
|
||||
|
||||
6. **Use Lowercase Letters**: Consistently use lowercase letters to avoid case sensitivity issues.
|
||||
|
||||
Here are some examples of well-named branches:
|
||||
|
||||
- `feature/user-authentication`
|
||||
- `bugfix/login-issue`
|
||||
- `hotfix/critical-bug`
|
||||
- `release/v1.2.0`
|
||||
- `chore/update-dependencies`
|
||||
- `refactor/user-model`
|
||||
- `docs/update-readme`
|
||||
|
||||
### Example Workflow
|
||||
|
||||
1. **Creating a New Feature Branch**:
|
||||
```sh
|
||||
git checkout -b feature/user-authentication
|
||||
```
|
||||
|
||||
2. **Creating a Bugfix Branch**:
|
||||
```sh
|
||||
git checkout -b bugfix/login-issue
|
||||
```
|
||||
|
||||
3. **Creating a Hotfix Branch**:
|
||||
```sh
|
||||
git checkout -b hotfix/critical-bug
|
||||
```
|
||||
|
||||
4. **Creating a Release Branch**:
|
||||
```sh
|
||||
git checkout -b release/v1.2.0
|
||||
```
|
||||
|
||||
5. **Creating a Chore Branch**:
|
||||
```sh
|
||||
git checkout -b chore/update-dependencies
|
||||
```
|
||||
|
||||
6. **Creating a Refactor Branch**:
|
||||
```sh
|
||||
git checkout -b refactor/user-model
|
||||
```
|
||||
|
||||
7. **Creating a Docs Branch**:
|
||||
```sh
|
||||
git checkout -b docs/update-readme
|
||||
```
|
||||
|
||||
By following these guidelines, you can maintain a well-organized and understandable branch structure in your Git repository, making collaboration and code management much easier.
|
||||
|
||||
--------------------
|
||||
|
||||
Message 2 sur 2 de l'utilisateur - 16/08/2025, 08:08:31:
|
||||
Is it a good practice to use main, develop and functionnal names for branches?
|
||||
|
||||
Mistral Small 3:
|
||||
Using `main`, `develop`, and feature branches is a common and widely accepted practice in Git workflows, particularly in the context of the Git Flow or GitHub Flow methodologies. Here's a breakdown of these branches and why they are useful:
|
||||
|
||||
1. **`main` Branch**:
|
||||
- **Purpose**: The `main` branch (or sometimes called `master`) is the stable branch that always contains the production-ready code. It represents the current state of the project that is deployed to production.
|
||||
- **Usage**: This branch should only contain code that has been thoroughly tested and is ready for release. It is typically protected to prevent direct commits and requires pull requests for changes.
|
||||
- **Example**: `git checkout main`
|
||||
|
||||
2. **`develop` Branch**:
|
||||
- **Purpose**: The `develop` branch is used for integrating features and preparing for the next release. It serves as a staging area for features that are in progress but not yet ready for production.
|
||||
- **Usage**: Developers merge their feature branches into `develop` for integration testing. This branch can be less stable than `main` but should still be in a reasonably good state.
|
||||
- **Example**: `git checkout develop`
|
||||
|
||||
3. **Feature Branches**:
|
||||
- **Purpose**: Feature branches are used to develop new features, bug fixes, or any other isolated work. Each feature branch is created from `develop` and merged back into `develop` once the work is complete.
|
||||
- **Usage**: These branches allow for parallel development without interfering with the stability of `develop` or `main`. They are typically named descriptively, as discussed earlier (e.g., `feature/user-authentication`, `bugfix/login-issue`).
|
||||
- **Example**: `git checkout -b feature/user-authentication`
|
||||
|
||||
### Example Workflow
|
||||
|
||||
1. **Create a Feature Branch**:
|
||||
```sh
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/user-authentication
|
||||
```
|
||||
|
||||
2. **Develop the Feature**:
|
||||
- Make changes, commit them, and push the branch to the remote repository.
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "Add user authentication feature"
|
||||
git push origin feature/user-authentication
|
||||
```
|
||||
|
||||
3. **Merge the Feature Branch into `develop`**:
|
||||
- Once the feature is complete and tested, create a pull request to merge it into `develop`.
|
||||
```sh
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git merge feature/user-authentication
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
4. **Prepare for Release**:
|
||||
- When preparing for a new release, create a release branch from `develop`.
|
||||
```sh
|
||||
git checkout -b release/v1.2.0
|
||||
```
|
||||
|
||||
5. **Merge Release Branch into `main`**:
|
||||
- After testing the release branch, merge it into `main` and tag the release.
|
||||
```sh
|
||||
git checkout main
|
||||
git merge release/v1.2.0
|
||||
git tag -a v1.2.0 -m "Release version 1.2.0"
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Stability**: The `main` branch remains stable and always contains production-ready code.
|
||||
- **Integration**: The `develop` branch allows for continuous integration of features, making it easier to test and integrate new work.
|
||||
- **Isolation**: Feature branches isolate work, allowing multiple developers to work on different features simultaneously without conflicts.
|
||||
|
||||
Using `main`, `develop`, and feature branches is a good practice as it helps maintain a clean, organized, and manageable repository, facilitating better collaboration and code management.
|
||||
288
docs/creating-shadcn-react-components.md
Normal file
288
docs/creating-shadcn-react-components.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# Creating New Shadcn and React Components
|
||||
|
||||
This guide explains how to create new Shadcn (UI) components and React components in this Rails application with React frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
This project uses:
|
||||
- **Shadcn/ui** for UI components (built on Radix UI and Tailwind CSS)
|
||||
- **React** for frontend components
|
||||
- **Rails** as the backend framework
|
||||
- **esbuild** for JavaScript bundling
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── javascript/
|
||||
│ ├── components/
|
||||
│ │ └── ui/ # Shadcn components
|
||||
│ └── controllers/ # React controllers
|
||||
├── views/
|
||||
│ └── components/ # Rails view components
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## Creating Shadcn Components
|
||||
|
||||
### 1. Using the Shadcn CLI
|
||||
|
||||
The easiest way to add new Shadcn components is using the CLI:
|
||||
|
||||
```bash
|
||||
# Navigate to the project root
|
||||
cd /home/acid/Documents/aperonight
|
||||
|
||||
# Add a new component (example: adding a card)
|
||||
npx shadcn-ui@latest add card
|
||||
```
|
||||
|
||||
This will:
|
||||
- Install the component to `app/javascript/components/ui/`
|
||||
- Update the components.json configuration
|
||||
- Create the necessary TypeScript/JavaScript files
|
||||
|
||||
### 2. Manual Component Creation
|
||||
|
||||
If the CLI is not available, create components manually:
|
||||
|
||||
#### Create the component file
|
||||
```bash
|
||||
# Create a new component (example: button.jsx)
|
||||
touch app/javascript/components/ui/button.jsx
|
||||
```
|
||||
|
||||
#### Basic component structure
|
||||
```javascript
|
||||
// app/javascript/components/ui/button.jsx
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Button = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button }
|
||||
```
|
||||
|
||||
## Creating React Components
|
||||
|
||||
### 1. Controller-Based Components
|
||||
|
||||
For components that need Rails integration:
|
||||
|
||||
#### Create controller file
|
||||
```bash
|
||||
# Create a new controller
|
||||
touch app/javascript/controllers/my_component_controller.js
|
||||
```
|
||||
|
||||
#### Basic controller structure
|
||||
```javascript
|
||||
// app/javascript/controllers/my_component_controller.js
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["container"]
|
||||
|
||||
connect() {
|
||||
const root = ReactDOM.createRoot(this.containerTarget)
|
||||
root.render(<MyComponent />)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Standalone React Components
|
||||
|
||||
For reusable React components:
|
||||
|
||||
#### Create component file
|
||||
```bash
|
||||
# Create a new React component
|
||||
touch app/javascript/components/MyNewComponent.jsx
|
||||
```
|
||||
|
||||
#### Component structure
|
||||
```javascript
|
||||
// app/javascript/components/MyNewComponent.jsx
|
||||
import React from "react"
|
||||
|
||||
const MyNewComponent = ({ title, description }) => {
|
||||
return (
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<p className="text-gray-600">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyNewComponent
|
||||
```
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### 1. Using in Rails Views
|
||||
|
||||
To use components in Rails views:
|
||||
|
||||
#### Create partial
|
||||
```erb
|
||||
<!-- app/views/components/_my_component.html.erb -->
|
||||
<div data-controller="my-component">
|
||||
<div data-my-component-target="container"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Include in page
|
||||
```erb
|
||||
<!-- app/views/pages/home.html.erb -->
|
||||
<%= render "components/my_component" %>
|
||||
```
|
||||
|
||||
### 2. Direct React Rendering
|
||||
|
||||
For pages that are primarily React:
|
||||
|
||||
#### Create page component
|
||||
```javascript
|
||||
// app/javascript/components/pages/HomePage.jsx
|
||||
import React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<h1>Welcome</h1>
|
||||
<Button>Get Started</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
```
|
||||
|
||||
## Configuration Updates
|
||||
|
||||
### 1. Update components.json
|
||||
```json
|
||||
{
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/assets/stylesheets/application.postcss.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "app/javascript/components",
|
||||
"utils": "app/javascript/lib/utils"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update JavaScript entry point
|
||||
```javascript
|
||||
// app/javascript/application.js
|
||||
import "./components"
|
||||
import "./controllers"
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Shadcn Components
|
||||
- Use kebab-case for filenames: `button.jsx`, `card.jsx`
|
||||
- Use PascalCase for exports: `export { Button }`
|
||||
- Follow Radix UI naming patterns
|
||||
|
||||
### React Components
|
||||
- Use PascalCase for filenames: `MyComponent.jsx`
|
||||
- Use PascalCase for components: `const MyComponent = () => {}`
|
||||
- Use camelCase for props: `myProp`, `onClick`
|
||||
|
||||
## Testing Components
|
||||
|
||||
### 1. Create test file
|
||||
```bash
|
||||
# Create test file
|
||||
touch test/components/my_component_test.rb
|
||||
```
|
||||
|
||||
### 2. Write component test
|
||||
```javascript
|
||||
// test/components/my_component_test.jsx
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import MyComponent from "../../app/javascript/components/MyComponent"
|
||||
|
||||
test("renders component", () => {
|
||||
render(<MyComponent title="Test" />)
|
||||
expect(screen.getByText("Test")).toBeInTheDocument()
|
||||
})
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Props Pattern
|
||||
```javascript
|
||||
// Pass Rails data as props
|
||||
const MyComponent = ({ user, config }) => {
|
||||
return <div>{user.name}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Event Handling
|
||||
```javascript
|
||||
// Handle events from Rails
|
||||
const MyComponent = ({ onAction }) => {
|
||||
return <button onClick={onAction}>Click me</button>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Styling Integration
|
||||
```javascript
|
||||
// Use Tailwind classes
|
||||
const MyComponent = () => {
|
||||
return <div className="bg-white dark:bg-gray-800">Content</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Component not rendering**: Check controller connection
|
||||
2. **Styling issues**: Verify Tailwind classes
|
||||
3. **Props not passing**: Check data-controller attributes
|
||||
4. **Import errors**: Verify alias paths in components.json
|
||||
|
||||
### Debug Steps
|
||||
|
||||
1. Check browser console for errors
|
||||
2. Verify component file exists in correct location
|
||||
3. Check import paths in application.js
|
||||
4. Verify Rails view includes correct data attributes
|
||||
|
||||
|
||||
## Example created for testing purpose
|
||||
|
||||
```html
|
||||
<!-- Shadcn Button Test -->
|
||||
<div data-controller="shadcn-test" class="mt-4">
|
||||
<div data-shadcn-test-target="container"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
103
docs/theme-rules.md
Normal file
103
docs/theme-rules.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Theme Rules & Color Palette - Aperonight
|
||||
|
||||
Extracted from `app/views/pages/home.html.erb`
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Primary Colors
|
||||
- **Indigo**: `#4338ca` (rgb(67, 56, 202)) - Used in hero gradient
|
||||
- **Purple**: `#8b5cf6` (rgb(139, 92, 246)) - Primary brand color
|
||||
- **Pink**: `#ec4899` (rgb(236, 72, 153)) - Accent color
|
||||
|
||||
### Background Gradients
|
||||
- **Hero**: `bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-700`
|
||||
- **CTA**: `bg-gradient-to-r from-purple-900 via-indigo-900 to-pink-900`
|
||||
- **Cards**: `bg-gradient-to-br from-gray-800 to-gray-900`
|
||||
- **Buttons**: `bg-gradient-to-r from-purple-600 to-pink-600`
|
||||
|
||||
### Text Colors
|
||||
- **White**: `text-white` - Primary text
|
||||
- **Gray-200**: `text-gray-200` - Secondary text
|
||||
- **Gray-300**: `text-gray-300` - Subtle text
|
||||
- **Gray-400**: `text-gray-400` - Muted text
|
||||
- **Transparent gradient**: `text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400` - Special highlight
|
||||
|
||||
### Background Colors
|
||||
- **Gray-900**: `bg-gray-900` - Main background
|
||||
- **Black**: `bg-black` - Overlay backgrounds
|
||||
- **Gray-800**: `bg-gray-800` - Card backgrounds
|
||||
- **White/Transparent**: `bg-white bg-opacity-10 backdrop-blur-sm` - Glass effect
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
### Hero Section
|
||||
- **Height**: `min-h-[70vh]`
|
||||
- **Max-width**: `max-w-7xl mx-auto`
|
||||
- **Padding**: `px-4 sm:px-6 lg:px-8`
|
||||
|
||||
### Grid Layouts
|
||||
- **Responsive**: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
|
||||
- **Gap**: `gap-8` standard spacing
|
||||
|
||||
### Padding Classes
|
||||
- **Section**: `py-16`, `py-20`
|
||||
- **Card**: `p-4`, `p-6`, `p-8`
|
||||
- **Button**: `py-3`, `py-4`, `px-6`, `px-8`
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Sizes
|
||||
- **Hero Title**: `text-5xl md:text-7xl`
|
||||
- **Section Title**: `text-4xl`
|
||||
- **Card Title**: `text-2xl`
|
||||
- **Body**: `text-xl`, `text-lg`
|
||||
- **Small**: `text-sm`
|
||||
|
||||
### Font Weights
|
||||
- **Bold**: `font-bold` (headings)
|
||||
- **Semibold**: `font-semibold` (buttons, important text)
|
||||
- **Medium**: `font-medium` (labels)
|
||||
|
||||
## Interactive States
|
||||
|
||||
### Hover Effects
|
||||
- **Scale**: `hover:scale-105`
|
||||
- **Transition**: `transition-all duration-300`
|
||||
- **Button Hover**: `hover:from-purple-700 hover:to-pink-700`
|
||||
- **Glass Hover**: `hover:bg-opacity-20`
|
||||
|
||||
### Shadows
|
||||
- **Default**: `shadow-lg`
|
||||
- **Strong**: `shadow-xl`
|
||||
- **Card**: `shadow-2xl`
|
||||
|
||||
## Border Radius
|
||||
- **Buttons**: `rounded-full` (pill-shaped)
|
||||
- **Cards**: `rounded-2xl`
|
||||
- **Inputs**: `rounded-lg`
|
||||
|
||||
## Icon Colors
|
||||
- **Primary**: `text-white` (on colored backgrounds)
|
||||
- **Accent**: `text-purple-400`, `text-pink-400`
|
||||
- **Muted**: `text-gray-400`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Primary Button
|
||||
```html
|
||||
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg"
|
||||
```
|
||||
|
||||
### Card Background
|
||||
```html
|
||||
class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-xl"
|
||||
```
|
||||
|
||||
### Hero Gradient
|
||||
```html
|
||||
class="bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-700"
|
||||
```
|
||||
|
||||
### Glass Effect
|
||||
```html
|
||||
class="bg-white bg-opacity-10 backdrop-blur-sm border border-white border-opacity-30"
|
||||
26
ecosystem.config.js
Normal file
26
ecosystem.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "watch-js", // Name of the process
|
||||
script: "yarn", // Run npm command
|
||||
args: "run build --watch", // Run build, build:css, and watch:files in sequence
|
||||
watch: false, // Watch for file changes (you can modify this as needed)
|
||||
autorestart: true, // Automatically restart on crash
|
||||
max_restarts: 10, // Maximum number of restarts PM2 will attempt
|
||||
env: {
|
||||
NODE_ENV: "development", // Set environment variables here (optional)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "watch-css", // Name of the process
|
||||
script: "yarn", // Run npm command
|
||||
args: "run build:css --watch", // Run build, build:css, and watch:files in sequence
|
||||
watch: false, // Watch for file changes (you can modify this as needed)
|
||||
autorestart: true, // Automatically restart on crash
|
||||
max_restarts: 10, // Maximum number of restarts PM2 will attempt
|
||||
env: {
|
||||
NODE_ENV: "development", // Set environment variables here (optional)
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
RAILS_ENV=production
|
||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||
APP_NAME=Pafterwork
|
||||
|
||||
# Database Configuration for production and development
|
||||
DB_HOST=mariadb
|
||||
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["app/javascript/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
4291
package-lock.json
generated
Normal file
4291
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,20 +1,36 @@
|
||||
{
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.9"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
|
||||
"build": "esbuild app/javascript/*.* --bundle --minify --sourcemap=external --format=esm --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx",
|
||||
"build:dev": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx",
|
||||
"build:css": "postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo-rails": "^8.0.16",
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@types/alpinejs": "^3.13.11",
|
||||
"alpinejs": "^3.14.9",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cssnano": "^7.0.6",
|
||||
"esbuild": "^0.25.4",
|
||||
"pm2": "^6.0.5",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nesting": "^13.0.2"
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"postcss-nesting": "^13.0.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('postcss-nesting'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
/**
|
||||
* PostCSS configuration
|
||||
*/
|
||||
const config = {
|
||||
plugins: [require("postcss-import"), require("@tailwindcss/postcss")],
|
||||
};
|
||||
|
||||
// Add more modules to production
|
||||
if (process.env.RAILS_ENV === "production") {
|
||||
config.plugins.push(
|
||||
require("cssnano")({ preset: "default" }),
|
||||
require("autoprefixer"),
|
||||
require("postcss-nested"),
|
||||
require("postcss-flexbugs-fixes"),
|
||||
);
|
||||
}
|
||||
|
||||
// Export config file
|
||||
module.exports = config;
|
||||
|
||||
3
rubocop.sh
Executable file
3
rubocop.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/env bash
|
||||
|
||||
docker compose run --rm rails bash -c "bundle exec rubocop"
|
||||
10
server.sh
Executable file
10
server.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
if [ -f tmp/pids/server.pid ]; then
|
||||
rm tmp/pids/server.pid
|
||||
fi
|
||||
|
||||
rails server -b 0.0.0.0
|
||||
|
||||
# This will exec the CMD from your Dockerfile, i.e. "npm start"
|
||||
exec "$@"
|
||||
81
tailwind.config.js
Normal file
81
tailwind.config.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { fontFamily } = require("tailwindcss/defaultTheme")
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./public/*.html',
|
||||
'./app/helpers/**/*.rb',
|
||||
'./app/javascript/**/*.js',
|
||||
'./app/views/**/*.{erb,haml,html,slim}',
|
||||
'./app/components/**/*.{erb,haml,html,slim,js}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
11
test/fixtures/users.yml
vendored
Normal file
11
test/fixtures/users.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# This model initially had no columns defined. If you add columns to the
|
||||
# model remove the "{}" from the fixture names and add the columns immediately
|
||||
# below each fixture, per the syntax in the comments below
|
||||
#
|
||||
one: {}
|
||||
# column: value
|
||||
#
|
||||
two: {}
|
||||
# column: value
|
||||
7
test/models/user_test.rb
Normal file
7
test/models/user_test.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class UserTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Reference in New Issue
Block a user