init
This commit is contained in:
parent
8ae5a072e9
commit
fe8b6e42cc
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
.next
|
||||
README.md
|
||||
.dockerignore
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
23
Dockerfile
Normal 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
25
docker-compose.yml
Normal 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
9
next.config.ts
Normal 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
2483
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal 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
7
postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
46
src/app/[locale]/layout.tsx
Normal file
46
src/app/[locale]/layout.tsx
Normal 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
14
src/app/[locale]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/[locale]/subscriptions/page.tsx
Normal file
7
src/app/[locale]/subscriptions/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
subscriptions-page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/app/[locale]/users/page.tsx
Normal file
7
src/app/[locale]/users/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
users-page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
src/app/components/LanguageSwitcher.tsx
Normal file
109
src/app/components/LanguageSwitcher.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
src/app/components/modalWindow.tsx
Normal file
29
src/app/components/modalWindow.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
24
src/app/hooks/useClickOutside.tsx
Normal file
24
src/app/hooks/useClickOutside.tsx
Normal 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]);
|
||||
}
|
||||
81
src/app/hooks/useNavigationBlocker.ts
Normal file
81
src/app/hooks/useNavigationBlocker.ts
Normal 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]);
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
380
src/app/styles/global-styles.scss
Normal file
380
src/app/styles/global-styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/app/styles/globals.css
Normal file
51
src/app/styles/globals.css
Normal 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";
|
||||
27
src/app/styles/variable.scss
Normal file
27
src/app/styles/variable.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/app/ui/admin-header-panel.tsx
Normal file
30
src/app/ui/admin-header-panel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
75
src/app/ui/admin-nav-links.tsx
Normal file
75
src/app/ui/admin-nav-links.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
140
src/app/ui/dashboard-general-info.tsx
Normal file
140
src/app/ui/dashboard-general-info.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
77
src/app/ui/icons/icons.tsx
Normal file
77
src/app/ui/icons/icons.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
49
src/app/ui/subscriptions-statistic.tsx
Normal file
49
src/app/ui/subscriptions-statistic.tsx
Normal 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
54
src/app/ui/users-top.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
src/i18n/messages/en.json
Normal file
9
src/i18n/messages/en.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Welcome to our application",
|
||||
"description": "This is a demo application with i18n support"
|
||||
},
|
||||
"Global": {
|
||||
"exit": "Exit"
|
||||
}
|
||||
}
|
||||
9
src/i18n/messages/ru.json
Normal file
9
src/i18n/messages/ru.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Добро пожаловать в наше приложение",
|
||||
"description": "Это демонстрационное приложение с поддержкой i18n"
|
||||
},
|
||||
"Global": {
|
||||
"exit": "Выход"
|
||||
}
|
||||
}
|
||||
5
src/i18n/navigation.ts
Normal file
5
src/i18n/navigation.ts
Normal 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
15
src/i18n/request.ts
Normal 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
6
src/i18n/routing.ts
Normal 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
30
src/proxy.ts
Normal 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
34
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user