10 Commits

Author SHA1 Message Date
kbe
3c6389c6e2 feat: Use application name from .env 2025-08-18 00:25:14 +02:00
kbe
2927a8344f chore: New js transpiler for dev and for production
The old system weren't minifying javascriptt to assets dir.
Now there is a command line for production (build) and dev (build:dev).
2025-08-18 00:13:25 +02:00
kbe
ad97a36650 Rename some doc files 2025-08-18 00:03:42 +02:00
kbe
07a6694883 chore: Counter now embedded with Stimulus
Before this commit, Counter js was autonomous. Now it's registered
like a controller in Stimulus.
2025-08-17 23:47:07 +02:00
kbe
ba3f36a5e8 feat: Add metrics on homepage 2025-08-17 23:39:24 +02:00
kbe
17e6711299 Wip on homepage.
Add documentation how to optimize code size in production.
2025-08-16 22:45:27 +02:00
kbe
31534b2c0c New homepage 2025-08-16 22:28:41 +02:00
kbe
87af76f078 Prepare working with authentication and Tailwind 2025-08-16 21:28:49 +02:00
kbe
9ec13c7d3d chore: Add authentications controller from Devise 2025-08-16 21:03:25 +02:00
kbe
6a1f1cca9a Prepare to use Devise as auth Gem 2025-08-16 17:03:14 +02:00
57 changed files with 9473 additions and 132 deletions

View File

@@ -1,24 +1,79 @@
# README # Aperonight - Party Booking Platform
This README would normally document whatever steps are necessary to get the ![Aperonight Screenshot](app/assets/images/screenshot-homepage.png)
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
}
```

View File

@@ -1 +1,9 @@
/* Entry point for your PostCSS build */ /* Entry point for your PostCSS build */
/* Import Tailwind using PostCSS */
@import "tailwindcss";
/** Default text color */
body {
color: #555555;
}

View 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

View File

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

View 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

View 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

View 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

View 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

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

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

View File

@@ -1,7 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}

View File

@@ -4,5 +4,8 @@
import { application } from "./application" import { application } from "./application"
import HelloController from "./hello_controller" import ShadcnTestController from "./shadcn_test_controller"
application.register("hello", HelloController) import CounterController from "./counter_controller"
application.register("shadcn-test", ShadcnTestController)
application.register("counter", CounterController)

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

View File

@@ -23,6 +23,11 @@
</head> </head>
<body> <body>
<div id="header">
<%= render "components/header" %>
</div><!-- /#header -->
<%= yield %> <%= yield %>
</body> </body>
</html> </html>

View File

@@ -1,2 +1,288 @@
<h1>Pages#home</h1> <!-- Hero Section -->
<p>Find me in app/views/pages/home.html.erb</p> <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
View 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"
}
}

View File

@@ -0,0 +1 @@
Rails.application.config.app_name = ENV.fetch("APP_NAME", "Aperonight")

View 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

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

View File

@@ -11,4 +11,24 @@ Rails.application.routes.draw do
# Defines the root path route ("/") # Defines the root path route ("/")
root "pages#home" 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 end

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

View 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

File diff suppressed because it is too large Load Diff

142
docs/branch-naming.md Normal file
View 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.

View 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
View 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
View 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)
},
},
],
};

View File

@@ -2,6 +2,7 @@
RAILS_ENV=production RAILS_ENV=production
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7 SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
DEVISE_SECRET_KEY=your_devise_secret_key_here DEVISE_SECRET_KEY=your_devise_secret_key_here
APP_NAME=Pafterwork
# Database Configuration for production and development # Database Configuration for production and development
DB_HOST=mariadb DB_HOST=mariadb

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["app/javascript/*"]
}
}
}

4291
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,36 @@
{ {
"name": "app", "name": "app",
"private": true, "private": true,
"devDependencies": {
"esbuild": "^0.25.9"
},
"scripts": { "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" "build:css": "postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css"
}, },
"dependencies": { "dependencies": {
"@hotwired/stimulus": "^3.2.2", "@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", "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-cli": "^11.0.1",
"postcss-import": "^16.1.1", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-nesting": "^13.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"
} }
} }

3
pm2.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/env bash
npx pm2 start

View File

@@ -1,7 +1,19 @@
module.exports = { /**
plugins: [ * PostCSS configuration
require('postcss-import'), */
require('postcss-nesting'), const config = {
require('autoprefixer'), 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
View File

@@ -0,0 +1,3 @@
#!/bin/env bash
docker compose run --rm rails bash -c "bundle exec rubocop"

10
server.sh Executable file
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
require "test_helper"
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

1735
yarn.lock

File diff suppressed because it is too large Load Diff