update: add custom-components and make home page

This commit is contained in:
BunyodL 2025-04-20 18:27:15 +05:00
parent 477d311213
commit ceef7c7efc
21 changed files with 1581 additions and 5632 deletions

View File

@ -10,12 +10,13 @@
"cssVariables": true,
"prefix": ""
},
"componentPath": "src/shared/shad-cn",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"components": "@/shared",
"utils": "@/shared/lib/utils",
"ui": "@/shared/shad-cn",
"lib": "@/shared/lib",
"hooks": "@/shared/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -10,7 +10,7 @@ import globals from 'globals';
import TS_ESLint from 'typescript-eslint';
export default TS_ESLint.config(
{ ignores: ['dist'] },
{ ignores: ['.next'] },
{
extends: [
js.configs.recommended,
@ -33,11 +33,7 @@ export default TS_ESLint.config(
parserOptions: {
ecmaVersion: 'latest',
project: [
'./tsconfig.app.json',
'./tsconfig.json',
'./tsconfig.node.json',
],
project: ['./tsconfig.json'],
projectService: true,
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
@ -50,9 +46,7 @@ export default TS_ESLint.config(
plugins: {
'react-hooks': reactHooks,
// 'react-refresh': reactRefresh,
'unused-imports': unusedImports,
// 'react-compiler': reactCompiler,
react,
},
@ -69,7 +63,7 @@ export default TS_ESLint.config(
ignores: [
'.prettierrc.cjs',
'dist',
'.next',
'postcss.config.js',
'tailwind.config.ts',
],

5461
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-dropdown-menu": "^2.1.11",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.8",
"@reduxjs/toolkit": "^2.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -22,6 +23,8 @@
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.6",
"zod": "^3.24.3"
},
"devDependencies": {

52
pnpm-lock.yaml generated
View File

@ -17,6 +17,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.0
version: 1.2.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-tabs':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@reduxjs/toolkit':
specifier: ^2.7.0
version: 2.7.0(react-redux@9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1))(react@19.1.0)
@ -47,6 +50,12 @@ importers:
tailwind-merge:
specifier: ^3.2.0
version: 3.2.0
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.4)
tw-animate-css:
specifier: ^1.2.6
version: 1.2.6
zod:
specifier: ^3.24.3
version: 3.24.3
@ -646,6 +655,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-tabs@1.1.8':
resolution: {integrity: sha512-4iUaN9SYtG+/E+hJ7jRks/Nv90f+uAsRHbLYA6BcA9EsR6GNWgsvtS4iwU2SP0tOZfDGAyqIT0yz7ckgohEIFA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@ -2271,6 +2293,11 @@ packages:
tailwind-merge@3.2.0:
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
tailwindcss@4.1.4:
resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
@ -2298,6 +2325,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.2.6:
resolution: {integrity: sha512-/DSl24Y1WNdtEWA187h3M5ixwvucje2DH2/Qi8N1plNn0Mb0O1E6F9trXknwzZbtVJCdnogaWLt45xQZOrKtpw==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -2882,6 +2912,22 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.2
'@radix-ui/react-tabs@1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)':
dependencies:
react: 19.1.0
@ -4627,6 +4673,10 @@ snapshots:
tailwind-merge@3.2.0: {}
tailwindcss-animate@1.0.7(tailwindcss@4.1.4):
dependencies:
tailwindcss: 4.1.4
tailwindcss@4.1.4: {}
tapable@2.2.1: {}
@ -4653,6 +4703,8 @@ snapshots:
tslib@2.8.1: {}
tw-animate-css@1.2.6: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1

View File

@ -1,5 +1,5 @@
@import 'tailwindcss';
/* @import "tw-animate-css"; */
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@ -45,71 +45,71 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--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 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {

View File

@ -1,23 +1,16 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { Geist, Geist_Mono, Inter } from 'next/font/google';
import { Providers } from '@/shared/providers/providers';
import './globals.css';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'GasNetwork - Сеть заправок в Таджикистане',
description:
'Качественное топливо, удобное расположение и отличный сервис для наших клиентов',
};
export default function RootLayout({
@ -27,9 +20,7 @@ export default function RootLayout({
}>) {
return (
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className={`${inter.className} antialiased`}>
<Providers>{children}</Providers>
</body>
</html>

View File

@ -1,104 +1,676 @@
import { ModeToggle } from "@/shared/theme/theme-toggle";
import Image from "next/image";
import {
ArrowRight,
Briefcase,
ChevronRight,
Fuel,
Gift,
Handshake,
Heart,
Mail,
MapPin,
Phone,
Users,
} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import AboutCounter from '@/shared/components/about-counter';
import GasStationMap from '@/shared/components/gas-station-map';
import PromotionSlider from '@/shared/components/promotion-slider';
import StatsSection from '@/shared/components/stats-section';
import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/shared/shadcn-ui/tabs';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ModeToggle/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<div className='flex min-h-screen flex-col'>
{/* Header */}
<header className='sticky top-0 z-40 w-full border-b bg-white'>
<div className='container flex h-16 items-center justify-between py-4'>
<div className='flex items-center gap-2'>
<Fuel className='h-6 w-6 text-red-600' />
<span className='text-xl font-bold'>GasNetwork</span>
</div>
<nav className='hidden items-center gap-6 md:flex'>
<Link
href='#stations'
className='text-sm font-medium transition-colors hover:text-red-600'
>
Наши заправки
</Link>
<Link
href='#about'
className='text-sm font-medium transition-colors hover:text-red-600'
>
О нас
</Link>
<Link
href='#vacancies'
className='text-sm font-medium transition-colors hover:text-red-600'
>
Вакансии
</Link>
<Link
href='#promotions'
className='text-sm font-medium transition-colors hover:text-red-600'
>
Акции
</Link>
<Link
href='#partners'
className='text-sm font-medium transition-colors hover:text-red-600'
>
Партнеры
</Link>
<Link
href='#charity'
className='text-sm font-medium transition-colors hover:text-red-600'
>
Благотворительность
</Link>
</nav>
<div className='flex items-center gap-4'>
<Button variant='outline' size='sm' className='hidden md:flex'>
TJ
</Button>
<Button variant='outline' size='sm' className='hidden md:flex'>
RU
</Button>
<Button className='bg-red-600 hover:bg-red-700'>Контакты</Button>
</div>
</div>
</header>
<main className='flex-1'>
{/* Hero Section */}
<section className='relative'>
<div className='relative h-[500px] w-full overflow-hidden'>
<Image
src='/placeholder.svg?height=500&width=1920'
alt='Gas Station Network'
width={1920}
height={500}
className='object-cover'
priority
/>
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
<div className='container'>
<div className='max-w-lg space-y-4 text-white'>
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
Сеть современных заправок в Таджикистане
</h1>
<p className='text-lg text-gray-200'>
Качественное топливо, удобное расположение и отличный сервис
для наших клиентов
</p>
<div className='flex gap-4'>
<Button className='bg-red-600 hover:bg-red-700'>
Найти заправку <MapPin className='ml-2 h-4 w-4' />
</Button>
<Button
variant='outline'
className='border-white text-white hover:bg-white/10'
>
Узнать больше
</Button>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<StatsSection />
{/* Map Section */}
<section id='stations' className='bg-gray-50 py-16'>
<div className='container'>
<div className='mb-12 flex flex-col items-center justify-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<MapPin className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Наши заправки
</h2>
<p className='max-w-2xl text-gray-600'>
Найдите ближайшую к вам заправку нашей сети. Мы расположены в
удобных местах по всему Таджикистану.
</p>
</div>
<div className='h-[500px] overflow-hidden rounded-xl border shadow-lg'>
<GasStationMap />
</div>
<div className='mt-8 flex justify-center'>
<Button className='bg-red-600 hover:bg-red-700'>
Показать все заправки <ChevronRight className='ml-2 h-4 w-4' />
</Button>
</div>
</div>
</section>
{/* About Section */}
<section id='about' className='py-16'>
<div className='container'>
<div className='grid items-center gap-12 md:grid-cols-2'>
<div>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
О нашей компании
</h2>
<p className='mb-6 text-gray-600'>
Наша сеть заправок является одной из ведущих в Таджикистане.
Мы предоставляем качественное топливо и высокий уровень
обслуживания для наших клиентов уже более 15 лет.
</p>
<p className='mb-6 text-gray-600'>
Мы постоянно развиваемся, открывая новые станции и улучшая
сервис на существующих. Наша цель - сделать заправку
автомобиля максимально удобной и быстрой для каждого клиента.
</p>
<AboutCounter />
<div className='space-y-4'>
<div className='flex items-start'>
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
<span className='text-xs text-white'></span>
</div>
<div className='ml-3'>
<h3 className='text-lg font-medium'>
Качественное топливо
</h3>
<p className='text-gray-600'>
Мы гарантируем высокое качество нашего топлива
</p>
</div>
</div>
<div className='flex items-start'>
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
<span className='text-xs text-white'></span>
</div>
<div className='ml-3'>
<h3 className='text-lg font-medium'>
Современное оборудование
</h3>
<p className='text-gray-600'>
Все наши станции оснащены современным оборудованием
</p>
</div>
</div>
<div className='flex items-start'>
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
<span className='text-xs text-white'></span>
</div>
<div className='ml-3'>
<h3 className='text-lg font-medium'>
Профессиональный персонал
</h3>
<p className='text-gray-600'>
Наши сотрудники - профессионалы своего дела
</p>
</div>
</div>
</div>
</div>
<div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'>
<Image
src='/placeholder.svg?height=400&width=600'
alt='About our company'
fill
className='object-cover'
/>
</div>
</div>
</div>
</section>
{/* Promotions Section */}
<section id='promotions' className='bg-gray-50 py-16'>
<div className='container'>
<div className='mb-12 flex flex-col items-center justify-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Gift className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Актуальные акции
</h2>
<p className='max-w-2xl text-gray-600'>
Специальные предложения и акции для наших клиентов.
Заправляйтесь выгодно!
</p>
</div>
<PromotionSlider />
<div className='mt-8 flex justify-center'>
<Button className='bg-red-600 hover:bg-red-700'>
Все акции <ArrowRight className='ml-2 h-4 w-4' />
</Button>
</div>
</div>
</section>
{/* Vacancies Section */}
<section id='vacancies' className='py-16'>
<div className='container'>
<div className='mb-12 flex flex-col items-center justify-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Briefcase className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Вакансии
</h2>
<p className='max-w-2xl text-gray-600'>
Присоединяйтесь к нашей команде профессионалов. Мы предлагаем
стабильную работу и возможности для роста.
</p>
</div>
<Tabs defaultValue='all' className='mx-auto w-full max-w-3xl'>
<TabsList className='mb-8 grid grid-cols-3'>
<TabsTrigger value='all'>Все вакансии</TabsTrigger>
<TabsTrigger value='office'>Офис</TabsTrigger>
<TabsTrigger value='stations'>Заправки</TabsTrigger>
</TabsList>
<TabsContent value='all' className='space-y-4'>
{[
'Оператор АЗС',
'Менеджер по продажам',
'Бухгалтер',
'Специалист по логистике',
].map((job, index) => (
<Card
key={index}
className='overflow-hidden transition-all hover:shadow-md'
>
<CardContent className='p-0'>
<div className='p-6'>
<div className='flex items-start justify-between'>
<div>
<h3 className='mb-2 text-lg font-bold'>{job}</h3>
<p className='mb-4 text-sm text-gray-500'>
Душанбе, Таджикистан
</p>
<div className='mb-4 flex flex-wrap gap-2'>
<span className='inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800'>
Полный день
</span>
<span className='inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800'>
Опыт от 1 года
</span>
</div>
</div>
<Button variant='outline' size='sm'>
Подробнее
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value='office' className='space-y-4'>
{[
'Менеджер по продажам',
'Бухгалтер',
'Специалист по логистике',
].map((job, index) => (
<Card
key={index}
className='overflow-hidden transition-all hover:shadow-md'
>
<CardContent className='p-0'>
<div className='p-6'>
<div className='flex items-start justify-between'>
<div>
<h3 className='mb-2 text-lg font-bold'>{job}</h3>
<p className='mb-4 text-sm text-gray-500'>
Душанбе, Таджикистан
</p>
<div className='mb-4 flex flex-wrap gap-2'>
<span className='inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800'>
Полный день
</span>
<span className='inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800'>
Опыт от 1 года
</span>
</div>
</div>
<Button variant='outline' size='sm'>
Подробнее
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value='stations' className='space-y-4'>
{['Оператор АЗС', 'Заправщик', 'Менеджер станции'].map(
(job, index) => (
<Card
key={index}
className='overflow-hidden transition-all hover:shadow-md'
>
<CardContent className='p-0'>
<div className='p-6'>
<div className='flex items-start justify-between'>
<div>
<h3 className='mb-2 text-lg font-bold'>{job}</h3>
<p className='mb-4 text-sm text-gray-500'>
Душанбе, Таджикистан
</p>
<div className='mb-4 flex flex-wrap gap-2'>
<span className='inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800'>
Сменный график
</span>
<span className='inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800'>
Обучение
</span>
</div>
</div>
<Button variant='outline' size='sm'>
Подробнее
</Button>
</div>
</div>
</CardContent>
</Card>
),
)}
</TabsContent>
</Tabs>
<div className='mt-8 flex justify-center'>
<Button className='bg-red-600 hover:bg-red-700'>
Отправить резюме <ArrowRight className='ml-2 h-4 w-4' />
</Button>
</div>
</div>
</section>
{/* Partners Section */}
<section id='partners' className='bg-gray-50 py-16'>
<div className='container'>
<div className='mb-12 flex flex-col items-center justify-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Handshake className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Наши партнеры
</h2>
<p className='max-w-2xl text-gray-600'>
Мы сотрудничаем с ведущими компаниями для предоставления лучших
услуг нашим клиентам.
</p>
</div>
<div className='grid grid-cols-2 gap-8 md:grid-cols-4'>
{[1, 2, 3, 4, 5, 6, 7, 8].map((partner) => (
<div
key={partner}
className='flex h-32 items-center justify-center rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'
>
<Image
src={`/placeholder.svg?height=80&width=160&text=Partner ${partner}`}
alt={`Partner ${partner}`}
width={160}
height={80}
className='max-h-16 w-auto'
/>
</div>
))}
</div>
<div className='mt-12 text-center'>
<h3 className='mb-4 text-xl font-bold'>
Станьте нашим партнером
</h3>
<p className='mx-auto mb-6 max-w-2xl text-gray-600'>
Мы открыты для сотрудничества и новых партнерских отношений.
Свяжитесь с нами для обсуждения возможностей.
</p>
<Button className='bg-red-600 hover:bg-red-700'>
Связаться с нами
</Button>
</div>
</div>
</section>
{/* Charity Section */}
<section id='charity' className='py-16'>
<div className='container'>
<div className='grid items-center gap-12 md:grid-cols-2'>
<div className='relative order-2 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-1'>
<Image
src='/placeholder.svg?height=400&width=600'
alt='Charity Foundation'
fill
className='object-cover'
/>
</div>
<div className='order-1 md:order-2'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Heart className='h-6 w-6 text-red-600' />
</div>
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
Благотворительный фонд
</h2>
<p className='mb-6 text-gray-600'>
Наш благотворительный фонд был создан для поддержки социально
значимых проектов в Таджикистане. Мы стремимся внести свой
вклад в развитие общества и помочь тем, кто в этом нуждается.
</p>
<p className='mb-6 text-gray-600'>
Основные направления деятельности нашего фонда:
</p>
<ul className='mb-6 space-y-2'>
<li className='flex items-center'>
<ChevronRight className='mr-2 h-5 w-5 text-red-600' />
<span>Поддержка образовательных программ</span>
</li>
<li className='flex items-center'>
<ChevronRight className='mr-2 h-5 w-5 text-red-600' />
<span>Помощь детям из малообеспеченных семей</span>
</li>
<li className='flex items-center'>
<ChevronRight className='mr-2 h-5 w-5 text-red-600' />
<span>Экологические инициативы</span>
</li>
<li className='flex items-center'>
<ChevronRight className='mr-2 h-5 w-5 text-red-600' />
<span>Поддержка спортивных мероприятий</span>
</li>
</ul>
<Button className='bg-red-600 hover:bg-red-700'>
Подробнее о фонде
</Button>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className='bg-red-600 py-16 text-white'>
<div className='container'>
<div className='flex flex-col items-center text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
Присоединяйтесь к нам
</h2>
<p className='mb-8 max-w-2xl'>
Станьте частью нашей сети. Получайте специальные предложения,
бонусы и скидки.
</p>
<div className='flex flex-col gap-4 sm:flex-row'>
<Button
variant='outline'
className='border-white text-white hover:bg-white/10'
>
Скачать приложение
</Button>
<Button className='bg-white text-red-600 hover:bg-gray-100'>
Получить карту лояльности
</Button>
</div>
</div>
</div>
</section>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
{/* Footer */}
<footer className='bg-gray-900 py-12 text-white'>
<div className='container'>
<div className='grid grid-cols-1 gap-8 md:grid-cols-4'>
<div>
<div className='mb-4 flex items-center gap-2'>
<Fuel className='h-6 w-6 text-red-500' />
<span className='text-xl font-bold'>GasNetwork</span>
</div>
<p className='mb-4 text-gray-400'>
Сеть современных заправок в Таджикистане. Качественное топливо и
отличный сервис.
</p>
<div className='flex space-x-4'>
<a href='#' className='text-gray-400 hover:text-white'>
<svg
className='h-6 w-6'
fill='currentColor'
viewBox='0 0 24 24'
aria-hidden='true'
>
<path
fillRule='evenodd'
d='M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z'
clipRule='evenodd'
/>
</svg>
</a>
<a href='#' className='text-gray-400 hover:text-white'>
<svg
className='h-6 w-6'
fill='currentColor'
viewBox='0 0 24 24'
aria-hidden='true'
>
<path
fillRule='evenodd'
d='M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z'
clipRule='evenodd'
/>
</svg>
</a>
<a href='#' className='text-gray-400 hover:text-white'>
<svg
className='h-6 w-6'
fill='currentColor'
viewBox='0 0 24 24'
aria-hidden='true'
>
<path d='M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84' />
</svg>
</a>
</div>
</div>
<div>
<h3 className='mb-4 text-lg font-semibold'>Контакты</h3>
<div className='space-y-3'>
<div className='flex items-start'>
<MapPin className='mt-0.5 mr-3 h-5 w-5 text-red-500' />
<p>ул. Рудаки 137, Душанбе, Таджикистан</p>
</div>
<div className='flex items-start'>
<Phone className='mt-0.5 mr-3 h-5 w-5 text-red-500' />
<p>+992 (37) 223-45-67</p>
</div>
<div className='flex items-start'>
<Mail className='mt-0.5 mr-3 h-5 w-5 text-red-500' />
<p>info@gasnetwork.tj</p>
</div>
</div>
</div>
<div>
<h3 className='mb-4 text-lg font-semibold'>Навигация</h3>
<ul className='space-y-2'>
<li>
<Link
href='#stations'
className='text-gray-400 hover:text-white'
>
Наши заправки
</Link>
</li>
<li>
<Link
href='#about'
className='text-gray-400 hover:text-white'
>
О нас
</Link>
</li>
<li>
<Link
href='#vacancies'
className='text-gray-400 hover:text-white'
>
Вакансии
</Link>
</li>
<li>
<Link
href='#promotions'
className='text-gray-400 hover:text-white'
>
Акции
</Link>
</li>
<li>
<Link
href='#partners'
className='text-gray-400 hover:text-white'
>
Партнеры
</Link>
</li>
<li>
<Link
href='#charity'
className='text-gray-400 hover:text-white'
>
Благотворительность
</Link>
</li>
</ul>
</div>
<div>
<h3 className='mb-4 text-lg font-semibold'>Подписка</h3>
<p className='mb-4 text-gray-400'>
Подпишитесь на нашу рассылку, чтобы получать новости и
специальные предложения.
</p>
<form className='space-y-2'>
<input
type='email'
placeholder='Ваш email'
className='w-full rounded-md border border-gray-700 bg-gray-800 px-4 py-2 text-white'
/>
<Button className='w-full bg-red-600 hover:bg-red-700'>
Подписаться
</Button>
</form>
</div>
</div>
<div className='mt-8 border-t border-gray-800 pt-8 text-center text-gray-400'>
<p>
&copy; {new Date().getFullYear()} GasNetwork. Все права защищены.
</p>
</div>
</div>
</footer>
</div>
);

View File

@ -0,0 +1,70 @@
'use client';
import { Users } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import AnimatedCounter from './animated-counter';
export default function AboutCounter() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
},
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={sectionRef} className='my-8 grid grid-cols-3 gap-6 text-center'>
<div className='transform rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? <AnimatedCounter end={150} suffix='+' /> : '0+'}
</h3>
<p className='text-gray-600'>Сотрудников</p>
</div>
<div className='transform rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? <AnimatedCounter end={5} suffix='M+' /> : '0M+'}
</h3>
<p className='text-gray-600'>Литров топлива в месяц</p>
</div>
<div className='transform rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? (
<AnimatedCounter end={98} suffix='%' decimals={1} />
) : (
'0%'
)}
</h3>
<p className='text-gray-600'>Довольных клиентов</p>
</div>
</div>
);
}

View File

@ -0,0 +1,73 @@
'use client';
import { useEffect, useRef, useState } from 'react';
interface AnimatedCounterProps {
end: number;
duration?: number;
prefix?: string;
suffix?: string;
decimals?: number;
className?: string;
}
export default function AnimatedCounter({
end,
duration = 2000,
prefix = '',
suffix = '',
decimals = 0,
className = '',
}: AnimatedCounterProps) {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const startTimeRef = useRef<number | null>(null);
const frameRef = useRef<number | null>(null);
useEffect(() => {
// Reset when end value changes
countRef.current = 0;
startTimeRef.current = null;
setCount(0);
const animate = (timestamp: number) => {
if (startTimeRef.current === null) {
startTimeRef.current = timestamp;
}
const progress = timestamp - startTimeRef.current;
const percentage = Math.min(progress / duration, 1);
// Easing function for smoother animation
const easeOutQuart = 1 - Math.pow(1 - percentage, 4);
const currentCount = Math.min(easeOutQuart * end, end);
countRef.current = currentCount;
setCount(currentCount);
if (percentage < 1) {
frameRef.current = requestAnimationFrame(animate);
}
};
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
}
};
}, [end, duration]);
const formatNumber = (num: number) => {
return num.toFixed(decimals);
};
return (
<span className={className}>
{prefix}
{formatNumber(count)}
{suffix}
</span>
);
}

View File

@ -0,0 +1,116 @@
'use client';
import { MapPin } from 'lucide-react';
import { useEffect, useRef } from 'react';
export default function GasStationMap() {
const mapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// This is a placeholder for a real map implementation
// In a real application, you would use a mapping library like Mapbox, Google Maps, or Leaflet
if (mapRef.current) {
const canvas = document.createElement('canvas');
canvas.width = mapRef.current.clientWidth;
canvas.height = mapRef.current.clientHeight;
mapRef.current.appendChild(canvas);
const ctx = canvas.getContext('2d');
if (ctx) {
// Draw a simple map placeholder
ctx.fillStyle = '#f3f4f6';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw some roads
ctx.strokeStyle = '#d1d5db';
ctx.lineWidth = 5;
// Horizontal roads
for (let i = 1; i < 5; i++) {
ctx.beginPath();
ctx.moveTo(0, canvas.height * (i / 5));
ctx.lineTo(canvas.width, canvas.height * (i / 5));
ctx.stroke();
}
// Vertical roads
for (let i = 1; i < 8; i++) {
ctx.beginPath();
ctx.moveTo(canvas.width * (i / 8), 0);
ctx.lineTo(canvas.width * (i / 8), canvas.height);
ctx.stroke();
}
// Draw gas station markers
const stations = [
{ x: 0.2, y: 0.3 },
{ x: 0.5, y: 0.2 },
{ x: 0.7, y: 0.4 },
{ x: 0.3, y: 0.6 },
{ x: 0.6, y: 0.7 },
{ x: 0.8, y: 0.8 },
{ x: 0.1, y: 0.9 },
];
stations.forEach((station) => {
// Draw marker
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(
station.x * canvas.width,
station.y * canvas.height,
10,
0,
2 * Math.PI,
);
ctx.fill();
// Draw white border
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(
station.x * canvas.width,
station.y * canvas.height,
10,
0,
2 * Math.PI,
);
ctx.stroke();
});
// Add city names
ctx.fillStyle = '#1f2937';
ctx.font = 'bold 16px Arial';
ctx.fillText('Душанбе', canvas.width * 0.45, canvas.height * 0.15);
ctx.fillText('Худжанд', canvas.width * 0.2, canvas.height * 0.25);
ctx.fillText('Куляб', canvas.width * 0.7, canvas.height * 0.35);
ctx.fillText('Бохтар', canvas.width * 0.3, canvas.height * 0.55);
ctx.fillText('Хорог', canvas.width * 0.6, canvas.height * 0.65);
ctx.fillText('Истаравшан', canvas.width * 0.8, canvas.height * 0.75);
ctx.fillText('Пенджикент', canvas.width * 0.1, canvas.height * 0.85);
}
}
return () => {
if (mapRef.current) {
while (mapRef.current.firstChild) {
mapRef.current.removeChild(mapRef.current.firstChild);
}
}
};
}, []);
return (
<div className='relative h-full w-full'>
<div ref={mapRef} className='h-full w-full'></div>
<div className='absolute right-4 bottom-4 rounded-lg bg-white p-3 shadow-lg'>
<div className='flex items-center gap-2 text-sm font-medium'>
<MapPin className='h-5 w-5 text-red-600' />
<span>Наши заправки</span>
</div>
<p className='mt-1 text-xs text-gray-500'>Всего станций: 25</p>
</div>
</div>
);
}

View File

@ -0,0 +1,144 @@
'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card';
const promotions = [
{
id: 1,
title: 'Скидка 10% на премиум топливо',
description:
'Получите скидку 10% на премиум топливо при заправке от 30 литров',
image: '/placeholder.svg?height=200&width=400&text=Promotion 1',
validUntil: '31.12.2023',
},
{
id: 2,
title: 'Кофе в подарок',
description: 'Получите бесплатный кофе при заправке от 20 литров',
image: '/placeholder.svg?height=200&width=400&text=Promotion 2',
validUntil: '15.11.2023',
},
{
id: 3,
title: 'Двойные баллы по карте лояльности',
description: 'Получайте в 2 раза больше баллов при заправке в выходные дни',
image: '/placeholder.svg?height=200&width=400&text=Promotion 3',
validUntil: '30.11.2023',
},
{
id: 4,
title: 'Скидка на автомойку',
description: 'Скидка 20% на автомойку при заправке от 40 литров',
image: '/placeholder.svg?height=200&width=400&text=Promotion 4',
validUntil: '31.12.2023',
},
];
export default function PromotionSlider() {
const [currentIndex, setCurrentIndex] = useState(0);
const [visibleItems, setVisibleItems] = useState(3);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 640) {
setVisibleItems(1);
} else if (window.innerWidth < 1024) {
setVisibleItems(2);
} else {
setVisibleItems(3);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const nextSlide = () => {
setCurrentIndex((prevIndex) =>
prevIndex + visibleItems >= promotions.length ? 0 : prevIndex + 1,
);
};
const prevSlide = () => {
setCurrentIndex((prevIndex) =>
prevIndex === 0
? Math.max(0, promotions.length - visibleItems)
: prevIndex - 1,
);
};
return (
<div className='relative'>
<div className='overflow-hidden'>
<div
className='flex transition-transform duration-300 ease-in-out'
style={{
transform: `translateX(-${currentIndex * (100 / visibleItems)}%)`,
}}
>
{promotions.map((promo) => (
<div
key={promo.id}
className='w-full flex-none p-2 sm:w-1/2 lg:w-1/3'
>
<Card className='h-full overflow-hidden transition-shadow hover:shadow-lg'>
<div className='relative h-48'>
<Image
src={promo.image || '/placeholder.svg'}
alt={promo.title}
fill
className='object-cover'
/>
</div>
<CardContent className='p-4'>
<h3 className='mb-2 text-lg font-bold'>{promo.title}</h3>
<p className='mb-3 text-sm text-gray-600'>
{promo.description}
</p>
<div className='flex items-center justify-between'>
<span className='text-xs text-gray-500'>
Действует до: {promo.validUntil}
</span>
<Button
variant='outline'
size='sm'
className='border-red-600 text-red-600 hover:bg-red-50'
>
Подробнее
</Button>
</div>
</CardContent>
</Card>
</div>
))}
</div>
</div>
<Button
variant='outline'
size='icon'
className='absolute top-1/2 left-0 z-10 -translate-x-1/2 -translate-y-1/2 border-gray-200 bg-white shadow-lg'
onClick={prevSlide}
>
<ChevronLeft className='h-4 w-4' />
<span className='sr-only'>Предыдущий</span>
</Button>
<Button
variant='outline'
size='icon'
className='absolute top-1/2 right-0 z-10 translate-x-1/2 -translate-y-1/2 border-gray-200 bg-white shadow-lg'
onClick={nextSlide}
>
<ChevronRight className='h-4 w-4' />
<span className='sr-only'>Следующий</span>
</Button>
</div>
);
}

View File

@ -0,0 +1,66 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import AnimatedCounter from './animated-counter';
export default function StatsSection() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
},
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
<section ref={sectionRef} className='bg-red-600 py-12 text-white'>
<div className='container'>
<div className='grid grid-cols-2 gap-8 text-center md:grid-cols-4'>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
</h3>
<p className='text-sm text-white/80'>Заправок по стране</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={10000} suffix='+' /> : '0+'}
</h3>
<p className='text-sm text-white/80'>Клиентов ежедневно</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={15} /> : '0'}
</h3>
<p className='text-sm text-white/80'>Лет на рынке</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={24} suffix='/7' /> : '0/7'}
</h3>
<p className='text-sm text-white/80'>Работаем круглосуточно</p>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,3 +1,5 @@
'use client';
import { Provider } from 'react-redux';
import { store } from '../store';

View File

@ -0,0 +1,86 @@
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'bg-card text-card-foreground rounded-lg border shadow-sm',
className,
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'text-2xl leading-none font-semibold tracking-tight',
className,
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@ -19,7 +19,7 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
@ -41,7 +41,7 @@ DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
@ -57,7 +57,7 @@ DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
@ -75,7 +75,7 @@ const DropdownMenuContent = React.forwardRef<
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
@ -93,7 +93,7 @@ const DropdownMenuItem = React.forwardRef<
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
@ -117,7 +117,7 @@ DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
@ -139,7 +139,7 @@ const DropdownMenuRadioItem = React.forwardRef<
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
@ -157,7 +157,7 @@ const DropdownMenuLabel = React.forwardRef<
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator

View File

@ -0,0 +1,55 @@
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1',
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -4,13 +4,13 @@ import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import * as React from 'react';
import { Button } from '@/shared/shad-cn/button';
import { Button } from '@/shared/shadcn-ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/shared/shad-cn/dropdown-menu';
} from '@/shared/shadcn-ui/dropdown-menu';
import { capitalize } from '../lib/capitalize';

181
tailwind.config.js Normal file
View File

@ -0,0 +1,181 @@
// /** @type {import('tailwindcss').Config} */
// import tailwindcssAnimate from 'tailwindcss-animate';
// export default {
// darkMode: ['class'],
// content: ['./src/**/*.{js,ts,jsx,tsx}'],
// theme: {
// extend: {
// borderRadius: {
// lg: 'var(--radius)',
// md: 'calc(var(--radius) - 2px)',
// sm: 'calc(var(--radius) - 4px)',
// },
// colors: {
// background: 'hsl(var(--background))',
// foreground: 'hsl(var(--foreground))',
// card: {
// DEFAULT: 'hsl(var(--card))',
// foreground: 'hsl(var(--card-foreground))',
// },
// popover: {
// DEFAULT: 'hsl(var(--popover))',
// foreground: 'hsl(var(--popover-foreground))',
// },
// primary: {
// DEFAULT: 'hsl(var(--primary))',
// foreground: 'hsl(var(--primary-foreground))',
// },
// secondary: {
// DEFAULT: 'hsl(var(--secondary))',
// foreground: 'hsl(var(--secondary-foreground))',
// },
// muted: {
// DEFAULT: 'hsl(var(--muted))',
// foreground: 'hsl(var(--muted-foreground))',
// },
// accent: {
// DEFAULT: 'hsl(var(--accent))',
// foreground: 'hsl(var(--accent-foreground))',
// },
// destructive: {
// DEFAULT: 'hsl(var(--destructive))',
// foreground: 'hsl(var(--destructive-foreground))',
// },
// border: 'hsl(var(--border))',
// 'soft-gray': 'hsl(var(--soft-gray))',
// 'forest-green': 'hsl(var(--forest-green))',
// 'berry-red': 'hsl(var(--berry-red))',
// input: 'hsl(var(--input))',
// ring: 'hsl(var(--ring))',
// radio: 'hsl(var(--radio))',
// chart: {
// 1: 'hsl(var(--chart-1))',
// 2: 'hsl(var(--chart-2))',
// 3: 'hsl(var(--chart-3))',
// 4: 'hsl(var(--chart-4))',
// 5: 'hsl(var(--chart-5))',
// },
// },
// fontFamily: {
// inter: ['Inter', 'sans-serif'],
// },
// keyframes: {
// 'accordion-down': {
// from: {
// height: '0',
// },
// to: {
// height: 'var(--radix-accordion-content-height)',
// },
// },
// 'accordion-up': {
// from: {
// height: 'var(--radix-accordion-content-height)',
// },
// to: {
// height: '0',
// },
// },
// 'caret-blink': {
// '0%,70%,100%': { opacity: '1' },
// '20%,50%': { opacity: '0' },
// },
// 'collapsible-down': {
// from: { height: '0' },
// to: { height: 'var(--radix-collapsible-content-height)' },
// },
// 'collapsible-up': {
// from: { height: 'var(--radix-collapsible-content-height)' },
// to: { height: '0' },
// },
// },
// animation: {
// 'accordion-down': 'accordion-down 0.2s ease-out',
// 'accordion-up': 'accordion-up 0.2s ease-out',
// 'caret-blink': 'caret-blink 1.25s ease-out infinite',
// 'collapsible-down': 'collapsible-down 0.2s ease-out',
// 'collapsible-up': 'collapsible-up 0.2s ease-out',
// },
// },
// },
// plugins: [tailwindcssAnimate],
// };
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'*.{js,ts,jsx,tsx,mdx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: '#e11d48', // Red-600 for primary
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
};

View File

@ -19,7 +19,11 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@/entities/*": ["./src/entities/*"],
"@/features/*": ["./src/features/*"],
"@/shared/*": ["./src/shared/*"],
"@/widgets/*": ["./src/widgets/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],