mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 13:31:09 -05:00
feat: Implement UI components for Django templates
- Added Button component with various styles and sizes. - Introduced Card component for displaying content with titles and descriptions. - Created Input component for form fields with support for various attributes. - Developed Toast Notification Container for displaying alerts and messages. - Designed pages for listing designers and operators with pagination and responsive layout. - Documented frontend migration from React to HTMX + Alpine.js, detailing component usage and integration.
This commit is contained in:
574
backend/static/css/components.css
Normal file
574
backend/static/css/components.css
Normal file
@@ -0,0 +1,574 @@
|
||||
/**
|
||||
* ThrillWiki Component Styles
|
||||
* Enhanced CSS matching shadcn/ui design system from React frontend
|
||||
*/
|
||||
|
||||
/* CSS Variables for Design System */
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Component Classes */
|
||||
.bg-background { background-color: hsl(var(--background)); }
|
||||
.bg-foreground { background-color: hsl(var(--foreground)); }
|
||||
.bg-card { background-color: hsl(var(--card)); }
|
||||
.bg-card-foreground { background-color: hsl(var(--card-foreground)); }
|
||||
.bg-popover { background-color: hsl(var(--popover)); }
|
||||
.bg-popover-foreground { background-color: hsl(var(--popover-foreground)); }
|
||||
.bg-primary { background-color: hsl(var(--primary)); }
|
||||
.bg-primary-foreground { background-color: hsl(var(--primary-foreground)); }
|
||||
.bg-secondary { background-color: hsl(var(--secondary)); }
|
||||
.bg-secondary-foreground { background-color: hsl(var(--secondary-foreground)); }
|
||||
.bg-muted { background-color: hsl(var(--muted)); }
|
||||
.bg-muted-foreground { background-color: hsl(var(--muted-foreground)); }
|
||||
.bg-accent { background-color: hsl(var(--accent)); }
|
||||
.bg-accent-foreground { background-color: hsl(var(--accent-foreground)); }
|
||||
.bg-destructive { background-color: hsl(var(--destructive)); }
|
||||
.bg-destructive-foreground { background-color: hsl(var(--destructive-foreground)); }
|
||||
|
||||
.text-background { color: hsl(var(--background)); }
|
||||
.text-foreground { color: hsl(var(--foreground)); }
|
||||
.text-card { color: hsl(var(--card)); }
|
||||
.text-card-foreground { color: hsl(var(--card-foreground)); }
|
||||
.text-popover { color: hsl(var(--popover)); }
|
||||
.text-popover-foreground { color: hsl(var(--popover-foreground)); }
|
||||
.text-primary { color: hsl(var(--primary)); }
|
||||
.text-primary-foreground { color: hsl(var(--primary-foreground)); }
|
||||
.text-secondary { color: hsl(var(--secondary)); }
|
||||
.text-secondary-foreground { color: hsl(var(--secondary-foreground)); }
|
||||
.text-muted { color: hsl(var(--muted)); }
|
||||
.text-muted-foreground { color: hsl(var(--muted-foreground)); }
|
||||
.text-accent { color: hsl(var(--accent)); }
|
||||
.text-accent-foreground { color: hsl(var(--accent-foreground)); }
|
||||
.text-destructive { color: hsl(var(--destructive)); }
|
||||
.text-destructive-foreground { color: hsl(var(--destructive-foreground)); }
|
||||
|
||||
.border-background { border-color: hsl(var(--background)); }
|
||||
.border-foreground { border-color: hsl(var(--foreground)); }
|
||||
.border-card { border-color: hsl(var(--card)); }
|
||||
.border-card-foreground { border-color: hsl(var(--card-foreground)); }
|
||||
.border-popover { border-color: hsl(var(--popover)); }
|
||||
.border-popover-foreground { border-color: hsl(var(--popover-foreground)); }
|
||||
.border-primary { border-color: hsl(var(--primary)); }
|
||||
.border-primary-foreground { border-color: hsl(var(--primary-foreground)); }
|
||||
.border-secondary { border-color: hsl(var(--secondary)); }
|
||||
.border-secondary-foreground { border-color: hsl(var(--secondary-foreground)); }
|
||||
.border-muted { border-color: hsl(var(--muted)); }
|
||||
.border-muted-foreground { border-color: hsl(var(--muted-foreground)); }
|
||||
.border-accent { border-color: hsl(var(--accent)); }
|
||||
.border-accent-foreground { border-color: hsl(var(--accent-foreground)); }
|
||||
.border-destructive { border-color: hsl(var(--destructive)); }
|
||||
.border-destructive-foreground { border-color: hsl(var(--destructive-foreground)); }
|
||||
.border-input { border-color: hsl(var(--input)); }
|
||||
|
||||
.ring-background { --tw-ring-color: hsl(var(--background)); }
|
||||
.ring-foreground { --tw-ring-color: hsl(var(--foreground)); }
|
||||
.ring-card { --tw-ring-color: hsl(var(--card)); }
|
||||
.ring-card-foreground { --tw-ring-color: hsl(var(--card-foreground)); }
|
||||
.ring-popover { --tw-ring-color: hsl(var(--popover)); }
|
||||
.ring-popover-foreground { --tw-ring-color: hsl(var(--popover-foreground)); }
|
||||
.ring-primary { --tw-ring-color: hsl(var(--primary)); }
|
||||
.ring-primary-foreground { --tw-ring-color: hsl(var(--primary-foreground)); }
|
||||
.ring-secondary { --tw-ring-color: hsl(var(--secondary)); }
|
||||
.ring-secondary-foreground { --tw-ring-color: hsl(var(--secondary-foreground)); }
|
||||
.ring-muted { --tw-ring-color: hsl(var(--muted)); }
|
||||
.ring-muted-foreground { --tw-ring-color: hsl(var(--muted-foreground)); }
|
||||
.ring-accent { --tw-ring-color: hsl(var(--accent)); }
|
||||
.ring-accent-foreground { --tw-ring-color: hsl(var(--accent-foreground)); }
|
||||
.ring-destructive { --tw-ring-color: hsl(var(--destructive)); }
|
||||
.ring-destructive-foreground { --tw-ring-color: hsl(var(--destructive-foreground)); }
|
||||
.ring-ring { --tw-ring-color: hsl(var(--ring)); }
|
||||
|
||||
.ring-offset-background { --tw-ring-offset-color: hsl(var(--background)); }
|
||||
|
||||
/* Enhanced Button Styles */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
@apply bg-primary text-primary-foreground hover:bg-primary/90;
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
@apply bg-destructive text-destructive-foreground hover:bg-destructive/90;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply border border-input bg-background hover:bg-accent hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply hover:bg-accent hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
@apply text-primary underline-offset-4 hover:underline;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply h-9 rounded-md px-3;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply h-11 rounded-md px-8;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@apply h-10 w-10;
|
||||
}
|
||||
|
||||
/* Enhanced Card Styles */
|
||||
.card {
|
||||
@apply rounded-lg border bg-card text-card-foreground shadow-sm;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@apply flex flex-col space-y-1.5 p-6;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-2xl font-semibold leading-none tracking-tight;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
@apply text-sm text-muted-foreground;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
@apply p-6 pt-0;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@apply flex items-center p-6 pt-0;
|
||||
}
|
||||
|
||||
/* Enhanced Input Styles */
|
||||
.input {
|
||||
@apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
/* Enhanced Form Styles */
|
||||
.form-group {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply text-sm font-medium text-destructive;
|
||||
}
|
||||
|
||||
.form-description {
|
||||
@apply text-sm text-muted-foreground;
|
||||
}
|
||||
|
||||
/* Enhanced Navigation Styles */
|
||||
.nav-link {
|
||||
@apply flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md hover:bg-accent hover:text-accent-foreground transition-colors;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
@apply bg-accent text-accent-foreground;
|
||||
}
|
||||
|
||||
/* Enhanced Dropdown Styles */
|
||||
.dropdown-content {
|
||||
@apply z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
@apply -mx-1 my-1 h-px bg-muted;
|
||||
}
|
||||
|
||||
/* Enhanced Modal Styles */
|
||||
.modal-overlay {
|
||||
@apply fixed inset-0 z-50 bg-background/80 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@apply fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply flex flex-col space-y-1.5 text-center sm:text-left;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@apply text-lg font-semibold leading-none tracking-tight;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
@apply text-sm text-muted-foreground;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@apply flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2;
|
||||
}
|
||||
|
||||
/* Enhanced Alert Styles */
|
||||
.alert {
|
||||
@apply relative w-full rounded-lg border p-4;
|
||||
}
|
||||
|
||||
.alert-default {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
.alert-destructive {
|
||||
@apply border-destructive/50 text-destructive dark:border-destructive;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
@apply mb-1 font-medium leading-none tracking-tight;
|
||||
}
|
||||
|
||||
.alert-description {
|
||||
@apply text-sm opacity-90;
|
||||
}
|
||||
|
||||
/* Enhanced Badge Styles */
|
||||
.badge {
|
||||
@apply inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2;
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
@apply border-transparent bg-primary text-primary-foreground hover:bg-primary/80;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
@apply border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80;
|
||||
}
|
||||
|
||||
.badge-destructive {
|
||||
@apply border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80;
|
||||
}
|
||||
|
||||
.badge-outline {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
/* Enhanced Table Styles */
|
||||
.table {
|
||||
@apply w-full caption-bottom text-sm;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@apply border-b;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
@apply divide-y;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
@apply border-b transition-colors hover:bg-muted/50;
|
||||
}
|
||||
|
||||
.table-head {
|
||||
@apply h-12 px-4 text-left align-middle font-medium text-muted-foreground;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
@apply p-4 align-middle;
|
||||
}
|
||||
|
||||
/* Enhanced Skeleton Styles */
|
||||
.skeleton {
|
||||
@apply animate-pulse rounded-md bg-muted;
|
||||
}
|
||||
|
||||
/* Enhanced Separator Styles */
|
||||
.separator {
|
||||
@apply shrink-0 bg-border;
|
||||
}
|
||||
|
||||
.separator-horizontal {
|
||||
@apply h-[1px] w-full;
|
||||
}
|
||||
|
||||
.separator-vertical {
|
||||
@apply h-full w-[1px];
|
||||
}
|
||||
|
||||
/* Enhanced Avatar Styles */
|
||||
.avatar {
|
||||
@apply relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
@apply aspect-square h-full w-full object-cover;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
@apply flex h-full w-full items-center justify-center rounded-full bg-muted;
|
||||
}
|
||||
|
||||
/* Enhanced Progress Styles */
|
||||
.progress {
|
||||
@apply relative h-4 w-full overflow-hidden rounded-full bg-secondary;
|
||||
}
|
||||
|
||||
.progress-indicator {
|
||||
@apply h-full w-full flex-1 bg-primary transition-all;
|
||||
}
|
||||
|
||||
/* Enhanced Scroll Area Styles */
|
||||
.scroll-area {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.scroll-viewport {
|
||||
@apply h-full w-full rounded-[inherit];
|
||||
}
|
||||
|
||||
.scroll-bar {
|
||||
@apply flex touch-none select-none transition-colors;
|
||||
}
|
||||
|
||||
.scroll-thumb {
|
||||
@apply relative flex-1 rounded-full bg-border;
|
||||
}
|
||||
|
||||
/* Enhanced Tabs Styles */
|
||||
.tabs-list {
|
||||
@apply inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground;
|
||||
}
|
||||
|
||||
.tabs-trigger {
|
||||
@apply inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
@apply mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
|
||||
}
|
||||
|
||||
/* Enhanced Tooltip Styles */
|
||||
.tooltip-content {
|
||||
@apply z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95;
|
||||
}
|
||||
|
||||
/* Enhanced Switch Styles */
|
||||
.switch {
|
||||
@apply peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
|
||||
}
|
||||
|
||||
.switch-thumb {
|
||||
@apply pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0;
|
||||
}
|
||||
|
||||
/* Enhanced Checkbox Styles */
|
||||
.checkbox {
|
||||
@apply peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground;
|
||||
}
|
||||
|
||||
/* Enhanced Radio Styles */
|
||||
.radio {
|
||||
@apply aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
/* Enhanced Select Styles */
|
||||
.select-trigger {
|
||||
@apply flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
.select-content {
|
||||
@apply relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
@apply relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleOut {
|
||||
from { transform: scale(1); opacity: 1; }
|
||||
to { transform: scale(0.95); opacity: 0; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
animation: fadeOut 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-out {
|
||||
animation: slideOut 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-out {
|
||||
animation: scaleOut 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive Design Helpers */
|
||||
@media (max-width: 640px) {
|
||||
.modal-content {
|
||||
@apply w-[95vw] max-w-none;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
@apply w-screen max-w-none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Specific Adjustments */
|
||||
.dark .shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .shadow-md {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Focus Visible Improvements */
|
||||
.focus-visible\:ring-2:focus-visible {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
/* High Contrast Mode Support */
|
||||
@media (prefers-contrast: high) {
|
||||
.border {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion Support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.transition-colors,
|
||||
.transition-all,
|
||||
.transition-transform {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.animate-fade-in,
|
||||
.animate-fade-out,
|
||||
.animate-slide-in,
|
||||
.animate-slide-out,
|
||||
.animate-scale-in,
|
||||
.animate-scale-out {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
711
backend/static/js/alpine-components.js
Normal file
711
backend/static/js/alpine-components.js
Normal file
@@ -0,0 +1,711 @@
|
||||
/**
|
||||
* Alpine.js Components for ThrillWiki
|
||||
* Enhanced components matching React frontend functionality
|
||||
*/
|
||||
|
||||
// Theme Toggle Component
|
||||
Alpine.data('themeToggle', () => ({
|
||||
theme: localStorage.getItem('theme') || 'system',
|
||||
|
||||
init() {
|
||||
this.updateTheme();
|
||||
|
||||
// Watch for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (this.theme === 'system') {
|
||||
this.updateTheme();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
const themes = ['light', 'dark', 'system'];
|
||||
const currentIndex = themes.indexOf(this.theme);
|
||||
this.theme = themes[(currentIndex + 1) % themes.length];
|
||||
localStorage.setItem('theme', this.theme);
|
||||
this.updateTheme();
|
||||
},
|
||||
|
||||
updateTheme() {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (this.theme === 'dark' ||
|
||||
(this.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Search Component
|
||||
Alpine.data('searchComponent', () => ({
|
||||
query: '',
|
||||
results: [],
|
||||
loading: false,
|
||||
showResults: false,
|
||||
|
||||
async search() {
|
||||
if (this.query.length < 2) {
|
||||
this.results = [];
|
||||
this.showResults = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search/?q=${encodeURIComponent(this.query)}`);
|
||||
const data = await response.json();
|
||||
this.results = data.results || [];
|
||||
this.showResults = this.results.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
this.results = [];
|
||||
this.showResults = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectResult(result) {
|
||||
window.location.href = result.url;
|
||||
this.showResults = false;
|
||||
this.query = '';
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.query = '';
|
||||
this.results = [];
|
||||
this.showResults = false;
|
||||
}
|
||||
}));
|
||||
|
||||
// Browse Menu Component
|
||||
Alpine.data('browseMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
}));
|
||||
|
||||
// Mobile Menu Component
|
||||
Alpine.data('mobileMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
|
||||
// Prevent body scroll when menu is open
|
||||
if (this.open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}));
|
||||
|
||||
// User Menu Component
|
||||
Alpine.data('userMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
}));
|
||||
|
||||
// Modal Component
|
||||
Alpine.data('modal', (initialOpen = false) => ({
|
||||
open: initialOpen,
|
||||
|
||||
show() {
|
||||
this.open = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.open = false;
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
toggle() {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Dropdown Component
|
||||
Alpine.data('dropdown', (initialOpen = false) => ({
|
||||
open: initialOpen,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
show() {
|
||||
this.open = true;
|
||||
}
|
||||
}));
|
||||
|
||||
// Tabs Component
|
||||
Alpine.data('tabs', (defaultTab = 0) => ({
|
||||
activeTab: defaultTab,
|
||||
|
||||
setTab(index) {
|
||||
this.activeTab = index;
|
||||
},
|
||||
|
||||
isActive(index) {
|
||||
return this.activeTab === index;
|
||||
}
|
||||
}));
|
||||
|
||||
// Accordion Component
|
||||
Alpine.data('accordion', (allowMultiple = false) => ({
|
||||
openItems: [],
|
||||
|
||||
toggle(index) {
|
||||
if (this.isOpen(index)) {
|
||||
this.openItems = this.openItems.filter(item => item !== index);
|
||||
} else {
|
||||
if (allowMultiple) {
|
||||
this.openItems.push(index);
|
||||
} else {
|
||||
this.openItems = [index];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isOpen(index) {
|
||||
return this.openItems.includes(index);
|
||||
},
|
||||
|
||||
open(index) {
|
||||
if (!this.isOpen(index)) {
|
||||
if (allowMultiple) {
|
||||
this.openItems.push(index);
|
||||
} else {
|
||||
this.openItems = [index];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
close(index) {
|
||||
this.openItems = this.openItems.filter(item => item !== index);
|
||||
}
|
||||
}));
|
||||
|
||||
// Form Component with Validation
|
||||
Alpine.data('form', (initialData = {}) => ({
|
||||
data: initialData,
|
||||
errors: {},
|
||||
loading: false,
|
||||
|
||||
setField(field, value) {
|
||||
this.data[field] = value;
|
||||
// Clear error when user starts typing
|
||||
if (this.errors[field]) {
|
||||
delete this.errors[field];
|
||||
}
|
||||
},
|
||||
|
||||
setError(field, message) {
|
||||
this.errors[field] = message;
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
hasError(field) {
|
||||
return !!this.errors[field];
|
||||
},
|
||||
|
||||
getError(field) {
|
||||
return this.errors[field] || '';
|
||||
},
|
||||
|
||||
async submit(url, options = {}) {
|
||||
this.loading = true;
|
||||
this.clearErrors();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '',
|
||||
...options.headers
|
||||
},
|
||||
body: JSON.stringify(this.data),
|
||||
...options
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (result.errors) {
|
||||
this.errors = result.errors;
|
||||
}
|
||||
throw new Error(result.message || 'Form submission failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Pagination Component
|
||||
Alpine.data('pagination', (initialPage = 1, totalPages = 1) => ({
|
||||
currentPage: initialPage,
|
||||
totalPages: totalPages,
|
||||
|
||||
goToPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.currentPage = page;
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
this.goToPage(this.currentPage + 1);
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
this.goToPage(this.currentPage - 1);
|
||||
},
|
||||
|
||||
hasNext() {
|
||||
return this.currentPage < this.totalPages;
|
||||
},
|
||||
|
||||
hasPrev() {
|
||||
return this.currentPage > 1;
|
||||
},
|
||||
|
||||
getPages() {
|
||||
const pages = [];
|
||||
const start = Math.max(1, this.currentPage - 2);
|
||||
const end = Math.min(this.totalPages, this.currentPage + 2);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
}));
|
||||
|
||||
// Toast/Alert Component
|
||||
Alpine.data('toast', () => ({
|
||||
toasts: [],
|
||||
|
||||
show(message, type = 'info', duration = 5000) {
|
||||
const id = Date.now();
|
||||
const toast = { id, message, type, visible: true };
|
||||
|
||||
this.toasts.push(toast);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.hide(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hide(id) {
|
||||
const toast = this.toasts.find(t => t.id === id);
|
||||
if (toast) {
|
||||
toast.visible = false;
|
||||
setTimeout(() => {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
}, 300); // Wait for animation
|
||||
}
|
||||
},
|
||||
|
||||
success(message, duration) {
|
||||
return this.show(message, 'success', duration);
|
||||
},
|
||||
|
||||
error(message, duration) {
|
||||
return this.show(message, 'error', duration);
|
||||
},
|
||||
|
||||
warning(message, duration) {
|
||||
return this.show(message, 'warning', duration);
|
||||
},
|
||||
|
||||
info(message, duration) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
}));
|
||||
|
||||
// Enhanced Authentication Modal Component
|
||||
Alpine.data('authModal', (defaultMode = 'login') => ({
|
||||
open: false,
|
||||
mode: defaultMode, // 'login' or 'register'
|
||||
showPassword: false,
|
||||
socialProviders: [],
|
||||
socialLoading: true,
|
||||
|
||||
// Login form data
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loginLoading: false,
|
||||
loginError: '',
|
||||
|
||||
// Register form data
|
||||
registerForm: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password1: '',
|
||||
password2: ''
|
||||
},
|
||||
registerLoading: false,
|
||||
registerError: '',
|
||||
|
||||
init() {
|
||||
this.fetchSocialProviders();
|
||||
|
||||
// Listen for auth modal events
|
||||
this.$watch('open', (value) => {
|
||||
if (value) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
this.resetForms();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async fetchSocialProviders() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/social-providers/');
|
||||
const data = await response.json();
|
||||
this.socialProviders = data.available_providers || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch social providers:', error);
|
||||
this.socialProviders = [];
|
||||
} finally {
|
||||
this.socialLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
show(mode = 'login') {
|
||||
this.mode = mode;
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
switchToLogin() {
|
||||
this.mode = 'login';
|
||||
this.resetForms();
|
||||
},
|
||||
|
||||
switchToRegister() {
|
||||
this.mode = 'register';
|
||||
this.resetForms();
|
||||
},
|
||||
|
||||
resetForms() {
|
||||
this.loginForm = { username: '', password: '' };
|
||||
this.registerForm = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password1: '',
|
||||
password2: ''
|
||||
};
|
||||
this.loginError = '';
|
||||
this.registerError = '';
|
||||
this.showPassword = false;
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.loginForm.username || !this.loginForm.password) {
|
||||
this.loginError = 'Please fill in all fields';
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginLoading = true;
|
||||
this.loginError = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/accounts/login/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
login: this.loginForm.username,
|
||||
password: this.loginForm.password
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Login successful - reload page to update auth state
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
this.loginError = data.message || 'Login failed. Please check your credentials.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.loginError = 'An error occurred. Please try again.';
|
||||
} finally {
|
||||
this.loginLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
if (!this.registerForm.first_name || !this.registerForm.last_name ||
|
||||
!this.registerForm.email || !this.registerForm.username ||
|
||||
!this.registerForm.password1 || !this.registerForm.password2) {
|
||||
this.registerError = 'Please fill in all fields';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registerForm.password1 !== this.registerForm.password2) {
|
||||
this.registerError = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerLoading = true;
|
||||
this.registerError = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/accounts/signup/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
first_name: this.registerForm.first_name,
|
||||
last_name: this.registerForm.last_name,
|
||||
email: this.registerForm.email,
|
||||
username: this.registerForm.username,
|
||||
password1: this.registerForm.password1,
|
||||
password2: this.registerForm.password2
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Registration successful
|
||||
this.close();
|
||||
// Show success message or redirect
|
||||
Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.');
|
||||
} else {
|
||||
const data = await response.json();
|
||||
this.registerError = data.message || 'Registration failed. Please try again.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
this.registerError = 'An error occurred. Please try again.';
|
||||
} finally {
|
||||
this.registerLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleSocialLogin(providerId) {
|
||||
const provider = this.socialProviders.find(p => p.id === providerId);
|
||||
if (!provider) {
|
||||
Alpine.store('toast').error(`Social provider ${providerId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to social auth URL
|
||||
window.location.href = provider.auth_url;
|
||||
},
|
||||
|
||||
getCSRFToken() {
|
||||
const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
|
||||
document.querySelector('meta[name=csrf-token]')?.getAttribute('content') ||
|
||||
document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1];
|
||||
return token || '';
|
||||
}
|
||||
}));
|
||||
|
||||
// Enhanced Toast Component with Better UX
|
||||
Alpine.data('toast', () => ({
|
||||
toasts: [],
|
||||
|
||||
show(message, type = 'info', duration = 5000) {
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
visible: true,
|
||||
progress: 100
|
||||
};
|
||||
|
||||
this.toasts.push(toast);
|
||||
|
||||
if (duration > 0) {
|
||||
// Animate progress bar
|
||||
const interval = setInterval(() => {
|
||||
toast.progress -= (100 / (duration / 100));
|
||||
if (toast.progress <= 0) {
|
||||
clearInterval(interval);
|
||||
this.hide(id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hide(id) {
|
||||
const toast = this.toasts.find(t => t.id === id);
|
||||
if (toast) {
|
||||
toast.visible = false;
|
||||
setTimeout(() => {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
|
||||
success(message, duration = 5000) {
|
||||
return this.show(message, 'success', duration);
|
||||
},
|
||||
|
||||
error(message, duration = 7000) {
|
||||
return this.show(message, 'error', duration);
|
||||
},
|
||||
|
||||
warning(message, duration = 6000) {
|
||||
return this.show(message, 'warning', duration);
|
||||
},
|
||||
|
||||
info(message, duration = 5000) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
}));
|
||||
|
||||
// Global Store for App State
|
||||
Alpine.store('app', {
|
||||
user: null,
|
||||
theme: 'system',
|
||||
searchQuery: '',
|
||||
notifications: [],
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = theme;
|
||||
localStorage.setItem('theme', theme);
|
||||
},
|
||||
|
||||
addNotification(notification) {
|
||||
this.notifications.push({
|
||||
id: Date.now(),
|
||||
...notification
|
||||
});
|
||||
},
|
||||
|
||||
removeNotification(id) {
|
||||
this.notifications = this.notifications.filter(n => n.id !== id);
|
||||
}
|
||||
});
|
||||
|
||||
// Global Toast Store
|
||||
Alpine.store('toast', {
|
||||
toasts: [],
|
||||
|
||||
show(message, type = 'info', duration = 5000) {
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
visible: true,
|
||||
progress: 100
|
||||
};
|
||||
|
||||
this.toasts.push(toast);
|
||||
|
||||
if (duration > 0) {
|
||||
const interval = setInterval(() => {
|
||||
toast.progress -= (100 / (duration / 100));
|
||||
if (toast.progress <= 0) {
|
||||
clearInterval(interval);
|
||||
this.hide(id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hide(id) {
|
||||
const toast = this.toasts.find(t => t.id === id);
|
||||
if (toast) {
|
||||
toast.visible = false;
|
||||
setTimeout(() => {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
|
||||
success(message, duration = 5000) {
|
||||
return this.show(message, 'success', duration);
|
||||
},
|
||||
|
||||
error(message, duration = 7000) {
|
||||
return this.show(message, 'error', duration);
|
||||
},
|
||||
|
||||
warning(message, duration = 6000) {
|
||||
return this.show(message, 'warning', duration);
|
||||
},
|
||||
|
||||
info(message, duration = 5000) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Alpine.js when DOM is ready
|
||||
document.addEventListener('alpine:init', () => {
|
||||
console.log('Alpine.js components initialized');
|
||||
});
|
||||
Reference in New Issue
Block a user