Compare commits
2 Commits
443fd7c727
...
aab4c6b101
| Author | SHA1 | Date | |
|---|---|---|---|
| aab4c6b101 | |||
| 1e2fd40c9f |
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_",
|
|
||||||
"caughtErrorsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "error",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"@typescript-eslint/await-thenable": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
|
||||||
"@typescript-eslint/prefer-optional-chain": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-condition": "off",
|
|
||||||
"@typescript-eslint/no-redundant-type-constituents": "error",
|
|
||||||
"@typescript-eslint/ban-ts-comment": [
|
|
||||||
"off",
|
|
||||||
{
|
|
||||||
"ts-expect-error": "allow-with-description",
|
|
||||||
"ts-ignore": true,
|
|
||||||
"ts-nocheck": true,
|
|
||||||
"ts-check": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"react/display-name": "warn",
|
|
||||||
"react/no-unescaped-entities": "error",
|
|
||||||
"react/no-unknown-property": "error",
|
|
||||||
"react/jsx-key": "error",
|
|
||||||
"react/jsx-no-duplicate-props": "error",
|
|
||||||
"react/jsx-no-undef": "error",
|
|
||||||
"react/jsx-uses-react": "off",
|
|
||||||
"react/jsx-uses-vars": "error",
|
|
||||||
"react/no-array-index-key": "off",
|
|
||||||
"react/no-danger": "off",
|
|
||||||
"react/no-deprecated": "error",
|
|
||||||
"react/no-direct-mutation-state": "error",
|
|
||||||
"react/no-typos": "error",
|
|
||||||
"react/self-closing-comp": "warn",
|
|
||||||
"react-hooks/rules-of-hooks": "error",
|
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
|
||||||
"no-console": ["warn", { "allow": ["warn", "error", "log"] }],
|
|
||||||
"no-debugger": "error",
|
|
||||||
"no-alert": "warn",
|
|
||||||
"no-var": "error",
|
|
||||||
"prefer-const": "error",
|
|
||||||
"prefer-arrow-callback": "warn",
|
|
||||||
"no-duplicate-imports": "error",
|
|
||||||
"no-unreachable": "error",
|
|
||||||
"no-unused-expressions": "error",
|
|
||||||
"no-useless-return": "error",
|
|
||||||
"no-useless-escape": "error",
|
|
||||||
"no-constant-condition": "error",
|
|
||||||
"no-empty": "warn",
|
|
||||||
"no-extra-semi": "error",
|
|
||||||
"no-func-assign": "error",
|
|
||||||
"no-inner-declarations": "error",
|
|
||||||
"no-irregular-whitespace": "error",
|
|
||||||
"no-obj-calls": "error",
|
|
||||||
"no-sparse-arrays": "error",
|
|
||||||
"no-undef": "off",
|
|
||||||
"no-unexpected-multiline": "error",
|
|
||||||
"no-unreachable-loop": "error",
|
|
||||||
"use-isnan": "error",
|
|
||||||
"valid-typeof": "error",
|
|
||||||
"@next/next/no-html-link-for-pages": "error",
|
|
||||||
"@next/next/no-img-element": "warn"
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"src/shared/ui/chart.tsx",
|
|
||||||
"/src/shared/hooks/theme-message-listener.tsx"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
51
AGENTS.md
Normal file
51
AGENTS.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Универсальный одностраничный лендинг-шаблон (shadcn + Payload-ready) для любого продукта/бизнеса. Композиция секций собрана в `src/app/page.tsx`; каждая секция — отдельный widget.
|
||||||
|
|
||||||
|
## Project Specifics
|
||||||
|
|
||||||
|
- `src/app/page.tsx` — только композиция: `Header` + секции в `<main>` + `Footer`. Не превращай его в монолит, добавляй/убирай секции, а не код блоков.
|
||||||
|
- Каждая секция лендинга — самостоятельный widget в `src/widgets/*-section.tsx`; правь нужную секцию точечно.
|
||||||
|
- Базовые UI-примитивы shadcn — в `src/shared/ui/*` (vendored, не переписывай).
|
||||||
|
- Проверка после правок: `pnpm lint` и `pnpm build`.
|
||||||
|
|
||||||
|
## Design System
|
||||||
|
|
||||||
|
Источник токенов — `src/app/globals.css` (`@theme` + `:root`/`.dark`). Шрифт — Roboto Flex (`--font-roboto-flex`). Работай через семантические классы Tailwind (`bg-primary`, `text-muted-foreground`, `border`), не хардкодь hex/oklch.
|
||||||
|
|
||||||
|
**Важно — это нейтральная база.** Токены сейчас grayscale (монохром, `--radius` 0.625rem) — дефолтная shadcn-тема без характера. Это значит главный риск шаблона — **дженерик «AI-лендинг»**, который выглядит как все остальные. Поэтому:
|
||||||
|
|
||||||
|
- **Сначала задай направление.** Под конкретный продукт выбери осознанную эстетику и зашей её в **токены** `globals.css` (`--primary`, `--secondary`, `--accent`, `--radius`, типографику) — а не точечными классами по секциям. Тогда все секции автоматически становятся on-brand.
|
||||||
|
- **Расширяй личность, а не сбрасывай в дефолт.** Если у проекта уже задан характер — держи его консистентно во всех секциях.
|
||||||
|
|
||||||
|
| Роль | База (neutral) | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| `background` / `foreground` | white / near-black | фон и текст |
|
||||||
|
| `primary` | near-black | основные CTA, акценты |
|
||||||
|
| `secondary` / `muted` | light gray | вторичные поверхности, приглушённый текст |
|
||||||
|
| `accent` | light gray | подсветки (задай ярче под бренд) |
|
||||||
|
| `border` / `ring` | gray | границы, фокус |
|
||||||
|
|
||||||
|
Do / Don't:
|
||||||
|
- **Do:** определять тему через токены; держать единый ритм отступов/типографики между секциями; контент-first (оффер, доказательства, цена).
|
||||||
|
- **Don't:** оставлять монохром-дефолт «как есть» под реальный продукт; хардкодить цвета мимо токенов; стакать секции без выбранного направления — это и есть «одинаковый AI-лендинг».
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
Композиция: `src/app/page.tsx` → `Header` + секции + `Footer`.
|
||||||
|
|
||||||
|
| Блок | Widget |
|
||||||
|
|---|---|
|
||||||
|
| Шапка | `src/widgets/header.tsx` |
|
||||||
|
| Hero | `src/widgets/hero-section.tsx` |
|
||||||
|
| Features | `src/widgets/features-section.tsx` |
|
||||||
|
| Stats | `src/widgets/stats-section.tsx` |
|
||||||
|
| How it works | `src/widgets/how-it-works-section.tsx` |
|
||||||
|
| Comparison | `src/widgets/comparison-section.tsx` |
|
||||||
|
| Gallery | `src/widgets/gallery-section.tsx` |
|
||||||
|
| Social proof | `src/widgets/social-proof-section.tsx` |
|
||||||
|
| Team | `src/widgets/team-section.tsx` |
|
||||||
|
| Pricing | `src/widgets/pricing-section.tsx` |
|
||||||
|
| FAQ | `src/widgets/faq-section.tsx` |
|
||||||
|
| CTA | `src/widgets/cta-section.tsx` |
|
||||||
|
| Footer | `src/widgets/footer.tsx` |
|
||||||
139
eslint.config.mjs
Normal file
139
eslint.config.mjs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import reactPlugin from "@eslint-react/eslint-plugin"
|
||||||
|
import nextPlugin from "@next/eslint-plugin-next"
|
||||||
|
import tsPlugin from "@typescript-eslint/eslint-plugin"
|
||||||
|
import tsParser from "@typescript-eslint/parser"
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks"
|
||||||
|
|
||||||
|
const config = [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
"src/shared/ui/carousel.tsx",
|
||||||
|
"src/shared/ui/chart.tsx",
|
||||||
|
"src/shared/ui/resizable.tsx",
|
||||||
|
"src/shared/ui/sidebar.tsx",
|
||||||
|
"src/shared/hooks/theme-message-listener.tsx",
|
||||||
|
"src/shared/hooks/use-mobile.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,jsx,mjs,ts,tsx,mts,cts}"],
|
||||||
|
...nextPlugin.configs["core-web-vitals"],
|
||||||
|
},
|
||||||
|
...tsPlugin.configs["flat/recommended"],
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{jsx,tsx}"],
|
||||||
|
name: "@eslint-react/recommended",
|
||||||
|
plugins: {
|
||||||
|
"@eslint-react": reactPlugin,
|
||||||
|
},
|
||||||
|
rules: reactPlugin.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.flat["recommended-latest"].rules,
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "error",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
|
"@typescript-eslint/await-thenable": "error",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
||||||
|
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "off",
|
||||||
|
"@typescript-eslint/no-redundant-type-constituents": "error",
|
||||||
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"ts-expect-error": "allow-with-description",
|
||||||
|
"ts-ignore": true,
|
||||||
|
"ts-nocheck": true,
|
||||||
|
"ts-check": false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"no-console": ["warn", { allow: ["warn", "error", "log"] }],
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-alert": "warn",
|
||||||
|
"no-var": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-arrow-callback": "warn",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-constant-condition": "error",
|
||||||
|
"no-empty": "warn",
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-inner-declarations": "error",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-unexpected-multiline": "error",
|
||||||
|
"no-unreachable-loop": "error",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-typeof": "error",
|
||||||
|
"@next/next/no-html-link-for-pages": "error",
|
||||||
|
"@next/next/no-img-element": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{jsx,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"@eslint-react/no-missing-component-display-name": "warn",
|
||||||
|
"@eslint-react/dom-no-unknown-property": "error",
|
||||||
|
"@eslint-react/no-missing-key": "error",
|
||||||
|
"@eslint-react/no-duplicate-key": "error",
|
||||||
|
"@eslint-react/no-array-index-key": "off",
|
||||||
|
"@eslint-react/dom-no-dangerously-set-innerhtml": "off",
|
||||||
|
"@eslint-react/no-direct-mutation-state": "error",
|
||||||
|
"@eslint-react/no-nested-component-definitions": "off",
|
||||||
|
"@eslint-react/no-use-context": "off",
|
||||||
|
"@eslint-react/no-context-provider": "off",
|
||||||
|
"@eslint-react/use-state": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default config
|
||||||
47
package.json
47
package.json
@@ -41,36 +41,35 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.38.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^1.14.0",
|
||||||
"next": "14.2.33",
|
"next": "16.2.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.6",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.14.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.6",
|
||||||
"react-hook-form": "^7.66.1",
|
"react-hook-form": "^7.75.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^4",
|
||||||
"recharts": "^3.4.1",
|
"recharts": "^3.8.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint-react/eslint-plugin": "^5.7.4",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@next/eslint-plugin-next": "^16.2.5",
|
||||||
"@types/node": "^24.10.1",
|
"@tailwindcss/postcss": "^4.2.4",
|
||||||
"@types/react": "^18.2.48",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react": "^19.2.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/parser": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||||
"eslint": "^8.57.1",
|
"@typescript-eslint/parser": "^8.59.2",
|
||||||
"eslint-config-next": "14.2.33",
|
"eslint": "^10.3.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"tailwindcss": "^4.2.4",
|
||||||
"tailwindcss": "^4.1.17",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4999
pnpm-lock.yaml
generated
4999
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { LucideIcon } from "lucide-react";
|
import { ArrowRight, LucideIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight } from "lucide-react";
|
|
||||||
|
|
||||||
export interface FeatureCardProps {
|
export interface FeatureCardProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function StatCard({
|
|||||||
const { ref, isInView } = useInView<HTMLDivElement>({ threshold: 0.5 });
|
const { ref, isInView } = useInView<HTMLDivElement>({ threshold: 0.5 });
|
||||||
const count = useMotionValue(0);
|
const count = useMotionValue(0);
|
||||||
const rounded = useTransform(count, (latest) => Math.round(latest));
|
const rounded = useTransform(count, (latest) => Math.round(latest));
|
||||||
const hasAnimated = useRef(false);
|
const hasAnimatedRef = useRef(false);
|
||||||
|
|
||||||
// Попытка извлечь число из строки для анимации
|
// Попытка извлечь число из строки для анимации
|
||||||
const numericValue =
|
const numericValue =
|
||||||
@@ -47,8 +47,8 @@ export function StatCard({
|
|||||||
const prefix = typeof value === "string" ? value.replace(/[0-9.,]/g, "") : "";
|
const prefix = typeof value === "string" ? value.replace(/[0-9.,]/g, "") : "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInView && !hasAnimated.current && animated && isNumeric) {
|
if (isInView && !hasAnimatedRef.current && animated && isNumeric) {
|
||||||
hasAnimated.current = true;
|
hasAnimatedRef.current = true;
|
||||||
const controls = animate(count, numericValue, {
|
const controls = animate(count, numericValue, {
|
||||||
duration: 2,
|
duration: 2,
|
||||||
ease: "easeOut",
|
ease: "easeOut",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Card } from "@/shared/ui/card";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Github, Linkedin, Twitter } from "lucide-react";
|
import { Code2, Network, AtSign } from "lucide-react";
|
||||||
|
|
||||||
export interface TeamMemberCardProps {
|
export interface TeamMemberCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,9 +20,9 @@ export interface TeamMemberCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const socialIcons = {
|
const socialIcons = {
|
||||||
github: Github,
|
github: Code2,
|
||||||
linkedin: Linkedin,
|
linkedin: Network,
|
||||||
twitter: Twitter,
|
twitter: AtSign,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface UseInViewOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UseInViewReturn<T extends HTMLElement> {
|
export interface UseInViewReturn<T extends HTMLElement> {
|
||||||
ref: RefObject<T>;
|
ref: RefObject<T | null>;
|
||||||
isInView: boolean;
|
isInView: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function useScrollAnimation(config: ScrollAnimationConfig = {}) {
|
|||||||
transition: {
|
transition: {
|
||||||
duration,
|
duration,
|
||||||
delay,
|
delay,
|
||||||
ease: "easeOut",
|
ease: "easeOut" as const,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export const cardHover = {
|
|||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
y: -5,
|
y: -5,
|
||||||
transition: {
|
transition: {
|
||||||
type: "spring",
|
type: "spring" as const,
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
damping: 20,
|
damping: 20,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { GripVerticalIcon } from "lucide-react"
|
import { GripVerticalIcon } from "lucide-react"
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
import * as ResizablePrimitive from "react-resizable-panels"
|
||||||
|
|
||||||
@@ -9,12 +8,12 @@ import { cn } from "@/shared/lib/utils"
|
|||||||
function ResizablePanelGroup({
|
function ResizablePanelGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
}: ResizablePrimitive.GroupProps) {
|
||||||
return (
|
return (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.Group
|
||||||
data-slot="resizable-panel-group"
|
data-slot="resizable-panel-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full aria-[orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -22,9 +21,7 @@ function ResizablePanelGroup({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResizablePanel({
|
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
|
||||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,25 +29,25 @@ function ResizableHandle({
|
|||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: ResizablePrimitive.SeparatorProps & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.Separator
|
||||||
data-slot="resizable-handle"
|
data-slot="resizable-handle"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{withHandle && (
|
{withHandle && (
|
||||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border">
|
||||||
<GripVerticalIcon className="size-2.5" />
|
<GripVerticalIcon className="size-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Button } from "@/shared/ui/button";
|
|||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { ArrowRight, Sparkles } from "lucide-react";
|
import { ArrowRight, Sparkles } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CTA Section - финальный призыв к действию
|
* CTA Section - финальный призыв к действию
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Separator } from "@/shared/ui/separator";
|
import { Separator } from "@/shared/ui/separator";
|
||||||
import { Github, Twitter, Linkedin, Youtube } from "lucide-react";
|
import { Code2, AtSign, Network, Play } from "lucide-react";
|
||||||
|
|
||||||
const footerLinks = {
|
const footerLinks = {
|
||||||
product: {
|
product: {
|
||||||
@@ -43,11 +43,13 @@ const footerLinks = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CURRENT_YEAR = new Date().getFullYear()
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
{ name: "GitHub", icon: Github, href: "#" },
|
{ name: "GitHub", icon: Code2, href: "#" },
|
||||||
{ name: "Twitter", icon: Twitter, href: "#" },
|
{ name: "Twitter", icon: AtSign, href: "#" },
|
||||||
{ name: "LinkedIn", icon: Linkedin, href: "#" },
|
{ name: "LinkedIn", icon: Network, href: "#" },
|
||||||
{ name: "YouTube", icon: Youtube, href: "#" },
|
{ name: "YouTube", icon: Play, href: "#" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,7 +110,7 @@ export function Footer() {
|
|||||||
{/* Bottom Bar */}
|
{/* Bottom Bar */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
© {new Date().getFullYear()} TaskFlow. Все права защищены.
|
© {CURRENT_YEAR} TaskFlow. Все права защищены.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
|
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { useState, useEffect } from "react";
|
|||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "@/shared/ui/sheet";
|
import { Sheet, SheetContent, SheetTrigger } from "@/shared/ui/sheet";
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, Moon, Sun, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Moon, Sun } from "lucide-react";
|
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: "Возможности", href: "#features" },
|
{ name: "Возможности", href: "#features" },
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { SectionContainer } from "@/features/section-container";
|
import { SectionContainer } from "@/features/section-container";
|
||||||
import { SectionHeader } from "@/features/section-header";
|
import { SectionHeader } from "@/features/section-header";
|
||||||
import { StepCard } from "@/features/step-card";
|
import { StepCard } from "@/features/step-card";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { UserPlus, FolderPlus, Users as UsersIcon, Rocket } from "lucide-react";
|
import { UserPlus, FolderPlus, Users as UsersIcon, Rocket } from "lucide-react";
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const pricingPlans = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Enterprise",
|
name: "Enterprise",
|
||||||
price: "custom",
|
price: "custom" as const,
|
||||||
description: "Для крупного бизнеса",
|
description: "Для крупного бизнеса",
|
||||||
features: [
|
features: [
|
||||||
{ text: "Неограниченные участники", included: true },
|
{ text: "Неограниченные участники", included: true },
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
|
|||||||
Reference in New Issue
Block a user