feat: base layout
This commit is contained in:
@@ -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
25
components.json
Normal 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": {}
|
||||
}
|
||||
29
package.json
29
package.json
@@ -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
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 |
@@ -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
BIN
public/font/IRANSansXV.woff
Normal file
Binary file not shown.
BIN
public/font/IRANSansXV.woff2
Normal file
BIN
public/font/IRANSansXV.woff2
Normal file
Binary file not shown.
15
public/font/font.css
Normal file
15
public/font/font.css
Normal 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
21
public/logo.svg
Normal 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 |
110
src/components/ui/avatar.tsx
Normal file
110
src/components/ui/avatar.tsx
Normal 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,
|
||||
}
|
||||
67
src/components/ui/button.tsx
Normal file
67
src/components/ui/button.tsx
Normal 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 }
|
||||
17
src/components/ui/link.tsx
Normal file
17
src/components/ui/link.tsx
Normal 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 };
|
||||
88
src/components/ui/tabs.tsx
Normal file
88
src/components/ui/tabs.tsx
Normal 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 }
|
||||
9
src/components/views/Footer.astro
Normal file
9
src/components/views/Footer.astro
Normal 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>
|
||||
40
src/components/views/Header.astro
Normal file
40
src/components/views/Header.astro
Normal 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>
|
||||
106
src/components/views/footer-bar.tsx
Normal file
106
src/components/views/footer-bar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
src/components/views/header-nav.tsx
Normal file
32
src/components/views/header-nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
<slot />
|
||||
<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
27
src/lib/nav-links.ts
Normal 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
6
src/lib/utils.ts
Normal 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
9
src/pages/blog.astro
Normal 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
9
src/pages/contact.astro
Normal 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>
|
||||
@@ -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
9
src/pages/plans.astro
Normal 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
227
src/styles/global.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
|
||||
Reference in New Issue
Block a user