summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorl3wdfut4pwr <l3wdfut4pwr@gmail.com>2025-12-30 13:46:39 +0200
committerl3wdfut4pwr <l3wdfut4pwr@gmail.com>2025-12-30 13:46:39 +0200
commitc3dcb9c827df6d80ad1b0b1a7c6155561527b39d (patch)
tree76d8b9e706f9e8fcf7acc157a633905ff16c6b74 /src
init
Diffstat (limited to 'src')
-rw-r--r--src/app/favicon.icobin0 -> 25931 bytes
-rw-r--r--src/app/globals.css106
-rw-r--r--src/app/layout.tsx36
-rw-r--r--src/app/page.tsx5
-rw-r--r--src/app/upload/page.tsx38
-rw-r--r--src/components/footer/Footer.tsx24
-rw-r--r--src/components/footer/index.ts1
-rw-r--r--src/components/header/Header.tsx10
-rw-r--r--src/components/header/NavBar.tsx75
-rw-r--r--src/components/header/ProfileOrLogin.tsx25
-rw-r--r--src/components/header/SubNav.tsx43
-rw-r--r--src/components/header/index.ts4
-rw-r--r--src/components/ui/button.tsx57
-rw-r--r--src/components/ui/index.ts2
-rw-r--r--src/components/ui/input.tsx9
-rw-r--r--src/components/ui/switch.tsx30
-rw-r--r--src/components/upload/Dropzone.tsx45
-rw-r--r--src/components/upload/SourceInput.tsx20
-rw-r--r--src/components/upload/TagsInput.tsx83
-rw-r--r--src/components/upload/UploadMenu.tsx11
-rw-r--r--src/components/upload/index.ts4
-rw-r--r--src/lib/consts.ts2
-rw-r--r--src/lib/contexts/Auth.context.tsx37
-rw-r--r--src/lib/contexts/Global.context.tsx8
-rw-r--r--src/lib/contexts/index.ts2
-rw-r--r--src/lib/utils.ts6
26 files changed, 683 insertions, 0 deletions
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/src/app/favicon.ico
Binary files differ
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..51cd92c
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,106 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: #05040a;
+ --foreground: #ffffff;
+
+ --violet: #464199;
+ --light-violet: #8784c9;
+ --dark-indigo: #0d0c1c;
+ --red: #e64c4f;
+
+ --radius: 0.625rem;
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.13 0.028 261.692);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.13 0.028 261.692);
+ --primary: oklch(0.21 0.034 264.665);
+ --primary-foreground: oklch(0.985 0.002 247.839);
+ --secondary: oklch(0.967 0.003 264.542);
+ --secondary-foreground: oklch(0.21 0.034 264.665);
+ --muted: oklch(0.967 0.003 264.542);
+ --muted-foreground: oklch(0.551 0.027 264.364);
+ --accent: oklch(0.967 0.003 264.542);
+ --accent-foreground: oklch(0.21 0.034 264.665);
+ --destructive: oklch(0.577 0.245 27.325);
+ --input: oklch(0.928 0.006 264.531);
+ --ring: oklch(0.707 0.022 261.325);
+ --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.985 0.002 247.839);
+ --sidebar-foreground: oklch(0.13 0.028 261.692);
+ --sidebar-primary: oklch(0.21 0.034 264.665);
+ --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
+ --sidebar-accent: oklch(0.967 0.003 264.542);
+ --sidebar-accent-foreground: oklch(0.21 0.034 264.665);
+ --sidebar-border: oklch(0.928 0.006 264.531);
+ --sidebar-ring: oklch(0.707 0.022 261.325);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-violet: var(--violet);
+ --color-light-violet: var(--light-violet);
+ --color-dark-indigo: var(--dark-indigo);
+ --color-red: var(--red);
+
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --radius-2xl: calc(var(--radius) + 8px);
+ --radius-3xl: calc(var(--radius) + 12px);
+ --radius-4xl: calc(var(--radius) + 16px);
+}
+
+@layer base {
+ * {
+ @apply border-violet outline-ring/50;
+ }
+ html {
+ @apply scroll-smooth;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ button:not(:disabled),
+ [role='button']:not(:disabled) {
+ cursor: pointer;
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..194dca2
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,36 @@
+import type { Metadata } from 'next';
+import { Nunito } from 'next/font/google';
+import './globals.css';
+import { Header } from '../components/header';
+import { GlobalContextProvider } from '../lib/contexts';
+import { Footer } from '../components/footer';
+
+const nunito = Nunito();
+
+export const metadata: Metadata = {
+ title: 'Artberry',
+ description: 'Happy gooning!',
+ icons: '/icons/logo.svg',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="en">
+ <body className={`${nunito.className} antialiased px-37.5`}>
+ <div className="max-w-375 min-h-dvh flex flex-col mx-auto">
+ <GlobalContextProvider>
+ <Header />
+ <main className="mt-[50px] mb-[80px] grow">
+ {children}
+ </main>
+ <Footer />
+ </GlobalContextProvider>
+ </div>
+ </body>
+ </html>
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..b073f26
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,5 @@
+import Image from 'next/image';
+
+export default function Home() {
+ return null;
+}
diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx
new file mode 100644
index 0000000..d0f096f
--- /dev/null
+++ b/src/app/upload/page.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { Button } from '@/components/ui';
+import {
+ UploadMenu,
+ FileDropzone,
+ TagsInput,
+ SourceInput,
+} from '@/components/upload';
+import { PUBLISH_DISCLAIMER } from '@/lib/consts';
+
+export default function Upload() {
+ return (
+ <div className="flex justify-between w-full">
+ <UploadMenu />
+ <form
+ className="flex flex-col grow max-w-[900px] gap-5"
+ onSubmit={(e) => {
+ console.log('SUBMIT');
+ e.preventDefault();
+
+ const formData = new FormData(e.currentTarget);
+ const data: any = Object.fromEntries(formData.entries());
+
+ console.log(data.files);
+ }}
+ >
+ <FileDropzone />
+ <TagsInput />
+ <SourceInput />
+ <span className="text-center text-sm">
+ {PUBLISH_DISCLAIMER}
+ </span>
+ <Button>Опубликовать</Button>
+ </form>
+ </div>
+ );
+}
diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx
new file mode 100644
index 0000000..9b2a3f2
--- /dev/null
+++ b/src/components/footer/Footer.tsx
@@ -0,0 +1,24 @@
+import Logo from '@icons/logo.svg';
+import ArtberryIcon from '@icons/artberry.svg';
+
+export function Footer() {
+ return (
+ <footer className="flex w-full justify-between py-[50px]">
+ <div className="flex flex-col justify-between w-[259px] h-[192px]">
+ <div className="text-right">
+ <div className="flex items-center gap-2.5 w-full">
+ <Logo />
+ <ArtberryIcon />
+ </div>
+ <span className="font-medium">Лучшее — здесь.</span>
+ </div>
+ <div className="flex flex-col gap-1.25">
+ <span className="underline text-sm">
+ Terms and Conditions · Privacy Policy
+ </span>
+ <span className="text-xs">© 2025 artberry.xyz</span>
+ </div>
+ </div>
+ </footer>
+ );
+}
diff --git a/src/components/footer/index.ts b/src/components/footer/index.ts
new file mode 100644
index 0000000..ddcc5a9
--- /dev/null
+++ b/src/components/footer/index.ts
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx
new file mode 100644
index 0000000..b096bdb
--- /dev/null
+++ b/src/components/header/Header.tsx
@@ -0,0 +1,10 @@
+import { NavBar, SubNav } from '.';
+
+export function Header() {
+ return (
+ <div className="flex flex-col w-full">
+ <NavBar />
+ <SubNav />
+ </div>
+ );
+}
diff --git a/src/components/header/NavBar.tsx b/src/components/header/NavBar.tsx
new file mode 100644
index 0000000..f186904
--- /dev/null
+++ b/src/components/header/NavBar.tsx
@@ -0,0 +1,75 @@
+//import { ShuffleIcon } from 'lucide-react';
+import ShuffleIcon from '@icons/ShuffleIcon.svg';
+import Image from 'next/image';
+import { Button, Input } from '../ui';
+import { ProfileOrLogin } from './ProfileOrLogin';
+import Link from 'next/link';
+import DiscordIcon from '@icons/discord.svg';
+import { Switch } from '@/components/ui/switch';
+export function NavBar() {
+ return (
+ <div className="flex w-full justify-between py-7.5 h-[110] items-center">
+ <div className="flex min-w-[267] max-w-[750] grow items-center gap-7.5">
+ <Link href="/" className="shrink-0 ">
+ <Image
+ src="/icons/logo.svg"
+ alt="ARTBERRY"
+ width={46}
+ height={50}
+ className="h-[50]"
+ />
+ </Link>
+ <div className="flex gap-3.75 grow items-center">
+ <Search />
+ <Button variant={'icon'} size={'icon'} asChild>
+ <ShuffleIcon width={20} height={19} strokeWidth={2} />
+ </Button>
+ <div className="gap-[20] flex items-center shrink-0 w-[185]">
+ <div className="flex h-[22] gap-2.5 opacity-100 items-center w-[66]">
+ <span>AI</span>
+ <Switch />
+ </div>
+ <div className="flex h-[22] gap-2.5 opacity-100 items-center w-[66]">
+ <span>NSFW</span>
+ <Switch />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <Sections />
+ <div className="flex gap-3.75 items-center shrink-0">
+ <Link href={'#'} className="hover:[&_path]:fill-light-violet">
+ <DiscordIcon />
+ </Link>
+ <ProfileOrLogin />
+ </div>
+ </div>
+ );
+}
+
+function Search() {
+ return (
+ <div className="flex w-[409] h-[50] border-2 items-center rounded-4xl p-3.75">
+ <div className="flex w-full gap-1.25">
+ <Image
+ src="/icons/search.svg"
+ alt=""
+ width={24}
+ height={24}
+ className="h-[24] w-[24]"
+ />
+ <Input
+ placeholder="Поиск"
+ className="text-light-violet placeholder:text-light-violet text-sm"
+ />
+ </div>
+ </div>
+ );
+}
+
+function Sections() {
+ return (
+ <div className="flex grow min-w-75 max-w-[450] justify-between px-7.5 font-medium"></div>
+ );
+}
diff --git a/src/components/header/ProfileOrLogin.tsx b/src/components/header/ProfileOrLogin.tsx
new file mode 100644
index 0000000..38c1eb1
--- /dev/null
+++ b/src/components/header/ProfileOrLogin.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import Image from 'next/image';
+import { useUser } from '../../lib/contexts';
+import { Button } from '../ui';
+import Link from 'next/link';
+
+export function ProfileOrLogin() {
+ const user = useUser();
+
+ if (!user) {
+ return <Button className="py-2.5 px-3.75">ВОЙТИ</Button>;
+ }
+
+ return (
+ <Link href={'/profile'}>
+ <Image
+ src={user?.avatar ?? 'icons/avatar.svg'}
+ alt=""
+ width={60}
+ height={60}
+ />
+ </Link>
+ );
+}
diff --git a/src/components/header/SubNav.tsx b/src/components/header/SubNav.tsx
new file mode 100644
index 0000000..a446ff0
--- /dev/null
+++ b/src/components/header/SubNav.tsx
@@ -0,0 +1,43 @@
+import Link from 'next/link';
+import { Button } from '../ui';
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+export function SubNav() {
+ return (
+ <div className="flex justify-between w-full">
+ <SectionLink href="#" className="rounded-tl-[5px] rounded-bl-4xl">
+ ТЕГИ
+ </SectionLink>
+ <SectionLink href="#" className="border-x border-light-violet">
+ КАТЕГОРИИ
+ </SectionLink>
+ <SectionLink href="#" className="border-x border-light-violet">
+ ПЕРСОНАЖИ
+ </SectionLink>
+ <SectionLink href="#" className="rounded-tr-[5px] rounded-br-4xl">
+ КОЛЛЕКЦИИ
+ </SectionLink>
+ </div>
+ );
+}
+
+function SectionLink({
+ children,
+ href,
+ className,
+}: React.PropsWithChildren & { href: string; className?: string }) {
+ return (
+ <Button
+ asChild
+ className={cn(
+ 'bg-violet grow py-3.5 w-full hover:bg-light-violet',
+ className,
+ )}
+ variant={'ghost'}
+ size={'text'}
+ >
+ <Link href={href}>{children}</Link>
+ </Button>
+ );
+}
diff --git a/src/components/header/index.ts b/src/components/header/index.ts
new file mode 100644
index 0000000..3a75160
--- /dev/null
+++ b/src/components/header/index.ts
@@ -0,0 +1,4 @@
+export * from './Header';
+export * from './ProfileOrLogin';
+export * from './NavBar';
+export * from './SubNav';
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..65c1e84
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap transition-colors duration-200 ease-out',
+ {
+ variants: {
+ variant: {
+ default: 'bg-violet rounded-4xl hover:bg-light-violet',
+ outline: '',
+ ghost: '',
+ link: 'hover:text-light-violet',
+ icon: 'bg-violet hover:bg-light-violet rounded-[10px] cursor-pointer',
+ menu: 'w-[350px] hover:bg-light-violet active:bg-violet active:border-violet rounded-4xl border-2 border-light-violet justify-start',
+ },
+ size: {
+ sm: 'py-1.75 px-3.75',
+ default: 'py-2.5 px-7.5',
+ lg: 'py-2.5 px-7.5 w-full',
+ icon: 'size-12.5 p-3.75',
+ text: '',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function Button({
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps<typeof buttonVariants> & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+ <Comp
+ data-slot="button"
+ data-variant={variant}
+ data-size={size}
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props}
+ />
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
new file mode 100644
index 0000000..4d2a1a0
--- /dev/null
+++ b/src/components/ui/index.ts
@@ -0,0 +1,2 @@
+export * from './input';
+export * from './button';
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..0424772
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,9 @@
+import { cn } from '@/lib/utils';
+import React from 'react';
+
+export function Input({
+ className,
+ ...props
+}: React.InputHTMLAttributes<HTMLInputElement>) {
+ return <input className={cn('outline-none', className)} {...props} />;
+}
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..26fe2c3
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as React from 'react';
+import * as SwitchPrimitive from '@radix-ui/react-switch';
+
+import { cn } from '@/lib/utils';
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
+ return (
+ <SwitchPrimitive.Root
+ data-slot="switch"
+ className={cn(
+ 'peer data-[state=checked]:bg-violet data-[state=unchecked]:bg-light-violet focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[18px] w-10 shrink-0 items-center rounded-full border border-transparent transition-all duration-200 ease-out outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
+ className,
+ )}
+ {...props}
+ >
+ <SwitchPrimitive.Thumb
+ data-slot="switch-thumb"
+ className={cn(
+ 'bg-light-violet data-[state=unchecked]:bg-violet dark:data-[state=checked]:bg-light-violet pointer-events-none block size-3.5 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[22px] data-[state=unchecked]:translate-x-[2px]',
+ )}
+ />{' '}
+ </SwitchPrimitive.Root>
+ );
+}
+export { Switch };
diff --git a/src/components/upload/Dropzone.tsx b/src/components/upload/Dropzone.tsx
new file mode 100644
index 0000000..6980f5c
--- /dev/null
+++ b/src/components/upload/Dropzone.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import Dropzone from 'react-dropzone';
+import UploadIcon from '@icons/upload.svg';
+import { Button } from '@/components/ui';
+import { useRef, useState } from 'react';
+
+export function FileDropzone() {
+ // const [files, setFiles] = useState<File[]>([]);
+ const hiddenInputRef = useRef<HTMLInputElement | null>(null);
+ return (
+ <Dropzone
+ multiple
+ onDrop={(acceptedFiles: File[]) => {
+ // setFiles((prev) => [...prev, ...acceptedFiles]);
+ }}
+ >
+ {({ getRootProps, getInputProps }) => (
+ <div
+ {...getRootProps({
+ className:
+ 'w-full h-[460px] justify-between p-7.5 border flex flex-col text-sm items-center rounded-[10px]',
+ })}
+ >
+ <input name="files" type="file" ref={hiddenInputRef} />
+ <input {...getInputProps()} />
+
+ <div className="flex flex-col justify-center items-center h-full w-full gap-2.5">
+ <UploadIcon />
+ <span>
+ Выбери файлы на компьютере или перетащи сюда. JPG,
+ PNG до 20MB.
+ </span>
+ <span>
+ {hiddenInputRef.current?.files?.length ?? 0}
+ </span>
+ </div>
+ <Button size={'lg'} className="justify-self-end">
+ Выбрать
+ </Button>
+ </div>
+ )}
+ </Dropzone>
+ );
+}
diff --git a/src/components/upload/SourceInput.tsx b/src/components/upload/SourceInput.tsx
new file mode 100644
index 0000000..c93c4b6
--- /dev/null
+++ b/src/components/upload/SourceInput.tsx
@@ -0,0 +1,20 @@
+'use client';
+import { Input } from '@/components/ui';
+
+export function SourceInput() {
+ return (
+ <div className="flex flex-col gap-2.5 w-full">
+ <span>ИСТОЧНИК</span>
+ <Input
+ name="source"
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ }
+ }}
+ className="w-full h-10 py-2.5 px-5 border rounded-4xl"
+ type="text"
+ />
+ </div>
+ );
+}
diff --git a/src/components/upload/TagsInput.tsx b/src/components/upload/TagsInput.tsx
new file mode 100644
index 0000000..1b88fa2
--- /dev/null
+++ b/src/components/upload/TagsInput.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import React, { useState } from 'react';
+import { Tag, WithContext } from 'react-tag-input';
+import ExclamationIcon from '@icons/exclamation.svg';
+import XIcon from '@icons/x.svg';
+import { Button } from '@/components/ui';
+
+export function TagsInput() {
+ const [tags, setTags] = useState<Tag[]>([]);
+
+ return (
+ <div className="flex flex-col w-full gap-2.5">
+ <div className="flex gap-2.5 font-semibold">
+ <span>ТЕГИ</span>
+ <span className="text-light-violet">{tags.length}/50</span>
+ <span className="flex gap-1.25 items-center text-light-violet">
+ <ExclamationIcon /> Введите минимум 5 тегов через пробел
+ </span>
+ </div>
+ <div
+ id="tags-container"
+ className="w-full min-h-[100px] border p-5 rounded-4xl"
+ >
+ <input
+ name="tags"
+ className="hidden"
+ defaultValue={tags.map((tag) => tag.id)}
+ />
+ <WithContext
+ id="tags-input"
+ tags={tags}
+ placeholder={'Введите теги'}
+ maxTags={50}
+ handleAddition={(tag) => {
+ setTags((prev) => {
+ return [...prev, tag];
+ });
+ }}
+ handleDelete={(index: number, e) => {
+ setTags(tags.filter((_, i) => i !== index));
+ }}
+ handleDrag={(tag: Tag, currPos: number, newPos: number) => {
+ const newTags = tags.slice();
+
+ newTags.splice(currPos, 1);
+ newTags.splice(newPos, 0, tag);
+
+ setTags(newTags);
+ }}
+ inputFieldPosition="inline"
+ classNames={{
+ tagInputField: 'outline-none text-light-violet',
+ tag: 'bg-violet px-2.5 py-1.25 flex gap-1.25 rounded-[10px] w-fit items-center',
+ selected: 'flex gap-2.5 flex-wrap items-center',
+ }}
+ removeComponent={RemoveTag}
+ />
+ </div>
+ </div>
+ );
+}
+
+class RemoveTag extends React.Component {
+ render(): React.ReactNode {
+ // @ts-expect-error ...
+ const { className, onRemove } = this.props;
+ return (
+ <Button
+ variant={'ghost'}
+ size={'text'}
+ onClick={onRemove}
+ className={className}
+ onFocus={(e) => {
+ // Это говно здесь из-за того, что без него после удаления бэкспейсом, фокусится крестик
+ document.getElementById('tags-input')?.focus();
+ }}
+ >
+ <XIcon />
+ </Button>
+ );
+ }
+}
diff --git a/src/components/upload/UploadMenu.tsx b/src/components/upload/UploadMenu.tsx
new file mode 100644
index 0000000..22ebafd
--- /dev/null
+++ b/src/components/upload/UploadMenu.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { Button } from '@/components/ui';
+
+export function UploadMenu() {
+ return (
+ <div className="">
+ <Button variant={'menu'}>АРТЫ</Button>
+ </div>
+ );
+}
diff --git a/src/components/upload/index.ts b/src/components/upload/index.ts
new file mode 100644
index 0000000..022fe11
--- /dev/null
+++ b/src/components/upload/index.ts
@@ -0,0 +1,4 @@
+export * from './UploadMenu';
+export * from './Dropzone';
+export * from './TagsInput';
+export * from './SourceInput';
diff --git a/src/lib/consts.ts b/src/lib/consts.ts
new file mode 100644
index 0000000..7dea370
--- /dev/null
+++ b/src/lib/consts.ts
@@ -0,0 +1,2 @@
+export const PUBLISH_DISCLAIMER =
+ 'Прочитайте правила, прежде чем загружать. После публикации требуется одобрение модератора. Одобрение может занять до 14 дней, не связывайтесь с модерацией по этому поводу. Нажимая "опубликовать", вы подтверждаете, что прочитали наши правила и понимаете, что не можете удалить свою собственную загрузку, если вы не являетесь автором контента.';
diff --git a/src/lib/contexts/Auth.context.tsx b/src/lib/contexts/Auth.context.tsx
new file mode 100644
index 0000000..ff2d369
--- /dev/null
+++ b/src/lib/contexts/Auth.context.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import React, { createContext, use } from 'react';
+
+type User = {
+ id: string;
+ avatar?: string;
+ // Чёто там ещё
+};
+
+interface AuthContext {
+ user: User | null;
+}
+
+const AuthContext = createContext<AuthContext | null>(null);
+
+export const AuthContextProvider = ({ children }: React.PropsWithChildren) => {
+ // TODO: подключить бэк
+ const user = null;
+ return (
+ <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
+ );
+};
+
+export const useAuthContext = () => {
+ const context = use(AuthContext);
+
+ if (!context) {
+ throw new Error(
+ 'useAuthContext must be used within AuthContextProvider',
+ );
+ }
+
+ return context;
+};
+
+export const useUser = () => useAuthContext().user;
diff --git a/src/lib/contexts/Global.context.tsx b/src/lib/contexts/Global.context.tsx
new file mode 100644
index 0000000..3f8b61b
--- /dev/null
+++ b/src/lib/contexts/Global.context.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { AuthContextProvider } from './Auth.context';
+
+export const GlobalContextProvider = ({
+ children,
+}: React.PropsWithChildren) => {
+ return <AuthContextProvider>{children}</AuthContextProvider>;
+};
diff --git a/src/lib/contexts/index.ts b/src/lib/contexts/index.ts
new file mode 100644
index 0000000..61ebbe8
--- /dev/null
+++ b/src/lib/contexts/index.ts
@@ -0,0 +1,2 @@
+export * from './Auth.context';
+export * from './Global.context';
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}