feat: base layout

This commit is contained in:
2026-06-07 16:04:13 +03:30
parent 3370f349d6
commit 2db6a63e38
28 changed files with 8652 additions and 34 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
dedupe-peer-dependents=true

View File

@@ -1,5 +1,12 @@
// @ts-check
import { defineConfig } from 'astro/config';
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
integrations: [react()],
vite: {
plugins: [tailwindcss()],
},
});

25
components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View File

@@ -12,6 +12,33 @@
"astro": "astro"
},
"dependencies": {
"astro": "^6.4.4"
"@astrojs/react": "^5.0.7",
"@fontsource-variable/geist": "^5.2.9",
"@tailwindcss/vite": "^4.3.0",
"astro": "^6.4.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"radix-ui": "^1.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"shadcn": "^4.10.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@types/node": "^25.9.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"vite": "^7.3.5"
},
"pnpm": {
"overrides": {
"vite": "$vite"
},
"onlyBuiltDependencies": [
"sharp"
]
}
}

7769
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

BIN
public/font/IRANSansXV.woff Normal file

Binary file not shown.

Binary file not shown.

15
public/font/font.css Normal file
View File

@@ -0,0 +1,15 @@
@font-face {
font-family: "iransansxv";
src:
url("IRANSansXV.woff") format("woff-variations"),
/* will be the standard and works in Safari now */ url("IRANSansXV.woff")
format("woff");
/* for the other supporting browsers */
font-weight: 100 1000;
font-display: fallback;
}
.ss02 {
-moz-font-feature-settings: "ss02";
-webkit-font-feature-settings: "ss02";
font-feature-settings: "ss02";
}

21
public/logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg width="345" height="376" viewBox="0 0 345 376" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M262 60.9844L217 171.984L163.5 81.9844C163.5 81.9844 156 67.9844 143 67.9844C130 67.9844 122 87.4844 122 87.4844L56 249.484L83.5 295.984L147 137.484L201 228.984C201 228.984 209 241.484 222 241.484C235 241.484 241.5 226.484 241.5 226.484L318.5 34.4844C318.5 34.4844 326 14.4843 316.5 4.98431C307 -4.51565 289.5 2.48441 289.5 2.48441L195 43.4844L217 80.9844L262 60.9844Z" fill="url(#paint0_linear_1781_1260)"/>
<g filter="url(#filter0_f_1781_1260)">
<ellipse cx="172.5" cy="337" rx="132.5" ry="6" fill="url(#paint1_linear_1781_1260)" fill-opacity="0.5"/>
</g>
<defs>
<filter id="filter0_f_1781_1260" x="0" y="291" width="345" height="92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_1781_1260"/>
</filter>
<linearGradient id="paint0_linear_1781_1260" x1="257.5" y1="72.4843" x2="57.8981" y2="295.984" gradientUnits="userSpaceOnUse">
<stop stop-color="#A5FF01"/>
<stop offset="1" stop-color="#192600"/>
</linearGradient>
<linearGradient id="paint1_linear_1781_1260" x1="241.366" y1="338.469" x2="240.039" y2="354.693" gradientUnits="userSpaceOnUse">
<stop stop-color="#A5FF01"/>
<stop offset="1" stop-color="#192600"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,110 @@
import * as React from "react"
import { Avatar as AvatarPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}

View File

@@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
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.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,17 @@
import React from "react";
import { Button } from "./button";
export interface LinkProps extends React.ComponentProps<typeof Button> {
href: string;
children: React.ReactNode;
}
const Link: React.FC<LinkProps> = ({ href, children, ...props }) => {
return (
<Button {...props} asChild>
<a href={href}>{children}</a>
</Button>
);
};
export { Link };

View File

@@ -0,0 +1,88 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@@ -0,0 +1,9 @@
---
import { FooterBar } from "@/components/views/footer-bar"
---
<footer class="shrink-0 border-t border-border bg-background">
<div class="mx-auto max-w-7xl px-6 py-8 lg:px-8">
<FooterBar client:load />
</div>
</footer>

View File

@@ -0,0 +1,40 @@
---
import { Link } from "@/components/ui/link";
import { HeaderNav } from "@/components/views/header-nav";
const pathname = Astro.url.pathname;
---
<header class="shrink-0 border-b border-border bg-background">
<div
class="mx-auto flex h-16 max-w-7xl items-center justify-between gap-3 px-4 md:h-20 md:gap-8 md:px-6 lg:h-[88px] lg:px-8"
>
<div
class="flex min-w-0 flex-1 items-center justify-between gap-3 md:flex-none md:justify-start md:gap-10 lg:gap-16"
>
<HeaderNav
client:load
pathname={pathname}
className="order-1 min-w-0 md:order-2"
/>
<a href="/" class="order-2 flex shrink-0 items-center gap-3 md:order-1">
<img
src="/logo.svg"
alt="نکستا"
class="size-9 md:size-10"
width="40"
height="40"
/>
<span class="hidden text-2xl font-bold text-primary md:inline">نکستا</span>
</a>
</div>
<Link
href="/plans"
className="hidden shrink-0 rounded-full px-6 md:inline-flex lg:px-8"
>
شروع کنید
</Link>
</div>
</header>

View File

@@ -0,0 +1,106 @@
import { Button } from "@/components/ui/button";
import { footerNavLinks, socialLinks } from "@/lib/nav-links";
import { cn } from "@/lib/utils";
function SocialIcon({ icon }: { icon: (typeof socialLinks)[number]["icon"] }) {
const className = "size-4";
switch (icon) {
case "facebook":
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M14 6h2.5V3h-3c-2.76 0-4.5 1.7-4.5 4.3V10H7v3h2.5v9H14v-9h2.7l.5-3H14V7.2C14 6.5 14.2 6 14 6z" />
</svg>
);
case "instagram":
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
aria-hidden="true"
>
<rect x="3.5" y="3.5" width="17" height="17" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.2" cy="6.8" r="1" fill="currentColor" stroke="none" />
</svg>
);
case "x":
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M16.6 4h3.1l-6.8 7.8L21 20h-6.2l-4.9-6.4L4.2 20H1.1l7.3-8.4L3 4h6.4l4.4 5.8L16.6 4zm-1.1 14.4h1.7L7.8 5.5H6l9.5 12.9z" />
</svg>
);
case "telegram":
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M21.8 4.2 3.6 11.1c-1.2.5-1.2 1.2-.2 1.5l4.6 1.4 1.8 5.5c.2.7.4.9 1 .9.6 0 .8-.3 1.1-.7l2.6-2.5 5.4 4c1 .6 1.7.3 1.9-1.1L23 5.5c.3-1.4-.5-2-1.2-1.3zM9.6 13.3l9.8-6.2c.5-.3.9-.1.5.2L11.2 14l-.4 3.8-1.2-4.5z" />
</svg>
);
}
}
type FooterBarProps = {
className?: string;
};
export function FooterBar({ className }: FooterBarProps) {
return (
<div
className={cn(
"flex flex-col-reverse items-center justify-between gap-6 sm:flex-row",
className,
)}
>
<nav className="flex items-center gap-8">
{footerNavLinks.map((link) => (
<Button
key={link.href}
variant="link"
className="h-auto px-0 text-base font-normal text-muted-foreground hover:text-foreground"
asChild
>
<a href={link.href}>{link.label}</a>
</Button>
))}
</nav>
<div className="flex items-center gap-3">
{socialLinks.map((social) => (
<Button
key={social.icon}
variant="outline"
size="icon"
className="size-10 rounded-full border-border text-muted-foreground hover:border-primary/40 hover:text-foreground"
asChild
>
<a
href={social.href}
target="_blank"
rel="noopener noreferrer"
aria-label={social.label}
>
<SocialIcon icon={social.icon} />
</a>
</Button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { getActiveNavTab, headerNavLinks } from "@/lib/nav-links"
import { cn } from "@/lib/utils"
type HeaderNavProps = {
pathname?: string
className?: string
}
export function HeaderNav({ pathname = "/", className }: HeaderNavProps) {
const activeTab = getActiveNavTab(pathname)
return (
<Tabs value={activeTab} className={cn("gap-0", className)}>
<TabsList
variant="line"
className="h-auto gap-3 bg-transparent p-0 pb-1 sm:gap-5 md:gap-8"
>
{headerNavLinks.map((link) => (
<TabsTrigger
key={link.value}
value={link.value}
asChild
className="h-auto flex-none px-0.5 py-1 text-sm font-normal whitespace-nowrap md:px-1 md:text-base data-active:after:bg-primary"
>
<a href={link.href}>{link.label}</a>
</TabsTrigger>
))}
</TabsList>
</Tabs>
)
}

View File

@@ -1,23 +1,31 @@
---
import "../styles/global.css";
import Header from "@/components/views/Header.astro";
import Footer from "@/components/views/Footer.astro";
interface Props {
title?: string;
}
const { title = "نکستا" } = Astro.props;
---
<!doctype html>
<html lang="en">
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/font/font.css" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
<title>{title}</title>
</head>
<body>
<Header />
<main>
<slot />
</main>
<Footer />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

27
src/lib/nav-links.ts Normal file
View File

@@ -0,0 +1,27 @@
export const headerNavLinks = [
{ value: "plans", label: "پلن‌ها", href: "/plans" },
{ value: "contact", label: "تماس با ما", href: "/contact" },
{ value: "blog", label: "بلاگ", href: "/blog" },
] as const
export const footerNavLinks = [
{ label: "تماس با ما", href: "/contact" },
{ label: "بلاگ", href: "/blog" },
] as const
export const socialLinks = [
{ label: "Facebook", href: "https://facebook.com", icon: "facebook" },
{ label: "Instagram", href: "https://instagram.com", icon: "instagram" },
{ label: "X", href: "https://x.com", icon: "x" },
{ label: "Telegram", href: "https://t.me", icon: "telegram" },
] as const
export type HeaderNavValue = (typeof headerNavLinks)[number]["value"]
export function getActiveNavTab(pathname: string): HeaderNavValue {
const path = pathname.replace(/\/$/, "") || "/"
if (path === "/blog" || path.startsWith("/blog/")) return "blog"
if (path === "/contact" || path.startsWith("/contact/")) return "contact"
return "plans"
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

9
src/pages/blog.astro Normal file
View File

@@ -0,0 +1,9 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="بلاگ | نکستا">
<section class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
<h1 class="text-3xl font-bold text-foreground">بلاگ</h1>
</section>
</Layout>

9
src/pages/contact.astro Normal file
View File

@@ -0,0 +1,9 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="تماس با ما | نکستا">
<section class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
<h1 class="text-3xl font-bold text-foreground">تماس با ما</h1>
</section>
</Layout>

View File

@@ -1,11 +1,3 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
return Astro.redirect("/plans");
---
<Layout>
<Welcome />
</Layout>

9
src/pages/plans.astro Normal file
View File

@@ -0,0 +1,9 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="پلن‌ها | نکستا">
<section class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
<h1 class="text-3xl font-bold text-foreground">پلن‌ها</h1>
</section>
</Layout>

227
src/styles/global.css Normal file
View File

@@ -0,0 +1,227 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";
@custom-variant dark (&:is(.dark *));
@theme inline {
--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);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Figma palette extras */
--color-tertiary: var(--tertiary);
--color-tertiary-foreground: var(--tertiary-foreground);
--color-foreground-disabled: var(--foreground-disabled);
}
/* Figma palette: primary #A5FF01, bg #060912, card #17181C, text main #EFEEEA, text sec #A2A09B, text disabled #363636, tertiary #32D9CB, secondary #00A6C0 */
:root {
--radius: 0.625rem;
/* Background */
--background: #060912;
/* Text main */
--foreground: #efeeea;
/* Card */
--card: #17181c;
--card-foreground: #efeeea;
--popover: #17181c;
--popover-foreground: #efeeea;
/* Primary */
--primary: #a5ff01;
--primary-foreground: #060912;
/* Secondary */
--secondary: #00a6c0;
--secondary-foreground: #efeeea;
/* Muted / surfaces */
--muted: #17181c;
/* Text secondary */
--muted-foreground: #a2a09b;
/* Tertiary = accent */
--tertiary: #32d9cb;
--tertiary-foreground: #17181c;
--accent: #32d9cb;
--accent-foreground: #17181c;
/* Text disabled */
--foreground-disabled: #363636;
--destructive: #ef4444;
--border: #2d3b2e;
--input: #2d3b2e;
--ring: #a5ff01;
--chart-1: #a5ff01;
--chart-2: #00a6c0;
--chart-3: #32d9cb;
--chart-4: #efeeea;
--chart-5: #a2a09b;
--sidebar: #17181c;
--sidebar-foreground: #efeeea;
--sidebar-primary: #a5ff01;
--sidebar-primary-foreground: #060912;
--sidebar-accent: #00a6c0;
--sidebar-accent-foreground: #efeeea;
--sidebar-border: #2d3b2e;
--sidebar-ring: #a5ff01;
}
.dark {
--background: #060912;
--foreground: #efeeea;
--card: #17181c;
--card-foreground: #efeeea;
--popover: #17181c;
--popover-foreground: #efeeea;
--primary: #a5ff01;
--primary-foreground: #060912;
--secondary: #00a6c0;
--secondary-foreground: #efeeea;
--muted: #17181c;
--muted-foreground: #a2a09b;
--tertiary: #32d9cb;
--tertiary-foreground: #17181c;
--accent: #32d9cb;
--accent-foreground: #17181c;
--foreground-disabled: #363636;
--destructive: #ef4444;
--border: #2d3b2e;
--input: #2d3b2e;
--ring: #a5ff01;
--chart-1: #a5ff01;
--chart-2: #00a6c0;
--chart-3: #32d9cb;
--chart-4: #efeeea;
--chart-5: #a2a09b;
--sidebar: #17181c;
--sidebar-foreground: #efeeea;
--sidebar-primary: #a5ff01;
--sidebar-primary-foreground: #060912;
--sidebar-accent: #00a6c0;
--sidebar-accent-foreground: #efeeea;
--sidebar-border: #2d3b2e;
--sidebar-ring: #a5ff01;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
min-height: 100vh;
min-height: 100dvh;
}
body {
@apply bg-background text-foreground;
min-height: 100vh;
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
font-family: "iransansxv", sans-serif;
}
}
@layer utilities {
.chat-messages-scroll {
scrollbar-width: thin;
scrollbar-color: var(--primary) transparent;
}
.chat-messages-scroll::-webkit-scrollbar {
width: 8px;
}
.chat-messages-scroll::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages-scroll::-webkit-scrollbar-thumb {
background-color: color-mix(in oklab, var(--primary) 70%, transparent);
border-radius: 9999px;
}
.chat-messages-scroll::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in oklab, var(--primary) 90%, transparent);
}
.chat-messages-scroll::-webkit-scrollbar-button {
-webkit-appearance: none;
display: none !important;
width: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
}
.chat-messages-scroll::-webkit-scrollbar-button:single-button,
.chat-messages-scroll::-webkit-scrollbar-button:start:decrement,
.chat-messages-scroll::-webkit-scrollbar-button:end:increment,
.chat-messages-scroll::-webkit-scrollbar-button:vertical:start:decrement,
.chat-messages-scroll::-webkit-scrollbar-button:vertical:end:increment {
-webkit-appearance: none;
display: none !important;
width: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
}
.scrollbar-styled {
scrollbar-width: thin;
scrollbar-color: rgb(var(--muted)) transparent;
}
.scrollbar-styled::-webkit-scrollbar {
width: 6px;
}
.scrollbar-styled::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-styled::-webkit-scrollbar-thumb {
background-color: rgb(var(--muted));
border-radius: 3px;
}
.scrollbar-styled::-webkit-scrollbar-thumb:hover {
background-color: rgb(var(--muted) / 0.8);
}
}

View File

@@ -1,4 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]