Compare commits

..

2 Commits

Author SHA1 Message Date
Ace ffa595451e Merge branch 'main' of https://git.coastit.co.za/caitlin/CRM_ENVR 2026-06-26 16:30:50 +02:00
Ace bd581739f9 Fuckups fixed 2026-06-26 16:30:48 +02:00
11 changed files with 256 additions and 48 deletions
+34 -1
View File
@@ -3,7 +3,6 @@
This is the current website appearance captured as a reusable CSS theme.
============================================================================ */
:root,
.theme-spidey {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@@ -34,3 +33,37 @@
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 0 100% 53%;
}
.theme-spidey body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
repeating-conic-gradient(
from 10deg,
transparent 0deg 10deg,
rgba(220, 38, 38, 0.04) 10deg 12deg
),
repeating-radial-gradient(
circle at 50% 50%,
transparent 0,
transparent 30px,
rgba(37, 99, 235, 0.03) 30px,
transparent 32px
),
radial-gradient(2px 2px at 10% 20%, rgba(220,38,38,0.35) 0%, transparent 100%),
radial-gradient(2px 2px at 30% 80%, rgba(37,99,235,0.35) 0%, transparent 100%),
radial-gradient(2px 2px at 50% 30%, rgba(220,38,38,0.3) 0%, transparent 100%),
radial-gradient(2px 2px at 70% 60%, rgba(37,99,235,0.3) 0%, transparent 100%),
radial-gradient(2px 2px at 85% 15%, rgba(220,38,38,0.35) 0%, transparent 100%),
radial-gradient(2px 2px at 20% 50%, rgba(37,99,235,0.25) 0%, transparent 100%),
radial-gradient(2px 2px at 65% 85%, rgba(220,38,38,0.25) 0%, transparent 100%),
radial-gradient(2px 2px at 40% 10%, rgba(37,99,235,0.3) 0%, transparent 100%),
radial-gradient(2px 2px at 90% 75%, rgba(220,38,38,0.25) 0%, transparent 100%),
radial-gradient(2px 2px at 5% 60%, rgba(37,99,235,0.3) 0%, transparent 100%),
radial-gradient(2px 2px at 55% 55%, rgba(220,38,38,0.2) 0%, transparent 100%);
background-size: 200% 200%;
animation: drift 60s linear infinite;
}
+64
View File
@@ -0,0 +1,64 @@
/* ============================================================================
Starforce Theme — Dark cosmic with crimson accents and neon green glow
Captures the current starfield + drift + glow effects used live.
============================================================================ */
.theme-starforce {
--background: 0 0% 10%;
--foreground: 210 40% 98%;
--card: 0 0% 9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 11%;
--popover-foreground: 210 40% 98%;
--primary: 0 100% 53%;
--primary-foreground: 222.2 47.4% 11.2%;
--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: 0 100% 53%;
--radius: 0.5rem;
--sidebar: 0 0% 11%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 145 65% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 0 100% 53%;
}
.theme-starforce body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(1px 1px at 10% 20%, rgba(100,100,120,0.4) 0%, transparent 100%),
radial-gradient(1px 1px at 20% 40%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 30% 10%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 40% 60%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 50% 80%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 60% 30%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 70% 50%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 90% 15%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 15% 70%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 25% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 35% 35%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 45% 15%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 55% 75%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 65% 55%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 75% 25%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 85% 65%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 95% 45%, rgba(80,80,100,0.5) 0%, transparent 100%);
background-size: 200% 200%;
animation: drift 60s linear infinite;
}
+59
View File
@@ -0,0 +1,59 @@
{
"theme": {
"id": "starforce",
"name": "Starforce",
"description": "Dark cosmic theme with crimson accents, starfield background, and neon green glow effects",
"type": "dark",
"default": true
},
"colors": {
"background": "hsl(0, 0%, 10%)",
"foreground": "hsl(210, 40%, 98%)",
"card": "hsl(0, 0%, 9%)",
"cardForeground": "hsl(210, 40%, 98%)",
"popover": "hsl(222.2, 84%, 11%)",
"popoverForeground": "hsl(210, 40%, 98%)",
"primary": "hsl(0, 100%, 53%)",
"primaryForeground": "hsl(222.2, 47.4%, 11.2%)",
"secondary": "hsl(217.2, 32.6%, 17.5%)",
"secondaryForeground": "hsl(210, 40%, 98%)",
"muted": "hsl(217.2, 32.6%, 17.5%)",
"mutedForeground": "hsl(215, 20.2%, 65.1%)",
"accent": "hsl(217.2, 32.6%, 17.5%)",
"accentForeground": "hsl(210, 40%, 98%)",
"destructive": "hsl(0, 62.8%, 30.6%)",
"destructiveForeground": "hsl(210, 40%, 98%)",
"border": "hsl(217.2, 32.6%, 17.5%)",
"input": "hsl(217.2, 32.6%, 17.5%)",
"ring": "hsl(0, 100%, 53%)",
"sidebar": "hsl(0, 0%, 11%)",
"sidebarForeground": "hsl(210, 40%, 98%)",
"sidebarPrimary": "hsl(145, 65%, 50%)",
"sidebarPrimaryForeground": "hsl(0, 0%, 100%)",
"sidebarAccent": "hsl(217.2, 32.6%, 17.5%)",
"sidebarAccentForeground": "hsl(210, 40%, 98%)",
"sidebarBorder": "hsl(217.2, 32.6%, 17.5%)",
"sidebarRing": "hsl(0, 100%, 53%)"
},
"borderRadius": "0.5rem",
"typography": {
"fontFamily": "Inter, sans-serif",
"headingFont": "Inter, sans-serif"
},
"keyColors": {
"primary": "#FF0000",
"background": "#1a1a1a",
"card": "#171717",
"sidebar": "#1c1c1c",
"border": "#2a2a35",
"text": "#e8e8ef",
"mutedText": "#888888",
"glow": "#39ff14"
},
"effects": {
"starfield": true,
"starTwinkle": true,
"starDrift": true,
"textGlow": "#39ff14"
}
}
+12 -8
View File
@@ -26,8 +26,10 @@ import {
import { DashboardStats } from "@/types"
import { CrossedLightsabers } from "@/components/dashboard/crossed-lightsabers"
import { StarField } from "@/components/dashboard/star-field"
import { useWebsiteTheme } from "@/providers/website-theme-provider"
export default function DashboardPage() {
const { websiteTheme: theme } = useWebsiteTheme()
const [period, setPeriod] = useState("6months")
const [stats, setStats] = useState<DashboardStats | null>(null)
const pollingRef = useRef<NodeJS.Timeout | null>(null)
@@ -65,20 +67,22 @@ export default function DashboardPage() {
return (
<div className="space-y-6 relative">
<StarField />
{theme === "starforce" && <StarField />}
<div className="relative z-[1] space-y-6">
<div>
<PageHeader
title={<span style={{fontFamily:"'Audiowide',sans-serif"}}>Dashboard</span>}
description="Overview of your sales pipeline"
>
<span
className="pointer-events-none select-none text-5xl text-gray-600 dark:text-[#b0f0c0] tracking-wider font-bold text-glow whitespace-nowrap"
style={{ fontFamily: "'Cormorant Garamond', serif" }}
>
Trust the Force, Find your Path
</span>
<CrossedLightsabers />
{theme === "starforce" && (
<span
className="pointer-events-none select-none text-5xl text-gray-600 dark:text-[#b0f0c0] tracking-wider font-bold text-glow whitespace-nowrap"
style={{ fontFamily: "'Cormorant Garamond', serif" }}
>
Trust the Force, Find your Path
</span>
)}
{theme === "starforce" && <CrossedLightsabers />}
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="h-9 w-[160px]">
<ListFilter className="mr-2 h-4 w-4" />
+2 -2
View File
@@ -12,7 +12,7 @@ export async function GET() {
[user.id],
)
const websiteTheme = result.rows[0]?.website_theme || "spidey"
const websiteTheme = result.rows[0]?.website_theme || "starforce"
return NextResponse.json({ websiteTheme })
} catch (error) {
console.error("Website theme GET error:", error)
@@ -26,7 +26,7 @@ export async function PUT(request: NextRequest) {
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const body = await request.json()
const theme = body.websiteTheme || "spidey"
const theme = body.websiteTheme || "starforce"
await query(
`UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`,
-29
View File
@@ -406,35 +406,6 @@ main {
z-index: 0;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(1px 1px at 10% 20%, rgba(100,100,120,0.4) 0%, transparent 100%),
radial-gradient(1px 1px at 20% 40%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 30% 10%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 40% 60%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 50% 80%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 60% 30%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 70% 50%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 90% 15%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 15% 70%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 25% 90%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 35% 35%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 45% 15%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 55% 75%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 65% 55%, rgba(80,80,100,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 75% 25%, rgba(100,100,120,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 85% 65%, rgba(90,90,110,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 95% 45%, rgba(80,80,100,0.5) 0%, transparent 100%);
background-size: 200% 200%;
animation: drift 60s linear infinite;
}
@keyframes drift {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
+7 -2
View File
@@ -1,8 +1,11 @@
import type { Metadata, Viewport } from "next"
import { Inter } from "next/font/google"
import { ThemeProvider } from "@/providers/theme-provider"
import { WebsiteThemeProvider } from "@/providers/website-theme-provider"
import { Toaster } from "@/components/ui/sonner"
import "./globals.css"
import "../../Web_Backgrounds/spidey-theme.css"
import "../../Web_Backgrounds/starforce-theme.css"
const inter = Inter({
variable: "--font-inter",
@@ -31,8 +34,10 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} min-h-screen antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
{children}
<Toaster />
<WebsiteThemeProvider>
{children}
<Toaster />
</WebsiteThemeProvider>
</ThemeProvider>
</body>
</html>
+4 -1
View File
@@ -33,6 +33,8 @@ import {
Trash2,
} from "lucide-react";
import { CrmIcon } from "./crm-icon";
import { CaptainAmericaShield } from "@/components/shared/captain-america-shield";
import { useWebsiteTheme } from "@/providers/website-theme-provider";
interface TopbarProps {
onMenuClick: () => void;
@@ -41,6 +43,7 @@ interface TopbarProps {
export function Topbar({ onMenuClick }: TopbarProps) {
const router = useRouter();
const { theme, setTheme } = useTheme();
const { websiteTheme } = useWebsiteTheme();
const { user, logout } = useUser();
const { notifications, unreadCount, markAsRead, markAllAsRead, dismiss } =
useNotifications();
@@ -74,7 +77,7 @@ export function Topbar({ onMenuClick }: TopbarProps) {
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-card dark:bg-[#141414] px-4 lg:px-6 relative overflow-hidden">
{/* Logo */}
<div className="flex items-center gap-2 mr-2 shrink-0">
<CrmIcon />
{websiteTheme === "spidey" ? <CaptainAmericaShield /> : <CrmIcon />}
<span className="text-[#CC0000] dark:text-[#FF1111] font-bold text-sm tracking-wide">
CRM
</span>
+38 -1
View File
@@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { cn } from "@/lib/utils"
import { Sun, Moon, Monitor } from "lucide-react"
import { Sun, Moon, Monitor, Sparkles, Shield } from "lucide-react"
import { useWebsiteTheme } from "@/providers/website-theme-provider"
const COLOR_THEME_KEY = "crm-color-theme"
@@ -29,6 +30,11 @@ const themeOptions = [
{ value: "ruby", label: "Ruby", color: "bg-red-600", ring: "ring-red-600" },
]
const backgroundOptions = [
{ value: "spidey", label: "Spidey", icon: Shield, color: "bg-red-600", ring: "ring-red-600", desc: "Dark theme with red accents" },
{ value: "starforce", label: "StarForce", icon: Sparkles, color: "bg-red-700", ring: "ring-red-700", desc: "Cosmic dark with starfield & green glow" },
]
function getStoredColorTheme(): string {
if (typeof window === "undefined") return "default"
return localStorage.getItem(COLOR_THEME_KEY) || "default"
@@ -46,6 +52,7 @@ function applyColorTheme(theme: string) {
export function ThemeSettings() {
const { theme, setTheme } = useTheme()
const { websiteTheme, setWebsiteTheme } = useWebsiteTheme()
const [colorTheme, setColorTheme] = useState("default")
const [mounted, setMounted] = useState(false)
@@ -119,6 +126,36 @@ export function ThemeSettings() {
</RadioGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Color Theme for Background</CardTitle>
<CardDescription>
Choose the website background theme.
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={websiteTheme} onValueChange={setWebsiteTheme} className="grid grid-cols-2 gap-4">
{backgroundOptions.map(({ value, label, icon: Icon, color, ring, desc }) => (
<div key={value}>
<RadioGroupItem value={value} id={`bg-${value}`} className="peer sr-only" />
<Label
htmlFor={`bg-${value}`}
className={cn(
"flex flex-col items-center gap-3 rounded-lg border-2 p-4 hover:bg-accent cursor-pointer transition-all",
"peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5"
)}
>
<Icon className="h-6 w-6" />
<div className={cn("h-8 w-8 rounded-full", color, ring, "ring-2 ring-offset-2 ring-offset-background")} />
<span className="text-sm font-medium">{label}</span>
<span className="text-xs text-muted-foreground">{desc}</span>
</Label>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,31 @@
"use client"
export function CaptainAmericaShield() {
return (
<svg
width="28"
height="28"
viewBox="0 0 100 100"
aria-hidden="true"
>
<defs>
<clipPath id="shield-clip">
<polygon points="50,95 5,60 5,30 50,5 95,30 95,60" />
</clipPath>
</defs>
<g clip-path="url(#shield-clip)">
<circle cx="50" cy="45" r="45" fill="#c62828" />
<circle cx="50" cy="45" r="35" fill="#efefef" />
<circle cx="50" cy="45" r="25" fill="#c62828" />
<circle cx="50" cy="45" r="15" fill="#1565c0" />
<polygon points="50,32 56,47 72,48 60,58 64,75 50,64 36,75 40,58 28,48 44,47" fill="#efefef" />
</g>
<polygon
points="50,95 5,60 5,30 50,5 95,30 95,60"
fill="none"
stroke="#c62828"
stroke-width="1.5"
/>
</svg>
)
}
+5 -4
View File
@@ -5,7 +5,8 @@ import { createContext, useContext, useState, useEffect, ReactNode, useCallback
const WEBSITE_THEME_KEY = "crm-website-theme"
const themeClasses: Record<string, string> = {
spidey: "",
spidey: "theme-spidey",
starforce: "theme-starforce",
}
interface WebsiteThemeContextValue {
@@ -38,7 +39,7 @@ function storeTheme(theme: string) {
}
export function WebsiteThemeProvider({ children }: { children: ReactNode }) {
const [websiteTheme, setWebsiteThemeState] = useState<string>("spidey")
const [websiteTheme, setWebsiteThemeState] = useState<string>("starforce")
const [loading, setLoading] = useState(true)
useEffect(() => {
@@ -52,13 +53,13 @@ export function WebsiteThemeProvider({ children }: { children: ReactNode }) {
fetch("/api/settings/website-theme")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
const theme = data?.websiteTheme || "spidey"
const theme = data?.websiteTheme || "starforce"
setWebsiteThemeState(theme)
storeTheme(theme)
applyWebsiteTheme(theme)
})
.catch(() => {
applyWebsiteTheme("spidey")
applyWebsiteTheme("starforce")
})
.finally(() => setLoading(false))
}, [])