This commit is contained in:
smanylov 2026-01-19 12:55:41 +07:00
parent 8ae5a072e9
commit fe8b6e42cc
34 changed files with 3930 additions and 0 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.env
.git
.gitignore
.next
README.md
.dockerignore

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:22.20.0 AS dependencies
WORKDIR /no-copy-admin-panel-frontend
COPY package.json package-lock.json ./
RUN npm install
FROM node:22.20.0 AS builder
WORKDIR /no-copy-admin-panel-frontend
COPY . .
COPY --from=dependencies /no-copy-admin-panel-frontend/node_modules ./node_modules
RUN npm run build
FROM node:22.20.0 AS runner
WORKDIR /no-copy-admin-panel-frontend
ENV NODE_ENV=production
COPY --from=builder /no-copy-admin-panel-frontend/public ./public
COPY --from=builder /no-copy-admin-panel-frontend/package.json ./package.json
COPY --from=builder /no-copy-admin-panel-frontend/.next ./.next
COPY --from=builder /no-copy-admin-panel-frontend/node_modules ./node_modules
EXPOSE 2999
CMD ["npm", "start"]

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
version: '3.8'
services:
frontend:
container_name: no-copy-admin-panel-frontend
build:
context: .
dockerfile: Dockerfile
ports:
- "2996:2996"
environment:
- NODE_ENV=production
- PORT=2996
- DOCKER_ENV=true
volumes:
- ./public:/app/public
- ./package.json:/app/package.json
restart: unless-stopped
networks:
- app-network
networks:
app-network:
external: true
name: app-network

9
next.config.ts Normal file
View File

@ -0,0 +1,9 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

2483
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "no-copy-admin-panel-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 2996",
"build": "next build",
"start": "next start -p 2996"
},
"dependencies": {
"clsx": "^2.1.1",
"next": "16.1.2",
"next-intl": "^4.7.0",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"sass": "^1.97.2",
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@ -0,0 +1,46 @@
import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { routing } from '@/i18n/routing';
import { notFound } from 'next/navigation';
import AdminNavLinks from '@/app/ui/admin-nav-links'
import AdminHeaderPanel from '@/app/ui/admin-header-panel';
import "../styles/globals.css";
import "../styles/global-styles.scss"
export const metadata: Metadata = {
title: "Admin panel no copy",
description: "Admin panel no copy",
};
export function generateStaticParams() {
return routing.locales.map((locale: any) => ({ locale }));
}
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang="en">
<body className="admin-panel">
<NextIntlClientProvider>
<AdminNavLinks />
<div className="admin-main">
<AdminHeaderPanel />
<main className="main-containter">
{children}
</main>
</div>
</NextIntlClientProvider>
</body>
</html >
);
}

14
src/app/[locale]/page.tsx Normal file
View File

@ -0,0 +1,14 @@
import DashboardGeneralInfo from '@/app/ui/dashboard-general-info';
import UsersTop from '@/app/ui/users-top';
import SubscriptionsStatistic from '@/app/ui/subscriptions-statistic';
export default function Page() {
return (
<div>
<DashboardGeneralInfo />
<div className="split-blocks">
<UsersTop />
<SubscriptionsStatistic />
</div>
</div>
)
}

View File

@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
subscriptions-page
</div>
)
}

View File

@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
users-page
</div>
)
}

View File

@ -0,0 +1,109 @@
'use client'
import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/navigation'
import { routing } from '@/i18n/routing'
import { useState, useRef, useEffect } from 'react'
import { useClickOutside } from '@/app/hooks/useClickOutside';
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const switchLanguage = (newLocale: string) => {
const currentLang = document.documentElement.getAttribute('lang') || 'ru';
router.replace(pathname, { locale: newLocale });
setIsOpen(false);
const observer = new MutationObserver((mutations) => {
const newLang = document.documentElement.getAttribute('lang')
if (newLang && newLang !== currentLang) {
observer.disconnect();
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
setTimeout(() => window.location.reload(), 100)
}
}
})
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['lang', 'dir']
})
}
router.replace(pathname, { locale: newLocale })
setTimeout(() => {
observer.disconnect();
}, 3000)
}
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false)
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, []);
return (
<div className="language-switcher" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="switcher-btn"
>
<span className="flex items-center">
<span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="switcher-list">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{routing.locales.map((lang) => (
<li key={lang} role="option">
<button
onClick={() => switchLanguage(lang)}
className={`language-btn ${locale === lang ? 'active' : ''
}`}
role="menuitem"
>
<span className="mr-2 font-medium">
{lang === 'ru' ? 'РУ' : lang.toUpperCase()}
</span>
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,29 @@
import React, { ReactNode } from 'react';
interface ModalWindowtProps {
children: ReactNode | null;
state: boolean;
callBack: (value: boolean) => void;
}
export default function ModalWindow({ children, state, callBack }: ModalWindowtProps) {
return (
<>
{state && (
<div
className="modal-wrapper"
onClick={() => {
callBack(false);
}}
>
<div
className="modal-window"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
)}
</>
)
}

View File

@ -0,0 +1,24 @@
'use client';
import { useEffect, RefObject } from 'react';
export function useClickOutside(
ref: RefObject<HTMLDivElement | null>,
callback: () => void
) {
useEffect(() => {
const handleClick = (event: MouseEvent) => {
if (!ref) {
return;
}
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
}
};
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [ref, callback]);
}

View File

@ -0,0 +1,81 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
interface UseNavigationBlockerProps {
shouldBlock: boolean;
message?: string;
onConfirm?: () => void;
onCancel?: () => void;
}
export const useNavigationBlocker = ({
shouldBlock,
message,
onConfirm,
onCancel
}: UseNavigationBlockerProps) => {
const router = useRouter();
useEffect(() => {
if (!shouldBlock) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = message;
return message;
};
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest('a');
if (link && link.href) {
const currentUrl = window.location.href;
if (link.href !== currentUrl && !link.href.startsWith('#')) {
e.preventDefault();
e.stopPropagation();
const confirmed = window.confirm(message);
if (confirmed) {
onConfirm?.();
window.location.href = link.href;
} else {
onCancel?.();
}
}
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
document.addEventListener('click', handleClick, true);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('click', handleClick, true);
};
}, [shouldBlock, message, onConfirm, onCancel]);
useEffect(() => {
if (!shouldBlock) return;
const originalPush = router.push;
router.push = async (...args) => {
const confirmed = window.confirm(message);
if (confirmed) {
onConfirm?.();
return originalPush.apply(router, args);
} else {
onCancel?.();
return Promise.resolve(false);
}
};
return () => {
router.push = originalPush;
};
}, [shouldBlock, message, router, onConfirm, onCancel]);
};

View File

@ -0,0 +1,380 @@
@use './variable.scss' as v;
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--secondary-color: #64748b;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #3b82f6;
--background: #f8fafc;
--surface: #ffffff;
--border: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #64748b;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.language-switcher {
position: relative;
.switcher-btn {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: v.$p-color;
background-color: v.$white;
border: 2px solid v.$p-color;
border-radius: 0.375rem;
box-shadow: 0 1px 2px 0 v.$shadow-1;
transition: background-color 0.2s;
cursor: pointer;
&:hover {
background-color: #f9fafb;
}
&:focus {
outline: none;
}
}
.switcher-list {
position: absolute;
right: 0;
z-index: 10;
width: 100%;
margin-top: 0.5rem;
transform-origin: top right;
background-color: v.$white;
border: 2px solid v.$p-color;
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
&:focus {
outline: none;
}
.language-btn {
display: flex;
align-items: center;
width: 100%;
padding: 0.5rem 1rem;
font-size: 0.875rem;
text-align: left;
transition: background-color 0.2s ease;
color: v.$p-color;
&:hover {
background-color: #dbeafe;
}
&.active {
background-color: #eff6ff;
font-weight: 700;
}
}
}
}
.admin-panel {
.admin-sidebar {
width: 280px;
background: var(--surface);
border-right: 1px solid var(--border);
position: fixed;
height: 100vh;
overflow-y: auto;
z-index: 1000;
.admin-nav {
padding: 16px 0;
}
.admin-logo {
padding: 24px 20px;
border-bottom: 1px solid var(--border);
h1 {
font-size: 20px;
font-weight: 700;
color: var(--primary-color);
}
p {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
}
.admin-nav-item {
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
gap: 12px;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
svg {
font-size: 18px;
width: 24px;
text-align: center;
color: var(--primary-color);
}
&.active {
background: #eff6ff;
color: var(--primary-color);
border-left-color: var(--primary-color);
}
}
}
.admin-header {
background: var(--surface);
padding: 20px 24px;
border-radius: 12px;
margin-bottom: 24px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
.admin-header-left {
h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
}
p {
color: var(--text-secondary);
font-size: 14px;
}
}
.admin-user-info {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 16px;
background: var(--background);
border-radius: 8px;
border: 1px solid var(--border);
}
.admin-header-right {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.admin-user-details span {
display: block;
font-size: 13px;
}
.admin-user-details .role {
color: var(--text-secondary);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.admin-user-details .name {
font-weight: 600;
color: var(--text-primary);
}
}
.admin-main {
flex: 1;
margin-left: 280px;
width: calc(100% - 280px);
padding: 24px;
max-width: 100%;
overflow-x: hidden;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
white-space: nowrap;
}
.switcher-btn {
border: 2px solid var(--primary-color);
color: var(--primary-color);
padding: 12px 24px;
.switcher-list {
background-color: v.$white;
border: 2px solid var(--primary-color);
}
}
.dashboard-general-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
.stat-card {
background: var(--surface);
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
}
.stat-card {
background: var(--surface);
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
}
.stat-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.stat-card-value {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.stat-card-label {
font-size: 12px;
color: var(--text-secondary);
}
.stat-card-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-secondary);
}
}
.admin-content {
background: var(--surface);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
.content-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
h3 {
font-size: 18px;
font-weight: 600;
}
}
.content-body {
padding: 24px;
max-width: 100%;
overflow-x: auto;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
min-width: 800px;
thead {
background: var(--background);
}
th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid var(--border);
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
}
tbody tr {
transition: background 0.2s;
}
.badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.badge-primary {
background: #eff6ff;
color: var(--primary-color);
}
}
}
.users-top {
&-user {
font-weight: 500;
}
&-mail {
font-size: 12px;
color: var(--text-secondary);
}
}
}

View File

@ -0,0 +1,51 @@
@font-face {
font-family: "Nunito Sans";
src:
url('./fonts/Nunito_Sans/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype-variations'),
url('./fonts/Nunito_Sans/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype');
font-weight: 100 1000;
font-style: normal;
}
@font-face {
font-family: "Nunito Sans";
src:
url('./fonts/Nunito_Sans/NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype-variations'),
url('./fonts/Nunito_Sans/NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype');
font-weight: 100 1000;
font-style: italic;
}
html,
body {
overflow-x: hidden;
}
body {
font-family: "Nunito Sans", 'Segoe UI', sans-serif;
}
* {
box-sizing: border-box;
}
a {
text-decoration: none;
}
h1,
h2,
h3,
h4 {
font-weight: bold;
font-family: 'Trebuchet MS', 'Segoe UI', sans-serif;
}
svg:focus,
g:focus,
path:focus {
outline: none;
outline-style: none;
}
@import "tailwindcss";

View File

@ -0,0 +1,27 @@
$p-color: #6366f1;
$s-color: #8b5cf6;
$text-p: #1f2937;
$text-s: #4b5563;
$text-m: #9ca3af;
$text-w: #ffffffcc;
$b-color-1: #f3f4f6;
$b-color-2: #e5e7eb;
$bg-hover: #e9ecef;
$bg-light: #f8f9ff;
$bg-darkening: #0000007a;
$shadow-1: #0000001a;
$white: #fff;
$red: #dc2626;
$color-image: #f08c00;
$color-video: #2f9e44;
$color-audio: #1971c2;
$color-document: #a561e6;
:root {
--side-bar-width: 280px;
@media (max-width: 1200px) {
--side-bar-width: 180px;
}
}

View File

@ -0,0 +1,30 @@
import Link from 'next/link';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
export default function AdminHeaderPanel() {
return (
<div className="admin-header">
<div className="admin-header-left">
<h2>Панель администратора</h2>
</div>
<div className="admin-header-right">
<LanguageSwitcher />
<div className="admin-user-info">
<div className="admin-user-avatar">
+
</div>
<div className="admin-user-details">
<span className="name">name name name</span>
<span className="role">role</span>
</div>
</div>
<Link
href={'/pages/dashboard'}
className="btn btn-primary"
>
<span>На главную</span>
</Link>
</div>
</div>
)
}

View File

@ -0,0 +1,75 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
export default function AdminNavLinks() {
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
const t = useTranslations('Global');
const links = [
{
name: 'dashboard',
href: '/',
img: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
},
{
name: 'users',
href: '/users',
img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7'
},
{
name: 'subscriptions',
href: '/subscriptions',
img: 'M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2zm-2 2H4V6h16zM4 11h16v7H4z'
}
];
return (
<div className=''>
<nav className="admin-sidebar">
<div className="admin-logo">
<h1>NO COPY</h1>
<p>Админ-панель</p>
</div>
<ul className="admin-nav">
{links.map((link) => {
return (
<Link
key={link.name}
href={link.href}
className={clsx(
`admin-nav-item`,
{
'active': pathname === link.href,
},
)}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d={link.img}></path>
</svg>
<p className="">{link.name}</p>
</Link>
);
})}
<Link
key={t('exit')}
href='/login'
className="admin-nav-item"
onClick={(e) => {
e.preventDefault();
}}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"></path>
</svg>
<p className="">{t('exit')}</p>
</Link>
</ul>
</nav>
</div>
);
}

View File

@ -0,0 +1,140 @@
export default function DashboardGeneralInfo() {
return (
<div className="dashboard-general-info">
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Всего пользователей</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Зарегистрировано в системе</div>
<div className="stat-card-footer">
Активных за месяц: 0</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Активные подписчики</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Платные подписки</div>
<div className="stat-card-footer">
Конверсия: 0%
</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Защищенный контент</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Файлов защищено</div>
<div className="stat-card-footer">
Всего файлов: 0</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Нарушения</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Обнаружено нарушений</div>
<div className="stat-card-footer">
На рассмотрении: 0</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Токены сегодня</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Пополнено токенов</div>
<div className="stat-card-footer">
За неделю: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Токены месяц</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Пополнено за месяц</div>
<div className="stat-card-footer">
Потрачено: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Доход сегодня</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Заработано сегодня</div>
<div className="stat-card-footer">
За неделю: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Доход месяц</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Заработано за месяц</div>
<div className="stat-card-footer">
Всего: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Траты токенов день</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Потрачено сегодня</div>
<div className="stat-card-footer">
За неделю: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Траты токенов год</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Потрачено за год</div>
<div className="stat-card-footer">
За месяц: 0 </div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">DMCA жалобы</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Всего жалоб</div>
<div className="stat-card-footer">
Успешно: 0 | Ожидает: 1
</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Использование диска</span>
<div className="stat-card-icon"></div>
</div>
<div className="stat-card-value">0.0 GB</div>
<div className="stat-card-label">Занято на диске</div>
<div className="stat-card-footer">
Файлов: 0</div>
</div>
</div>
)
}

View File

@ -0,0 +1,77 @@
/* https://icon-sets.iconify.design/ic/ */
export function IconRuble() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M7.5 20v-2.808h-2v-1h2v-2.73h-2v-1h2V4h6.27q1.978 0 3.354 1.375T18.5 8.727t-1.376 3.356t-3.355 1.379H8.5v2.73h4v1h-4V20zm1-7.539h5.27q1.545 0 2.638-1.092T17.5 8.73t-1.092-2.638Q15.314 5 13.769 5H8.5z" />
</svg>
)
}
export function IconDocument() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M4 4a2 2 0 0 1 2-2h8a1 1 0 0 1 .707.293l5 5A1 1 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm13.586 4L14 4.414V8zM12 4H6v16h12V10h-5a1 1 0 0 1-1-1zm-4 9a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1m0 4a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1" /></svg>
)
}
export function IconHummer() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m2.3 20.28l9.6-9.6l-1.4-1.42l-.72.71a.996.996 0 0 1-1.41 0l-.71-.71a.996.996 0 0 1 0-1.41l5.66-5.66a.996.996 0 0 1 1.41 0l.71.71c.39.39.39 1.02 0 1.41l-.71.69l1.42 1.43a.996.996 0 0 1 1.41 0c.39.39.39 1.03 0 1.42l1.41 1.41l.71-.71c.39-.39 1.03-.39 1.42 0l.7.71c.39.39.39 1.03 0 1.42l-5.65 5.65c-.39.39-1.03.39-1.42 0l-.7-.7a.99.99 0 0 1 0-1.42l.7-.71l-1.41-1.41l-9.61 9.61a.996.996 0 0 1-1.41 0c-.39-.39-.39-1.03 0-1.42M20 19a2 2 0 0 1 2 2v1H12v-1a2 2 0 0 1 2-2z" /></svg>
)
}
export function IconBell() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M14.235 19c.865 0 1.322 1.024.745 1.668A4 4 0 0 1 12 22a4 4 0 0 1-2.98-1.332c-.552-.616-.158-1.579.634-1.661l.11-.006zM12 2c1.358 0 2.506.903 2.875 2.141l.046.171l.008.043a8.01 8.01 0 0 1 4.024 6.069l.028.287L19 11v2.931l.021.136a3 3 0 0 0 1.143 1.847l.167.117l.162.099c.86.487.56 1.766-.377 1.864L20 18H4c-1.028 0-1.387-1.364-.493-1.87a3 3 0 0 0 1.472-2.063L5 13.924l.001-2.97A8 8 0 0 1 8.822 4.5l.248-.146l.01-.043a3 3 0 0 1 2.562-2.29l.182-.017z" /></svg>
)
}
export function IconShield() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="m466.5 83.7l-192-80a48.15 48.15 0 0 0-36.9 0l-192 80C27.7 91.1 16 108.6 16 128c0 198.5 114.5 335.7 221.5 380.3c11.8 4.9 25.1 4.9 36.9 0C360.1 472.6 496 349.3 496 128c0-19.4-11.7-36.9-29.5-44.3M256.1 446.3l-.1-381l175.9 73.3c-3.3 151.4-82.1 261.1-175.8 307.7" /></svg>
)
}
export function IconBlank() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" strokeWidth="2"><path d="M14.172 21H7c-1.886 0-2.828 0-3.414-.586S3 18.886 3 17V7c0-1.886 0-2.828.586-3.414S5.114 3 7 3h10c1.886 0 2.828 0 3.414.586S21 5.114 21 7v7.172c0 .408 0 .613-.076.797c-.076.183-.22.328-.51.617l-4.828 4.828c-.29.29-.434.434-.617.51c-.184.076-.389.076-.797.076Z" /><path d="M14 21v-4.667c0-1.1 0-1.65.342-1.991c.341-.342.891-.342 1.991-.342H21" /></g></svg>
)
}
export function IconClock() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /><path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 4a1 1 0 0 0-1 1v5a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V7a1 1 0 0 0-1-1" /></g></svg>
)
}
export function IconGraphMultiple() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M20 16V4H8v12m14 0c0 1.1-.9 2-2 2H8c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h12c1.1 0 2 .9 2 2m-6 16v2H4c-1.1 0-2-.9-2-2V7h2v13m12-9h2v3h-2m-3-8h2v8h-2m-3-6h2v6h-2Z" /></svg>
)
}
export function IconEye() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
<path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5s-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3" />
</svg>
)
}
export function IconTelegram() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m21.936 5.17l-3.03 14.185c-.226.999-.806 1.224-1.644.773l-4.545-3.352l-2.225 2.127c-.225.226-.451.452-.967.452l.355-4.675l8.478-7.704c.354-.355-.097-.484-.548-.193l-10.541 6.64l-4.546-1.386c-.999-.322-.999-1 .226-1.45L20.614 3.72c.87-.258 1.612.194 1.322 1.45" /></svg>
)
}
export function IconLink() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="none" stroke="currentColor" strokeDasharray="28" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 6l2 -2c1 -1 3 -1 4 0l1 1c1 1 1 3 0 4l-5 5c-1 1 -3 1 -4 0M11 18l-2 2c-1 1 -3 1 -4 0l-1 -1c-1 -1 -1 -3 0 -4l5 -5c1 -1 3 -1 4 0"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="28;0" /></path></svg>
)
}
export function IconRefresh() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="M21.074 12.154a.75.75 0 0 1 .672.82c-.49 4.93-4.658 8.776-9.724 8.776c-2.724 0-5.364-.933-7.238-2.68L3 20.85a.75.75 0 0 1-.75-.75v-3.96c0-.714.58-1.29 1.291-1.29h3.97a.75.75 0 0 1 .75.75l-2.413 2.407c1.558 1.433 3.78 2.243 6.174 2.243c4.29 0 7.817-3.258 8.232-7.424a.75.75 0 0 1 .82-.672m-18.82-1.128c.49-4.93 4.658-8.776 9.724-8.776c2.724 0 5.364.933 7.238 2.68L21 3.15a.75.75 0 0 1 .75.75v3.96c0 .714-.58 1.29-1.291 1.29h-3.97a.75.75 0 0 1-.75-.75l2.413-2.408c-1.558-1.432-3.78-2.242-6.174-2.242c-4.29 0-7.817 3.258-8.232 7.424a.75.75 0 1 1-1.492-.148" /></svg>
)
}

View File

@ -0,0 +1,49 @@
export default function SubscriptionsStatistic() {
return (
<div className="admin-content">
<div className="content-header">
<h3>Статистика подписок</h3>
</div>
<div className="content-body">
<table className="admin-table">
<thead>
<tr>
<th>Тариф</th>
<th>Пользователей</th>
<th>Доля</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span className="badge badge-primary">
СТАРТ
</span>
</td>
<td>0</td>
<td>0%</td>
</tr>
<tr>
<td>
<span className="badge badge-primary">
ДЕМО
</span>
</td>
<td>0</td>
<td>0%</td>
</tr>
<tr>
<td>
<span className="badge badge-primary">
ПРОФИ
</span>
</td>
<td>0</td>
<td>0%</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}

54
src/app/ui/users-top.tsx Normal file
View File

@ -0,0 +1,54 @@
export default function UsersTop() {
return (
<div className="admin-content users-top">
<div className="content-header">
<h3>Топ пользователей</h3>
</div>
<div className="content-body">
<table className="admin-table">
<thead>
<tr>
<th>Пользователь</th>
<th>Файлов</th>
<th>Размер</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div className="users-top-user">Сидоров Станислав Андреевич</div>
<div className="users-top-mail">Info@pro-ctrl.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
<tr>
<td>
<div className="users-top-user">Валерка</div>
<div className="users-top-mail">va@no-copy.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
<tr>
<td>
<div className="users-top-user">Игорь Викторович</div>
<div className="users-top-mail">igor@pro-ctrl.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
<tr>
<td>
<div className="users-top-user">Синицын Игорь Владимирович</div>
<div className="users-top-mail">admin@no-copy.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
{
"HomePage": {
"title": "Welcome to our application",
"description": "This is a demo application with i18n support"
},
"Global": {
"exit": "Exit"
}
}

View File

@ -0,0 +1,9 @@
{
"HomePage": {
"title": "Добро пожаловать в наше приложение",
"description": "Это демонстрационное приложение с поддержкой i18n"
},
"Global": {
"exit": "Выход"
}
}

5
src/i18n/navigation.ts Normal file
View File

@ -0,0 +1,5 @@
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);

15
src/i18n/request.ts Normal file
View File

@ -0,0 +1,15 @@
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default
};
});

6
src/i18n/routing.ts Normal file
View File

@ -0,0 +1,6 @@
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['ru', 'en'],
defaultLocale: 'ru'
});

30
src/proxy.ts Normal file
View File

@ -0,0 +1,30 @@
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers'
import createIntlMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'
const intlMiddleware = createIntlMiddleware(routing);
export default async function proxy(request: NextRequest) {
const path = request.nextUrl.pathname;
if (
path.startsWith('/_next') ||
path.includes('/api/') ||
path.includes('.')
) {
return NextResponse.next()
}
const intlResponse = intlMiddleware(request);
if (intlResponse) {
return intlResponse
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
};

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}