Compare commits

...

20 Commits

Author SHA1 Message Date
19a9014f61 Change dark and light theme like ubuntu 2026-02-12 13:12:15 +01:00
2cc52a3fab style: refine greeting section with peace blessing and new verse intro 2026-02-08 02:22:06 +01:00
4c832549f2 fix(layout): handle localized routes in page title logic 2026-02-08 02:20:56 +01:00
3f7369fc86 Add greetings to the Dashboard for logged in users 2026-02-06 22:57:25 +01:00
d77046194a Added users section to en 2026-02-05 11:54:19 +01:00
6f1d93fc07 AI generated context file 2026-02-05 11:54:03 +01:00
01cbaab498 Adapt params type again 2026-02-03 15:26:41 +01:00
9ac415145d Adapt props type 2026-02-03 15:22:56 +01:00
85b618836b Add next-intl to internationalize our project 2026-02-03 15:11:59 +01:00
a073665dc4 Add user creation form with adaptive Dialog/Drawer and Zod validation 2026-02-02 00:30:18 +01:00
288cac4432 fix title color 2026-01-31 09:10:41 +01:00
8ab553ac6c fix 2026-01-31 01:42:58 +01:00
712a1a2937 feat: implement dynamic mobile header with centered page title and adaptive layout 2026-01-31 01:40:28 +01:00
f26dee782c change version von span to badge component 2026-01-30 22:20:18 +01:00
a13c40bfc1 build: add ui command to Makefile for shadcn components 2026-01-30 22:15:46 +01:00
dd8dca4efb feat: create initial users page and update ModuleCard to support navigation 2026-01-30 00:04:10 +01:00
c77682ff46 style: fix badge alignment and stats card responsivenes 2026-01-29 23:11:14 +01:00
e4b5771b8c Open and close menu on small devices 2026-01-29 11:42:53 +01:00
66e3f5a3d1 Add prettier and plugin for tailwind; Adapted global css for light and dark theme 2026-01-28 11:12:26 +01:00
30228bf6b1 Use theme provider to allow users use systems theme 2026-01-28 09:00:41 +01:00
43 changed files with 3480 additions and 289 deletions

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": [
"prettier-plugin-tailwindcss"
]
}

87
AGENTS.md Normal file
View File

@@ -0,0 +1,87 @@
# AGENTS.md
This file provides guidance to WARP (warp.dev) when working with code in this repository.
## Project Overview
CGR App - A Next.js 16 admin dashboard application with internationalization (i18n) support for English, German, and Russian. Uses React 19, TypeScript, Tailwind CSS v4, and shadcn/ui components.
## Commands
### Development
```bash
# Run dev server via Docker (preferred)
make run
# Or directly with npm
npm run dev
```
### Build & Lint
```bash
npm run build # Production build
npm run lint # ESLint
```
### Docker Operations
```bash
make run # Start dev container (docker-compose.yml)
make stop # Stop containers
make logs # View container logs
make build # Rebuild container
# Production deployment (uses docker-compose.prod.yml)
make ENV=prod deploy
```
### Adding Dependencies
```bash
# Install npm package via Docker
make i package=<package-name>
# Add shadcn/ui component via Docker
make ui component=<component-name>
```
## Architecture
### App Router Structure
```
app/
├── layout.tsx # Root layout (pass-through for i18n)
├── [locale]/ # Dynamic locale segment (en, de, ru)
│ ├── layout.tsx # Main layout with providers (Theme, i18n)
│ └── (backend)/ # Route group for admin dashboard
│ ├── layout.tsx # Dashboard shell (Sidebar + Header)
│ ├── page.tsx # Overview page
│ ├── admin/
│ └── users/
│ └── components/ # Page-specific components
```
### Key Patterns
**Internationalization (next-intl)**
- Routing config: `i18n/routing.ts` defines supported locales
- Request handler: `i18n/request.ts` loads messages dynamically
- Navigation: Use `@/i18n/navigation` exports (`Link`, `useRouter`, `usePathname`) for locale-aware navigation
- Messages: JSON files in `messages/` directory (`en.json`, `de.json`, `ru.json`)
**Component Organization**
- `components/ui/` - shadcn/ui primitives (new-york style, RSC enabled)
- `components/` - App-specific components (Header, Sidebar, etc.)
- Page-specific components live under their route: `app/[locale]/(backend)/users/components/`
**Providers Hierarchy** (in `app/[locale]/layout.tsx`)
```
ThemeProvider (next-themes) → NextIntlClientProvider → children
```
### Path Aliases
- `@/*` maps to project root (e.g., `@/components`, `@/lib/utils`, `@/hooks`)
## Styling
- Tailwind CSS v4 with CSS variables for theming
- `cn()` utility from `lib/utils.ts` for conditional class merging
- Global styles in `app/globals.css`

View File

@@ -3,6 +3,8 @@
ENV ?= dev
COMPOSE_FILE = docker-compose$(if $(filter $(ENV),prod),.prod,).yml
COMPOSE = docker compose -f $(COMPOSE_FILE)
# Service name of the dev container
SERVICE_NAME = app
.PHONY: up stop build deploy logs
@@ -30,4 +32,10 @@ logs:
$(COMPOSE) logs -f
clean:
docker image prune -f
docker image prune -f
ui:
$(COMPOSE) exec $(SERVICE_NAME) npx shadcn@latest add $(component)
i:
$(COMPOSE) exec $(SERVICE_NAME) npm install $(package)

View File

@@ -1,31 +0,0 @@
"use client";
import Header from "@/components/Header/index";
import Sidebar from "@/components/Sidebar";
import React, { useState } from "react";
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div data-cmp="DashboardLayout" className="min-h-screen bg-muted/30">
<Sidebar />
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div className="fixed inset-0 bg-black/50 z-40 md:hidden" onClick={() => setSidebarOpen(false)} />
)}
<div className={`md:pl-64 flex flex-col min-h-screen transition-all duration-300`}>
<Header onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<main className="flex-1 p-4 md:p-8 animate-in fade-in duration-500">{children}</main>
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,62 @@
"use client";
import Header from "@/components/Header/index";
import Sidebar from "@/components/Sidebar";
import React, { useState } from "react";
import { usePathname } from "next/navigation";
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const pathname = usePathname();
const getPageTitle = (path: string) => {
const pathSegments = path.split("/").filter(Boolean);
if (pathSegments.length <= 1) return "Overview";
const purePath = `/${pathSegments.slice(1).join("/")}`;
const titles: Record<string, string> = {
"/users": "Users",
"/settings": "Settings",
"/profile": "Profile",
"/songs": "Songs Database",
"/events": "Worship & Events",
};
return titles[purePath] || "Dashboard";
};
return (
<div className="dashboard">
<Sidebar open={sidebarOpen} close={() => setSidebarOpen(false)} />
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 md:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div
className={`flex min-h-screen flex-col transition-all duration-300 md:pl-64`}
>
<Header
onMenuClick={() => setSidebarOpen(!sidebarOpen)}
pageTitle={getPageTitle(pathname)}
/>
<main className="animate-in fade-in flex-1 p-4 duration-500 md:p-8">
{children}
</main>
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,32 @@
// Local data storage for development without DB
// Each object represents a verse for a specific day
export interface localizedContent {
en: string;
de: string;
ru: string;
}
export interface BibleVerse {
reference: string;
content: localizedContent;
}
export const bibleVerses: BibleVerse[] = [
{
reference: "Epheser 2:19-22",
content: {
en: "So then you are no longer strangers and aliens, but you are fellow citizens with the saints and members of the household of God, built on the foundation of the apostles and prophets, Christ Jesus himself being the cornerstone, in whom the whole structure, being joined together, grows into a holy temple in the Lord. In him you also are being built together into a dwelling place for God by the Spirit.",
de: "So seid ihr nun nicht mehr Gäste und Fremdlinge, sondern Mitbürger der Heiligen und Gottes Hausgenossen, erbaut auf den Grund der Apostel und Propheten, da Jesus Christus der Eckstein ist, auf welchem der ganze Bau ineinandergefügt wächst zu einem heiligen Tempel in dem Herrn. Durch ihn werdet auch ihr mit erbaut zu einer Wohnung Gottes im Geist.",
ru: "Итак, вы уже не чужие и не пришельцы, но сограждане святым и свои Богу, быв утверждены на основании Апостолов и пророков, имея Самого Иисуса Христа краеугольным камнем, на котором всё здание, слагаясь стройно, возрастает в святой храм в Господе, на котором и вы устрояетесь в жилище Божие Духом."
}
},
{
reference: "Psalm 23:1",
content: {
en: "The Lord is my shepherd...",
de: "Der Herr ist mein Hirte...",
ru: "Господь — Пастырь мой..."
}
},
// Add as many as you want here
];

View File

@@ -2,6 +2,7 @@
import ModuleCard from "@/components/ModuleCard";
import StatCard from "@/components/StatCard";
import { Badge } from "@/components/ui/badge";
import { ModuleCategory } from "@/lib/types/dashboard";
import {
BookOpen,
@@ -20,9 +21,28 @@ import {
UserCircle,
Users,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import Link from "next/link";
import React from "react";
import { bibleVerses, localizedContent } from "./lib/dailyVerseData";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import Image from "next/image";
const Dashboard: React.FC = () => {
// 1. Get a unique number for the current day
const today = new Date();
today.setHours(0, 0, 0, 0); // Normalize to midnight
const dayTimestamp = today.getTime();
// 2. Use the timestamp to get a consistent index
// The modulo (%) operator ensures the index is always within array bounds
const verseIndex = dayTimestamp % bibleVerses.length;
const todayVerse = bibleVerses[verseIndex];
const params = useParams();
const lng = params.locale as keyof localizedContent || "en"; // Default to English if no locale is provided
const t = useTranslations("dashboard");
const categories: ModuleCategory[] = [
{
title: "People & Community",
@@ -51,7 +71,8 @@ const Dashboard: React.FC = () => {
{
id: "addresses",
title: "Addresses",
description: "Database of addresses for registered users and locations.",
description:
"Database of addresses for registered users and locations.",
icon: MapPin,
path: "/addresses",
},
@@ -63,14 +84,16 @@ const Dashboard: React.FC = () => {
{
id: "services",
title: "Services & Lectures",
description: "Schedule and manage church services and lecture events.",
description:
"Schedule and manage church services and lecture events.",
icon: Calendar,
path: "/services",
},
{
id: "lectures_list",
title: "Lectures List",
description: "Browse the complete archive of lectures from all communities.",
description:
"Browse the complete archive of lectures from all communities.",
icon: Mic2,
path: "/lectures",
},
@@ -136,7 +159,8 @@ const Dashboard: React.FC = () => {
{
id: "types",
title: "Lecture Types",
description: "Define and categorize different types of presentations.",
description:
"Define and categorize different types of presentations.",
icon: Tag,
path: "/types",
},
@@ -159,45 +183,76 @@ const Dashboard: React.FC = () => {
];
return (
<div data-cmp="Dashboard" className="max-w-7xl mx-auto space-y-8">
<div className="mx-auto max-w-7xl space-y-8">
{/* Welcome Section */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground tracking-tight">Community Dashboard</h1>
<p className="text-muted-foreground mt-1">
Welcome to the admin area. Manage your community resources safely.
</p>
<div data-cmp="ModuleCard" className="group bg-card border border-border rounded-xl p-6 h-full overflow-hidden">
<div className="animate-in items-baseline fade-in slide-in-from-bottom-4 duration-700 flex flex-col sm:flex-row grow pr-6 gap-2">
<span className="text-2xl md:text-3xl text-nowrap font-semibold leading-none">{t("greeting_name", { user: "Alexander" })}</span>
<span className="text-xl font-semibold text-nowrap leading-none"> {t("greeting")}</span>
</div>
<div className="flex space-x-3">
<span className="text-sm px-3 py-1 bg-primary/10 text-primary rounded-full font-medium">v2.4.0 Live</span>
<div className="flex-col mt-4">
<p>
<span className="font-semibold">{t("verse_intro")}</span> <br />
<i className="italic text-foreground">"{todayVerse.content[lng]}"</i>
</p>
<div className="flex justify-end mt-3">
<Badge variant="outline" className="rounded-sm self-start">
{todayVerse.reference}
</Badge>
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard label="Total Members" value="2,845" icon={Users} trend="up" change="12% this month" />
<StatCard label="Upcoming Services" value="24" icon={Calendar} trend="neutral" change="Next 7 days" />
<StatCard label="Songs Database" value="1,430" icon={Music} trend="up" change="5 new added" />
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<StatCard
label="Total Members"
value="2,845"
icon={Users}
trend="up"
change="12% this month"
/>
<StatCard
label="Upcoming Services"
value="24"
icon={Calendar}
trend="neutral"
change="Next 7 days"
/>
<StatCard
label="Songs Database"
value="1,430"
icon={Music}
trend="up"
change="5 new added"
/>
</div>
{/* Categories */}
<div className="space-y-10">
{categories.map((category) => (
<div key={category.title} className="animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="flex items-center space-x-2 mb-6">
<h2 className="text-xl font-semibold text-foreground border-l-4 border-primary pl-3">{category.title}</h2>
<div className="h-px bg-border flex-1 ml-4 opacity-50"></div>
<div
key={category.title}
className="animate-in fade-in slide-in-from-bottom-4 duration-700"
>
<div className="mb-6 flex items-center space-x-2">
<h2 className="text-foreground border-primary border-l-4 pl-3 text-xl font-semibold">
{category.title}
</h2>
<div className="bg-border ml-4 h-px flex-1 opacity-50"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{category.items.map((item) => (
<ModuleCard
key={item.id}
title={item.title}
description={item.description}
icon={item.icon}
onClick={() => console.log(`Navigating to ${item.path}`)}
/>
<Link key={item.id} href={item.path} className="block">
<ModuleCard
key={item.id}
title={item.title}
description={item.description}
icon={item.icon}
//onClick={() => console.log(`Navigating to ${item.path}`)}
/>
</Link>
))}
</div>
</div>

View File

@@ -0,0 +1,136 @@
'use client';
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
// Shadcn UI components
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
// 1. Schema definition
const formSchema = z.object({
email: z.string().email("Invalid email address."),
role: z.string().min(1, "Please select a role."), // Rollenbezeichnung
status: z.boolean().default(true), // Status (Active/Inactive)
password: z.string().min(8, "Password must be at least 8 characters."),
})
export type FormValues = z.infer<typeof formSchema>
export default function UserForm() {
// 2. Initialize the form
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
role: "user",
status: true,
password: "",
},
})
// 3. Define the submit handler
function onSubmit(values: FormValues) {
// All comments in code must be in English
// Later we will connect this to a Server Action
console.log("Form values:", values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-4">
{/* Email Field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="example@mail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Role Select Field */}
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role (Rollenbezeichnung)</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Status Switch Field */}
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Active Status</FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* Password Field */}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Save User</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,73 @@
"use client"
import * as React from "react"
import { Plus } from "lucide-react"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import UserForm from "./UserForm"
export function UserFormWrapper() {
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery("(min-width: 768px)")
// Common button content
const TriggerButton = (
<Button
variant="default"
className="bg-secondary hover:bg-secondary/90 border border-border rounded-lg cursor-pointer h-10"
>
<Plus className="text-muted-foreground size-5 md:mr-2" />
<span className="hidden md:inline text-sm text-muted-foreground">
Add New User
</span>
</Button>
)
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{TriggerButton}
</DialogTrigger>
<DialogContent className="sm:max-w-106.25 bg-card text-white border-border">
<DialogHeader>
<DialogTitle>Add New User</DialogTitle>
</DialogHeader>
<UserForm />
</DialogContent>
</Dialog>
)
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
{TriggerButton}
</DrawerTrigger>
<DrawerContent className="bg-card border-border text-white max-h-[96dvh]">
<DrawerHeader className="text-left">
<DrawerTitle>Add New User</DrawerTitle>
</DrawerHeader>
<div className="px-4 overflow-y-auto pb-4">
<UserForm />
</div>
</DrawerContent>
</Drawer>
)
}

View File

@@ -0,0 +1,62 @@
'use client';
import { MoreVertical, Mail, Shield, Calendar } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
interface UserMobileCardProps {
name: string;
email: string;
role: string;
joinedAt?: string;
status: 'active' | 'inactive';
}
export const UserMobileCard: React.FC<UserMobileCardProps> = ({ name, email, role, joinedAt, status }) => {
return (
<div className="bg-card border border-border rounded-xl p-4 shadow-sm md:hidden active:scale-[0.99] transition-all">
{/* Header section */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
{/* Avatar will be here */}
<div className="size-11 rounded-full bg-primary/10 flex items-center justify-center border border-primary/20">
<span className="text-primary font-bold text-sm">
{name.split(" ").map(n => n[0]).join("")}
</span>
</div>
<div className="flex flex-col">
<span className="font-semibold text-foreground leading-none">{name}</span>
<span className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
<Mail className="size-3" />
{email}
</span>
</div>
</div>
<Badge
variant={status === "active" ? "default" : "secondary"}
className="text-[10px] px-2 py-0 h-5"
>
{status}
</Badge>
</div>
{/* Footer section: role and options */}
<div className="flex items-center justify-between mt-5 pt-3 border-t border-border/50">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Shield className="size-3 text-primary/70" />
<span>Role: <b className="text-foreground font-medium">{role}</b></span>
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/80">
<Calendar className="size-3" />
<span>Joined: {joinedAt}</span>
</div>
<Button variant="ghost" size="icon-xs" className="text-muted-foreground">
<MoreVertical className="size-4" />
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,185 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Filter, MoreVertical, Search } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { UserFormWrapper } from "./components/UserFormWrapper";
import { UserMobileCard } from "./components/UserMobileCard";
// Temporary user data for demonstration
const tempUsers = [
{
id: 1,
name: "John Doe",
email: "john@example.com",
role: "Admin",
joinedAt: "2023-01-01",
status: "active",
},
{
id: 2,
name: "Alice Smith",
email: "alice@test.com",
role: "Editor",
joinedAt: "2023-02-15",
status: "active",
},
{
id: 3,
name: "Bob Johnson",
email: "bob@community.com",
role: "Member",
joinedAt: "2023-03-10",
status: "inactive",
},
{
id: 4,
name: "Charlie Brown",
email: "charlie@community.com",
role: "Member",
joinedAt: "2023-04-20",
status: "active",
},
];
export default function UsersPage() {
const [open, setOpen] = useState(false);
const t = useTranslations("users");
return (
<div className="space-y-6">
<div className="flex flex-col gap-6 p-0 md:gap-10">
{/* Header: Title + Button for Desktop only */}
<div className="hidden items-center justify-between md:flex">
<div className="space-y-1">
<h1 className="text-4xl font-bold tracking-tight text-white">
{t("title")}
</h1>
<p className="text-muted-foreground mt-3 hidden max-w-2xl text-lg md:block">
{t("welcome")}
</p>
</div>
{/* Button: Add New User for Desktop only */}
<UserFormWrapper />
</div>
{/* Search + Filter */}
<div className="flex gap-3 md:gap-4">
<div className="group relative w-full">
<Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input
className="bg-card border-border h-10 w-full rounded-lg border py-1 pr-12 pl-10 focus:outline-1"
placeholder="Search users..."
/>
<div className="absolute top-1/2 right-2 -translate-y-1/2">
<Button
variant="ghost"
size="icon"
className="hover:bg-muted text-muted-foreground size-8 transition-colors hover:text-white md:size-10"
>
<Filter className="size-4 md:size-5" />
<span className="sr-only">Filters</span>
</Button>
</div>
</div>
{/* Button: Add New User for Mobile only */}
<div className="md:hidden">
<UserFormWrapper />
</div>
</div>
</div>
{/* --- For Mobile Phones --- */}
<div className="flex flex-col gap-4 md:hidden">
{tempUsers.map((user) => (
<UserMobileCard
key={user.id}
name={user.name}
email={user.email}
role={user.role}
joinedAt={user.joinedAt}
status={user.status as "active" | "inactive"}
/>
))}
</div>
{/* Users List / Table */}
<div className="bg-card border-border hidden overflow-hidden rounded-xl border shadow-sm md:block">
<table className="w-full text-left">
<thead className="bg-muted/50 border-border border-b">
<tr>
<th className="text-muted-foreground px-6 py-3 text-xs font-semibold tracking-wider uppercase">
User
</th>
<th className="text-muted-foreground px-6 py-3 text-xs font-semibold tracking-wider uppercase">
Role
</th>
<th className="text-muted-foreground px-6 py-3 text-xs font-semibold tracking-wider uppercase">
Status
</th>
<th className="text-muted-foreground px-6 py-3 text-xs font-semibold tracking-wider uppercase">
Joined
</th>
<th className="px-6 py-3"></th>
</tr>
</thead>
<tbody className="divide-border divide-y">
{/* Example Row */}
{tempUsers.map((user) => (
<tr
key={user.id}
className="hover:bg-muted/20 cursor-pointer transition-colors"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="bg-primary/20 text-primary flex h-10 w-10 items-center justify-center rounded-full font-bold">
{user.name.charAt(0)}
</div>
<div>
<div className="text-foreground text-sm font-medium">
{user.name}
</div>
<div className="text-muted-foreground text-xs">
{user.email}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<span className="text-muted-foreground text-sm">
{user.role}
</span>
</td>
<td className="px-6 py-4">
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
user.status === "active"
? "bg-green-500/10 text-green-500"
: "bg-red-500/10 text-red-500"
}`}
>
{user.status}
</span>
</td>
<td className="text-muted-foreground px-6 py-4 text-sm">
{user.joinedAt}
</td>
<td className="px-6 py-4 text-right">
<button
title="More options"
className="hover:bg-muted rounded-full p-2 transition-colors"
>
<MoreVertical className="text-muted-foreground h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

45
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { routing } from "@/i18n/routing";
import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { ThemeProvider } from "next-themes";
import { Geist, Geist_Mono } from "next/font/google";
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "CGR App",
description: "Manager for CGR Data",
};
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale?: string }>;
}>) {
const { locale } = await params;
return (
<html lang={locale || routing.defaultLocale} suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider attribute="class" enableSystem>
<NextIntlClientProvider locale={locale || routing.defaultLocale}>
{children}
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -44,72 +44,114 @@
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
--radius: 0.125rem;
/* ---------- Base background (warm light) ---------- */
--background: oklch(0.97 0.01 95);
--foreground: oklch(0.22 0.02 30);
/* ---------- Surfaces ---------- */
--card: oklch(0.99 0.005 95);
--card-foreground: oklch(0.22 0.02 30);
--popover: oklch(1 0.004 95);
--popover-foreground: oklch(0.22 0.02 30);
/* ---------- Ubuntu orange primary ---------- */
--primary: oklch(0.62 0.2 45);
--primary-foreground: oklch(0.99 0.005 95);
/* ---------- Secondary / muted warm grays ---------- */
--secondary: oklch(0.92 0.01 90);
--secondary-foreground: oklch(0.3 0.02 35);
--muted: oklch(0.94 0.008 90);
--muted-foreground: oklch(0.45 0.015 35);
--accent: oklch(0.9 0.015 85);
--accent-foreground: oklch(0.3 0.02 35);
/* ---------- Destructive ---------- */
--destructive: oklch(0.58 0.23 25);
/* ---------- Borders / inputs ---------- */
--border: oklch(0.85 0.01 90);
--input: oklch(0.88 0.01 90);
--ring: oklch(0.62 0.2 45 / 0.5);
/* ---------- Charts ---------- */
--chart-1: oklch(0.62 0.2 45); /* orange */
--chart-2: oklch(0.55 0.16 150); /* green */
--chart-3: oklch(0.7 0.17 85); /* yellow */
--chart-4: oklch(0.6 0.18 300); /* purple */
--chart-5: oklch(0.6 0.2 20); /* red */
/* ---------- Sidebar ---------- */
--sidebar: oklch(0.95 0.01 90);
--sidebar-foreground: oklch(0.28 0.02 30);
--sidebar-primary: oklch(0.62 0.2 45);
--sidebar-primary-foreground: oklch(0.99 0.005 95);
--sidebar-accent: oklch(0.9 0.015 85);
--sidebar-accent-foreground: oklch(0.3 0.02 35);
--sidebar-border: oklch(0.85 0.01 90);
--sidebar-ring: oklch(0.62 0.2 45 / 0.5);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
/* ---------- Base background (warm charcoal) ---------- */
--background: oklch(0.11 0.015 30);
--foreground: oklch(0.97 0.01 95);
/* ---------- Surfaces ---------- */
--card: oklch(0.16 0.018 32);
--card-foreground: oklch(0.97 0.01 95);
--popover: oklch(0.18 0.02 32);
--popover-foreground: oklch(0.97 0.01 95);
/* ---------- Ubuntu orange primary ---------- */
--primary: oklch(0.68 0.19 45);
--primary-foreground: oklch(0.98 0.01 95);
/* ---------- Secondary / muted warm grays ---------- */
--secondary: oklch(0.22 0.02 30);
--secondary-foreground: oklch(0.92 0.015 95);
--muted: oklch(0.22 0.02 30);
--muted-foreground: oklch(0.68 0.02 90);
--accent: oklch(0.26 0.025 35);
--accent-foreground: oklch(0.95 0.01 95);
/* ---------- Destructive (Ubuntu red tone) ---------- */
--destructive: oklch(0.62 0.22 25);
/* ---------- Borders / inputs ---------- */
--border: oklch(0.3 0.015 35 / 0.6);
--input: oklch(0.3 0.015 35 / 0.8);
--ring: oklch(0.68 0.19 45 / 0.6);
/* ---------- Charts ---------- */
--chart-1: oklch(0.68 0.19 45); /* orange */
--chart-2: oklch(0.7 0.15 150); /* green */
--chart-3: oklch(0.78 0.16 85); /* yellow */
--chart-4: oklch(0.66 0.18 300); /* purple */
--chart-5: oklch(0.7 0.18 20); /* red */
/* ---------- Sidebar (slightly darker & warmer) ---------- */
--sidebar: oklch(0.14 0.018 30);
--sidebar-foreground: oklch(0.95 0.01 95);
--sidebar-primary: oklch(0.68 0.19 45);
--sidebar-primary-foreground: oklch(0.98 0.01 95);
--sidebar-accent: oklch(0.24 0.02 32);
--sidebar-accent-foreground: oklch(0.95 0.01 95);
--sidebar-border: oklch(0.3 0.015 35 / 0.5);
--sidebar-ring: oklch(0.68 0.19 45 / 0.6);
}
/* === CUSTOM COLOR THEME === */
@@ -118,263 +160,349 @@
--color-gray-900: #050505;
--color-gray-800: #141414;
--color-gray-700: #212328;
--color-gray-600: #30333A;
--color-gray-500: #9095A1;
--color-gray-400: #CCDADC;
--color-gray-600: #30333a;
--color-gray-500: #9095a1;
--color-gray-400: #ccdadc;
--color-gray-100: #fafafa;
/* Vibrant Colors */
--color-blue-600: #5862FF;
--color-yellow-400: #FDD458;
--color-yellow-500: #E8BA40;
--color-teal-400: #0FEDBE;
--color-red-500: #FF495B;
--color-orange-500: #FF8243;
--color-purple-500: #D13BFF;
--color-blue-600: #5862ff;
--color-yellow-400: #fdd458;
--color-yellow-500: #e8ba40;
--color-teal-400: #0fedbe;
--color-red-500: #ff495b;
--color-orange-500: #ff8243;
--color-purple-500: #d13bff;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-gray-900 text-foreground;
@apply text-foreground bg-gray-900;
}
}
@layer utilities {
.dashboard {
@apply min-h-screen bg-gray-100 dark:bg-gray-900;
}
.container {
@apply mx-auto max-w-screen-2xl px-4 md:px-6 lg:px-8;
}
.yellow-btn {
@apply h-12 cursor-pointer bg-gradient-to-b from-yellow-400 to-yellow-500 hover:from-yellow-500 hover:to-yellow-400 text-gray-950 font-medium text-base rounded-lg shadow-lg disabled:opacity-50;
@apply h-12 cursor-pointer rounded-lg bg-gradient-to-b from-yellow-400 to-yellow-500 text-base font-medium text-gray-950 shadow-lg hover:from-yellow-500 hover:to-yellow-400 disabled:opacity-50;
}
.home-wrapper {
@apply text-gray-400 flex-col gap-4 md:gap-10 items-center sm:items-start;
@apply flex-col items-center gap-4 text-gray-400 sm:items-start md:gap-10;
}
.home-section {
@apply w-full gap-8 grid-cols-1 md:grid-cols-2 xl:grid-cols-3;
@apply w-full grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3;
}
.header {
@apply z-50 w-full h-[70px] bg-gray-800;
@apply z-50 bg-white dark:bg-gray-800;
}
.side-bar {
@apply z-50 bg-white dark:bg-gray-800;
}
.header-wrapper {
@apply flex justify-between items-center px-6 py-4 text-gray-500;
@apply flex items-center justify-between px-6 py-4 text-gray-500;
}
.auth-layout {
@apply flex flex-col justify-between lg:flex-row h-screen bg-gray-900 relative overflow-hidden;
@apply relative flex h-screen flex-col justify-between overflow-hidden bg-gray-900 lg:flex-row;
}
.auth-logo {
@apply pt-6 lg:pt-8 mb-8 lg:mb-12;
@apply mb-8 pt-6 lg:mb-12 lg:pt-8;
}
.auth-left-section {
@apply w-full lg:w-[45%] lg:h-screen px-6 lg:px-16 flex flex-col overflow-y-auto;
@apply flex w-full flex-col overflow-y-auto px-6 lg:h-screen lg:w-[45%] lg:px-16;
}
.auth-right-section {
@apply w-full max-lg:border-t max-lg:border-gray-600 lg:w-[55%] lg:h-screen bg-gray-800 px-6 py-4 md:p-6 lg:py-12 lg:px-18 flex flex-col justify-start;
@apply flex w-full flex-col justify-start bg-gray-800 px-6 py-4 max-lg:border-t max-lg:border-gray-600 md:p-6 lg:h-screen lg:w-[55%] lg:px-18 lg:py-12;
}
.auth-blockquote {
@apply text-sm md:text-xl lg:text-2xl font-medium text-gray-400 mb-1 md:mb-6 lg:mb-8;
@apply mb-1 text-sm font-medium text-gray-400 md:mb-6 md:text-xl lg:mb-8 lg:text-2xl;
}
.auth-testimonial-author {
@apply text-xs md:text-lg font-bold text-gray-400 not-italic;
@apply text-xs font-bold text-gray-400 not-italic md:text-lg;
}
.auth-dashboard-preview {
@apply border-6 border-gray-800 left-0 hidden w-[1024px] h-auto max-w-none lg:block rounded-xl shadow-2xl;
@apply left-0 hidden h-auto w-[1024px] max-w-none rounded-xl border-6 border-gray-800 shadow-2xl lg:block;
}
.form-title {
@apply text-4xl font-bold text-gray-400 mb-10;
@apply mb-10 text-4xl font-bold text-gray-400;
}
.form-label {
@apply text-sm font-medium text-gray-400;
}
.form-input {
@apply h-12 px-3 py-3 text-white text-base placeholder:text-gray-500 border-gray-600 bg-gray-800 rounded-lg focus:!border-yellow-500 focus:ring-0;
@apply rounded-lg !border-yellow-500 border-gray-600 bg-gray-800 px-3 py-3 text-base text-white placeholder:text-gray-500 focus:h-12 focus:ring-0;
}
.select-trigger {
@apply w-full !h-12 px-3 py-3 text-base border-gray-600 bg-gray-800 text-white rounded-lg focus:!border-yellow-500 focus:ring-0;
@apply w-full rounded-lg !border-yellow-500 border-gray-600 bg-gray-800 px-3 py-3 text-base text-white focus:!h-12 focus:ring-0;
}
.country-select-trigger {
@apply h-12 px-3 py-3 text-base w-full justify-between font-normal border-gray-600 bg-gray-800 text-gray-400 rounded-lg focus:!border-yellow-500 focus:ring-0;
@apply w-full justify-between rounded-lg !border-yellow-500 border-gray-600 bg-gray-800 px-3 py-3 text-base font-normal text-gray-400 focus:h-12 focus:ring-0;
}
.country-select-input {
@apply !bg-gray-800 text-gray-400 border-0 border-b border-gray-600 rounded-none focus:ring-0 placeholder:text-gray-500;
@apply rounded-none border-0 border-b border-gray-600 !bg-gray-800 text-gray-400 placeholder:text-gray-500 focus:ring-0;
}
.country-select-empty {
@apply text-gray-500 py-6 text-center !bg-gray-800;
@apply !bg-gray-800 py-6 text-center text-gray-500;
}
.country-select-item {
@apply text-white cursor-pointer px-3 py-2 rounded-sm bg-gray-800 hover:!bg-gray-600;
@apply rounded-sm !bg-gray-600 bg-gray-800 px-3 py-2 text-white hover:cursor-pointer;
}
.footer-link {
@apply text-gray-400 font-medium hover:text-yellow-400 hover:underline transition-colors;
@apply font-medium text-gray-400 transition-colors hover:text-yellow-400 hover:underline;
}
.search-text {
@apply cursor-pointer hover:text-yellow-500;
}
.search-btn {
@apply cursor-pointer px-4 py-2 w-fit flex items-center gap-2 text-sm md:text-base bg-yellow-500 hover:bg-yellow-500 text-black font-medium rounded;
@apply flex w-fit cursor-pointer items-center gap-2 rounded bg-yellow-500 px-4 py-2 text-sm font-medium text-black hover:bg-yellow-500 md:text-base;
}
.search-dialog {
@apply !bg-gray-800 lg:min-w-[800px] border-gray-600 fixed top-10 left-1/2 -translate-x-1/2 translate-y-10;
@apply fixed top-10 left-1/2 -translate-x-1/2 translate-y-10 border-gray-600 !bg-gray-800 lg:min-w-[800px];
}
.search-field {
@apply !bg-gray-800 border-b border-gray-600 relative;
@apply relative border-b border-gray-600 !bg-gray-800;
}
.search-list {
@apply !bg-gray-800 max-h-[400px];
@apply max-h-[400px] !bg-gray-800;
}
.search-list-indicator {
@apply px-5 py-2
@apply px-5 py-2;
}
.search-list-empty {
@apply py-6 !bg-transparent text-center text-gray-500;
@apply !bg-transparent py-6 text-center text-gray-500;
}
.search-input {
@apply !bg-gray-800 border-0 text-gray-400 placeholder:text-gray-500 focus:ring-0 text-base h-14 pr-10;
@apply h-14 border-0 !bg-gray-800 pr-10 text-base text-gray-400 placeholder:text-gray-500 focus:ring-0;
}
.search-loader {
@apply absolute right-12 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 animate-spin;
@apply absolute top-1/2 right-12 h-4 w-4 -translate-y-1/2 animate-spin text-gray-500;
}
.search-count {
@apply py-2 px-4 text-sm font-medium text-gray-400 bg-gray-700 border-b border-gray-700;
@apply border-b border-gray-700 bg-gray-700 px-4 py-2 text-sm font-medium text-gray-400;
}
.search-item {
@apply rounded-none my-3 px-1 w-full data-[selected=true]:bg-gray-600;
@apply my-3 w-full rounded-none px-1 data-[selected=true]:bg-gray-600;
}
.search-item-link {
@apply px-2 w-full cursor-pointer border-b border-gray-600 last:border-b-0 transition-colors flex items-center gap-3;
@apply flex w-full cursor-pointer items-center gap-3 border-b border-gray-600 px-2 transition-colors last:border-b-0;
}
.search-item-name {
@apply font-medium text-base text-gray-400;
@apply text-base font-medium text-gray-400;
}
.nav-list {
@apply flex flex-col sm:flex-row p-2 gap-3 sm:gap-10 font-medium;
@apply flex flex-col gap-3 p-2 font-medium sm:flex-row sm:gap-10;
}
.stock-details-container {
@apply w-full grid-cols-1 gap-6 xl:grid-cols-3 space-y-6 sm:space-y-8;
@apply w-full grid-cols-1 gap-6 space-y-6 sm:space-y-8 xl:grid-cols-3;
}
.watchlist-btn {
@apply bg-yellow-500 text-base hover:bg-yellow-500 text-gray-900 w-full rounded h-11 font-semibold cursor-pointer;
@apply h-11 w-full cursor-pointer rounded bg-yellow-500 text-base font-semibold text-gray-900 hover:bg-yellow-500;
}
.watchlist-remove {
@apply bg-red-500! hover:bg-red-500! text-gray-900!
@apply bg-red-500! text-gray-900! hover:bg-red-500!;
}
.watchlist-empty-container {
@apply container gap-8 flex-col items-center md:mt-10 p-6 text-center;
@apply container flex-col items-center gap-8 p-6 text-center md:mt-10;
}
.watchlist-empty {
@apply flex flex-col items-center justify-center text-center;
}
.watchlist-star {
@apply h-16 w-16 text-gray-500 mb-4;
@apply mb-4 h-16 w-16 text-gray-500;
}
.empty-title {
@apply text-xl font-semibold text-gray-400 mb-2;
@apply mb-2 text-xl font-semibold text-gray-400;
}
.empty-description {
@apply text-gray-500 mb-6 max-w-md;
@apply mb-6 max-w-md text-gray-500;
}
.watchlist-container {
@apply flex flex-col-reverse lg:grid lg:grid-cols-3 gap-8;
@apply flex flex-col-reverse gap-8 lg:grid lg:grid-cols-3;
}
.watchlist {
@apply lg:col-span-2 space-y-8;
@apply space-y-8 lg:col-span-2;
}
.watchlist-alerts {
@apply items-start gap-6 h-full flex-col w-full lg:col-span-1;
@apply h-full w-full flex-col items-start gap-6 lg:col-span-1;
}
.watchlist-icon-btn {
@apply w-fit cursor-pointer hover:bg-transparent! text-gray-400 hover:text-yellow-500;
@apply w-fit cursor-pointer text-gray-400 hover:bg-transparent! hover:text-yellow-500;
}
.watchlist-icon-added {
@apply !text-yellow-500 hover:!text-yellow-600;
@apply !text-yellow-600 hover:!text-yellow-500;
}
.watchlist-icon {
@apply w-8 h-8 rounded-full flex items-center justify-center bg-gray-700/50;
@apply flex h-8 w-8 items-center justify-center rounded-full bg-gray-700/50;
}
.trash-icon {
@apply h-4 w-4 text-gray-400 hover:text-red-400;
}
.star-icon {
@apply h-4 w-4;
}
.watchlist-title {
@apply text-xl md:text-2xl font-bold text-gray-100;
@apply text-xl font-bold text-gray-100 md:text-2xl;
}
.watchlist-table {
@apply !relative overflow-hidden !w-full bg-gray-800 border !border-gray-600 !rounded-lg;
@apply !relative !w-full overflow-hidden !rounded-lg border !border-gray-600 bg-gray-800;
}
.table-header-row {
@apply text-gray-400 font-medium bg-gray-700 border-b border-gray-600 hover:bg-gray-700;
@apply border-b border-gray-600 bg-gray-700 font-medium text-gray-400 hover:bg-gray-700;
}
.table-header:first-child {
@apply pl-4;
}
.table-row {
@apply border-b cursor-pointer text-gray-100 border-gray-600 hover:bg-gray-700/50 transition-colors;
@apply cursor-pointer border-b border-gray-600 text-gray-100 transition-colors hover:bg-gray-700/50;
}
.table-cell {
@apply font-medium text-base
@apply text-base font-medium;
}
.add-alert {
@apply flex text-sm items-center whitespace-nowrap gap-1.5 px-3 w-fit py-2 text-yellow-600 border border-yellow-600/20 rounded font-medium bg-transparent hover:bg-transparent cursor-pointer transition-colors;
@apply flex w-fit cursor-pointer items-center gap-1.5 rounded border border-yellow-600/20 bg-transparent px-3 py-2 text-sm font-medium whitespace-nowrap text-yellow-600 transition-colors hover:bg-transparent;
}
.watchlist-news {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4;
@apply grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3;
}
.news-item {
@apply bg-gray-800 rounded-lg border w-full border-gray-600 p-4 duration-200 hover:border-gray-600 cursor-pointer;
@apply w-full cursor-pointer rounded-lg border border-gray-600 bg-gray-800 p-4 duration-200 hover:border-gray-600;
}
.news-tag {
@apply inline-block w-fit px-2 py-1 mb-5 rounded bg-gray-600/60 text-green-500 text-sm font-mono font-medium;
@apply mb-5 inline-block w-fit rounded bg-gray-600/60 px-2 py-1 font-mono text-sm font-medium text-green-500;
}
.news-title {
@apply text-lg font-semibold text-gray-100 leading-tight mb-3 line-clamp-2;
@apply mb-3 line-clamp-2 text-lg leading-tight font-semibold text-gray-100;
}
.news-meta {
@apply flex items-center text-sm text-gray-500 mb-1;
@apply mb-1 flex items-center text-sm text-gray-500;
}
.news-summary {
@apply text-gray-400 flex-1 text-base leading-relaxed mb-3 line-clamp-3;
@apply mb-3 line-clamp-3 flex-1 text-base leading-relaxed text-gray-400;
}
.news-cta {
@apply text-sm align-bottom text-yellow-500 hover:text-gray-400;
@apply align-bottom text-sm text-yellow-500 hover:text-gray-400;
}
.alert-dialog {
@apply bg-gray-800 border-gray-600 text-gray-400 max-w-md;
@apply max-w-md border-gray-600 bg-gray-800 text-gray-400;
}
.alert-title {
@apply text-xl font-semibold text-gray-100;
}
.alert-list {
@apply overflow-y-auto w-full max-h-[911px] rounded-lg flex border border-gray-600 flex-col gap-4 bg-gray-800 p-3 flex-1;
@apply flex max-h-[911px] w-full flex-1 flex-col gap-4 overflow-y-auto rounded-lg border border-gray-600 bg-gray-800 p-3;
}
.alert-empty {
@apply px-6 py-8 text-center text-gray-500/50;
}
.alert-item {
@apply p-4 rounded-lg bg-gray-700 border border-gray-600;
@apply rounded-lg border border-gray-600 bg-gray-700 p-4;
}
.alert-name {
@apply mb-2 text-lg text-yellow-500 font-semibold;
@apply mb-2 text-lg font-semibold text-yellow-500;
}
.alert-details {
@apply flex border-b pb-3 items-center justify-between gap-3 mb-2;
@apply mb-2 flex items-center justify-between gap-3 border-b pb-3;
}
.alert-company {
@apply text-gray-400 text-base;
@apply text-base text-gray-400;
}
.alert-price {
@apply text-gray-100 font-bold;
@apply font-bold text-gray-100;
}
.alert-actions {
@apply flex items-end justify-between;
}
.alert-update-btn {
@apply text-gray-400 rounded-full bg-transparent hover:bg-green-500/15 cursor-pointer;
@apply cursor-pointer rounded-full bg-transparent text-gray-400 hover:bg-green-500/15;
}
.alert-delete-btn {
@apply text-gray-400 rounded-full hover:bg-red-600/15 bg-transparent cursor-pointer transition-colors;
@apply cursor-pointer rounded-full bg-transparent text-gray-400 transition-colors hover:bg-red-600/15;
}
}
@@ -403,7 +531,6 @@
.tradingview-widget-container__widget {
background-color: #141414 !important;
height: 100% !important;
}
.widget-stock-heatmap-container .screenerMapWrapper-BBVfGP0b {
@@ -451,7 +578,7 @@
}
.custom-chart.tradingview-widget-container iframe {
border: 1px solid #30333A;
border: 1px solid #30333a;
border-radius: 8px !important;
overflow: hidden !important;
}
@@ -477,13 +604,13 @@
}
.scrollbar-hide-default:hover {
scrollbar-color: #30333A transparent;
scrollbar-color: #30333a transparent;
}
.scrollbar-hide-default:hover::-webkit-scrollbar-thumb {
background-color: #30333A;
background-color: #30333a;
}
.scrollbar-hide-default::-webkit-scrollbar-thumb:hover {
background-color: #9095A1;
}
background-color: #9095a1;
}

View File

@@ -1,30 +1,11 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ReactNode } from "react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "CGR App",
description: "Manager for CGR Data",
type Props = {
children: ReactNode;
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
</html>
);
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({ children }: Props) {
return children;
}

View File

@@ -1,18 +1,40 @@
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import NavItems from "./NavItems";
import UserDropdown from "./UserDropdown";
const Header = () => {
const t = useTranslations();
return (
<header className="sticky top-0 header">
<div className="container header-wrapper">
<header className="header sticky top-0">
<div className="header-wrapper container">
<Link href="/">
<Image src="/logo.svg" alt="CGR Logo" width={100} height={100} className="h-8 w-auto cursor-pointer" />
<Image
src="/logo.svg"
alt={t("header.title")}
width={100}
height={100}
className="h-8 w-auto cursor-pointer"
/>
</Link>
<nav className="hidden sm:block">
<NavItems />
</nav>
<div className="mr-4 flex items-center gap-2">
<Link href="/" locale="en" className="text-sm">
EN
</Link>
<Link href="/" locale="de" className="text-sm">
DE
</Link>
<Link href="/" locale="rus" className="text-sm">
RU
</Link>
</div>
<UserDropdown />
</div>
</header>

View File

@@ -3,50 +3,76 @@ import React from "react";
interface HeaderProps {
onMenuClick: () => void;
pageTitle?: string;
}
const Header: React.FC<HeaderProps> = ({ onMenuClick }) => {
const Header: React.FC<HeaderProps> = ({ onMenuClick, pageTitle }) => {
const [hasError, setHasError] = React.useState(false);
return (
<header
data-cmp="Header"
className="h-16 bg-card border-b border-border sticky top-0 z-20 px-4 md:px-8 flex items-center justify-between"
className="header border-border bg-background/95 sticky top-0 z-20 flex h-16 items-center border-b px-4 backdrop-blur md:px-8"
>
<div className="flex items-center md:hidden">
<button title="Menu" onClick={onMenuClick} className="p-2 -ml-2 text-foreground hover:bg-muted rounded-md">
<Menu className="w-6 h-6" />
{/* 1. Left: Menu button (only on mobile) */}
<div className="flex flex-1 items-center md:hidden">
<button
title="Menu"
onClick={onMenuClick}
className="text-foreground hover:bg-muted -ml-2 rounded-md p-2"
>
<Menu className="h-6 w-6" />
</button>
</div>
<div className="hidden md:flex flex-1 max-w-xl relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
{/* 2. Center: Title (only on mobile) */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 md:hidden">
<span className="text-xl font-bold tracking-tight text-muted-foreground">
{pageTitle}
</span>
</div>
{/* 3. Desktop: Search */}
<div className="relative hidden max-w-xl flex-1 md:flex">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<input
type="text"
placeholder="Search modules, users, or files..."
className="w-full pl-10 pr-4 py-2 bg-muted/50 border-none rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all"
className="bg-muted/50 text-foreground focus:ring-primary/20 w-full rounded-lg border-none py-2 pr-4 pl-10 text-sm transition-all focus:ring-2 focus:outline-none"
/>
</div>
<div className="flex items-center space-x-4">
{/* 4. RIGHT SIDE: Notifications and Profile */}
<div className="flex flex-1 items-center justify-end space-x-4">
<button
title="Notifications"
className="relative p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-full transition-colors"
className="text-muted-foreground hover:text-foreground hover:bg-muted relative rounded-full p-2 transition-colors"
>
<Bell className="w-5 h-5" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full border-2 border-card"></span>
<Bell className="h-5 w-5" />
<span className="bg-destructive border-card absolute top-1.5 right-1.5 h-2 w-2 rounded-full border-2"></span>
</button>
<div className="flex items-center space-x-3 pl-4 border-l border-border/50">
<div className="text-right hidden sm:block">
<p className="text-sm font-medium text-foreground">Community Admin</p>
<p className="text-xs text-muted-foreground">Super Administrator</p>
</div>
<div className="w-9 h-9 bg-primary/10 rounded-full flex items-center justify-center border border-primary/20">
<User className="w-5 h-5 text-primary" />
<div className="border-border/50 flex items-center space-x-3 border-l pl-4">
<div className="hidden text-right sm:block text-nowrap">
<p className="text-foreground text-sm font-medium">Community Admin</p>
<p className="text-muted-foreground text-xs">Super Administrator</p>
</div>
{hasError ? (
<div className="w-10 h-10 rounded-full border border-border bg-muted flex items-center justify-center">
<User className="w-6 h-6 text-muted-foreground" />
</div>
) : (
<img
src="/images/IMG_84271.jpg"
alt="Profile"
className="w-10 h-10 rounded-full object-cover border border-border"
/* If image fails to load, set hasError to true */
onError={() => setHasError(true)}
/>
)}
</div>
</div>
</header>
);
};
export default Header;
export default Header;

View File

@@ -1,17 +1,21 @@
"use client";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { usePathname } from "next/navigation";
type NavItemProps = {
item: {
label: string;
labelKey: string;
href: string;
};
};
export const NavItem: React.FC<NavItemProps> = ({ item: { label, href } }) => {
export const NavItem: React.FC<NavItemProps> = ({
item: { labelKey, href },
}) => {
const pathname = usePathname();
const t = useTranslations();
const isActive = (path: string) => {
if (path === "/") return pathname === "/";
@@ -19,12 +23,13 @@ export const NavItem: React.FC<NavItemProps> = ({ item: { label, href } }) => {
};
const active = isActive(href);
const label = t(labelKey);
return (
<li key={href}>
<Link
href={href}
className={`hover:text-yellow-500 transition-colors ${active ? "text-gray-100 font-bold" : ""}`}
className={`transition-colors hover:text-yellow-500 ${active ? "font-bold text-gray-100" : ""}`}
>
{label}
</Link>

View File

@@ -1,4 +1,13 @@
import { Building2, Calendar, FileText, LayoutDashboard, LogOut, Music, Settings, Users } from "lucide-react";
import {
Building2,
Calendar,
FileText,
LayoutDashboard,
LogOut,
Music,
Settings,
Users,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
@@ -12,46 +21,60 @@ const navItems = [
{ icon: Settings, label: "Settings", path: "#settings" },
];
const Sidebar: React.FC = () => {
type SidebarProps = {
open?: boolean;
close: () => void;
};
const Sidebar: React.FC<SidebarProps> = ({ open, close }) => {
const pathName = usePathname();
return (
<aside
data-cmp="Sidebar"
className="hidden md:flex flex-col w-64 h-screen bg-card border-r border-border fixed left-0 top-0 z-30"
className={`side-bar border-border fixed top-0 left-0 z-30 h-screen w-64 flex-col border-r md:flex ${open ? "flex" : "hidden"}`}
>
<div className="p-6 border-b border-border">
<div className="border-border border-b p-6">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-primary-foreground" />
<div className="bg-primary flex h-8 w-8 items-center justify-center rounded-lg">
<Building2 className="text-primary-foreground h-5 w-5" />
</div>
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-purple-600">
<span className="from-primary bg-gradient-to-r to-purple-600 bg-clip-text text-xl font-bold text-transparent">
CGR Admin
</span>
</div>
</div>
<nav className="flex-1 overflow-y-auto py-6 px-3 space-y-1">
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-6">
{navItems.map((item) => {
const isActive = pathName === item.path;
return (
<Link
key={item.label}
href={item.path}
className={`flex items-center space-x-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
isActive ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground"
className={`flex items-center space-x-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
isActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
onClick={() => {
if (open) {
close();
}
}}
>
<item.icon className={`w-5 h-5 ${isActive ? "text-primary" : ""}`} />
<item.icon
className={`h-5 w-5 ${isActive ? "text-primary" : ""}`}
/>
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="p-4 border-t border-border">
<button className="flex items-center space-x-3 w-full px-3 py-2.5 rounded-lg text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors">
<LogOut className="w-5 h-5" />
<div className="border-border border-t p-4">
<button className="text-destructive hover:bg-destructive/10 flex w-full items-center space-x-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors">
<LogOut className="h-5 w-5" />
<span>Sign Out</span>
</button>
</div>

View File

@@ -18,11 +18,11 @@ const StatCard: React.FC<StatCardProps> = ({ label, value, icon: Icon, trend, ch
<Icon className="w-4 h-4 text-foreground" />
</div>
</div>
<div className="flex items-baseline space-x-2">
<div className="flex items-baseline flex-wrap space-x-2">
<h2 className="text-3xl font-bold text-foreground">{value}</h2>
{change && (
<span
className={`flex items-center text-xs font-medium ${
className={`flex items-center text-xs font-medium whitespace-nowrap ${
trend === "up" ? "text-green-600" : trend === "down" ? "text-red-500" : "text-muted-foreground"
}`}
>

48
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

220
components/ui/calendar.tsx Normal file
View File

@@ -0,0 +1,220 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

158
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,158 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

135
components/ui/drawer.tsx Normal file
View File

@@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

167
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import type * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

21
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
)}
{...props}
/>
)
}
export { Input }

24
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

190
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,190 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

35
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

19
hooks/use-media-query.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
const result = window.matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)
return () => result.removeEventListener("change", onChange)
}, [query])
return value
}

7
i18n/navigation.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
// Lightweight wrappers around Next.js' navigation
// APIs that consider the routing configuration
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);

16
i18n/request.ts Normal file
View File

@@ -0,0 +1,16 @@
import { hasLocale } from "next-intl";
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// Typically corresponds to the `[locale]` segment
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});

9
i18n/routing.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
// A list of all locales that are supported
locales: ["en", "de", "ru"],
// Used when no locale matches
defaultLocale: "en",
});

View File

@@ -1,6 +1,6 @@
export const NAV_ITEMS = [
{ label: "Home", href: "/" },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
{ label: "Admin", href: "/admin" },
{ labelKey: "nav.home", href: "/" },
{ labelKey: "nav.about", href: "/about" },
{ labelKey: "nav.contact", href: "/contact" },
{ labelKey: "nav.admin", href: "/admin" },
];

22
messages/de.json Normal file
View File

@@ -0,0 +1,22 @@
{
"header": {
"title": "CGR Anwendung"
},
"nav": {
"home": "Startseite",
"about": "Über",
"contact": "Kontakt",
"admin": "Admin"
},
"dashboard": {
"title": "Community-Dashboard",
"welcome": "Willkommen im Admin-Bereich. Verwalten Sie Ihre Community-Ressourcen sicher.",
"greeting_name": "Lieber Bruder {user},",
"greeting": "Friede sei mit Dir!",
"verse_intro": "Zur Ermutigung:"
},
"users": {
"title": "Benutzer",
"welcome": "Verwalten Sie hier Ihre Community-Mitglieder, Rollen und Berechtigungen."
}
}

22
messages/en.json Normal file
View File

@@ -0,0 +1,22 @@
{
"header": {
"title": "CGR App"
},
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact",
"admin": "Admin"
},
"dashboard": {
"title": "Community Dashboard",
"welcome": "Welcome to the admin area. Manage your community resources safely.",
"greeting_name": "Dear brother {user},",
"greeting": "peace be with you!",
"verse_intro": "For encouragement:"
},
"users": {
"title": "Users",
"welcome": "Manage your community members, roles, and permissions here."
}
}

22
messages/ru.json Normal file
View File

@@ -0,0 +1,22 @@
{
"header": {
"title": "CGR Приложение"
},
"nav": {
"home": "Главная",
"about": "О нас",
"contact": "Контакт",
"admin": "Админ"
},
"dashboard": {
"title": "Community Dashboard",
"welcome": "Добро пожаловать в административный раздел. Безопасно управляйте ресурсами вашего сообщества.",
"greeting_name": "Дорогой брат {user},",
"greeting": "мир Тебе!",
"verse_intro": "Для ободрения:"
},
"users": {
"title": "Пользователи",
"welcome": "Управляйте членами вашего сообщества, ролями и разрешениями здесь."
}
}

7
next-intl.config.js Normal file
View File

@@ -0,0 +1,7 @@
/** @type {import('next-intl').NextIntlConfig} */
module.exports = {
locales: ['en', 'de', 'ru'],
defaultLocale: 'en',
// Directory where your message JSON files live
messagesDirectory: './messages'
};

View File

@@ -1,8 +1,11 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
const nextConfig: NextConfig = {
output: "standalone",
/* config options here */
};
export default nextConfig;
export default withNextIntl(nextConfig);

1151
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,17 +9,28 @@
"lint": "eslint"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.563.0",
"next": "16.1.4",
"next-intl": "^4.8.2",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
"react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
"vaul": "^1.1.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -29,6 +40,8 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.4",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"

11
proxy.ts Normal file
View File

@@ -0,0 +1,11 @@
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
};

BIN
public/images/IMG_84271.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB