Compare commits
6 Commits
01cbaab498
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 19a9014f61 | |||
| 2cc52a3fab | |||
| 4c832549f2 | |||
| 3f7369fc86 | |||
| d77046194a | |||
| 6f1d93fc07 |
87
AGENTS.md
Normal file
87
AGENTS.md
Normal 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`
|
||||
@@ -14,8 +14,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const getPageTitle = (path: string) => {
|
||||
if (path === "/") return "Overview";
|
||||
|
||||
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",
|
||||
@@ -24,7 +27,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
"/events": "Worship & Events",
|
||||
};
|
||||
|
||||
return titles[path] || "Dashboard";
|
||||
return titles[purePath] || "Dashboard";
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -42,11 +46,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
<div
|
||||
className={`flex min-h-screen flex-col transition-all duration-300 md:pl-64`}
|
||||
>
|
||||
<Header
|
||||
onMenuClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
pageTitle={getPageTitle(pathname)}
|
||||
<Header
|
||||
onMenuClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
pageTitle={getPageTitle(pathname)}
|
||||
/>
|
||||
|
||||
|
||||
<main className="animate-in fade-in flex-1 p-4 duration-500 md:p-8">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
32
app/[locale]/(backend)/lib/dailyVerseData.ts
Normal file
32
app/[locale]/(backend)/lib/dailyVerseData.ts
Normal 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
|
||||
];
|
||||
@@ -22,11 +22,27 @@ import {
|
||||
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",
|
||||
@@ -169,17 +185,21 @@ const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl space-y-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<div>
|
||||
<h1 className="text-foreground text-3xl font-bold tracking-tight">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">{t("welcome")}</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">
|
||||
<Badge variant="secondary" className="px-2.5 py-0.5 text-sm">
|
||||
v2.4.0 Live
|
||||
</Badge>
|
||||
<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>
|
||||
|
||||
@@ -230,7 +250,7 @@ const Dashboard: React.FC = () => {
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
//onClick={() => console.log(`Navigating to ${item.path}`)}
|
||||
//onClick={() => console.log(`Navigating to ${item.path}`)}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
168
app/globals.css
168
app/globals.css
@@ -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 === */
|
||||
|
||||
@@ -7,6 +7,7 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ onMenuClick, pageTitle }) => {
|
||||
const [hasError, setHasError] = React.useState(false);
|
||||
return (
|
||||
<header
|
||||
data-cmp="Header"
|
||||
@@ -55,9 +56,19 @@ const Header: React.FC<HeaderProps> = ({ onMenuClick, pageTitle }) => {
|
||||
<p className="text-foreground text-sm font-medium">Community Admin</p>
|
||||
<p className="text-muted-foreground text-xs">Super Administrator</p>
|
||||
</div>
|
||||
<div className="bg-primary/10 border-primary/20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full border">
|
||||
<User className="text-primary h-5 w-5" />
|
||||
</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>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Community-Dashboard",
|
||||
"welcome": "Willkommen im Admin-Bereich. Verwalten Sie Ihre Community-Ressourcen sicher."
|
||||
"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",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Community Dashboard",
|
||||
"welcome": "Welcome to the admin area. Manage your community resources safely."
|
||||
"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."
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Community Dashboard",
|
||||
"welcome": "Добро пожаловать в административный раздел. Безопасно управляйте ресурсами вашего сообщества."
|
||||
"welcome": "Добро пожаловать в административный раздел. Безопасно управляйте ресурсами вашего сообщества.",
|
||||
"greeting_name": "Дорогой брат {user},",
|
||||
"greeting": "мир Тебе!",
|
||||
"verse_intro": "Для ободрения:"
|
||||
},
|
||||
"users": {
|
||||
"title": "Пользователи",
|
||||
|
||||
BIN
public/images/IMG_84271.jpg
Normal file
BIN
public/images/IMG_84271.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Reference in New Issue
Block a user