Add website files

This commit is contained in:
2026-01-19 23:20:04 +03:30
parent 69bee6597e
commit 2f0bba567f
24 changed files with 3855 additions and 1 deletions

View File

@@ -1,3 +1,3 @@
# bokhary-homepage
Homepage for bokhary.ir - coded in typescript with react.js and tailwind
A cute homepage for bokhary.ir

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/image.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bokhary | بخاری</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "bokhary-homepage",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/vazirmatn": "^5.2.8",
"@tailwindcss/vite": "^4.1.18",
"@tsparticles/all": "^3.9.1",
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

3221
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- '@tsparticles/engine'
- esbuild

BIN
public/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@@ -0,0 +1,16 @@
export default function Bokhary() {
return (
<div className="min-h-screen grid place-items-center">
<div className="flex flex-col items-center gap-3">
<img
src="/image.png"
alt="Bokhary"
className="w-64 h-auto drop-shadow-[0_0_15px_rgba(255,255,255,0.8)]"
/>
<span className="text-4xl font-bold text-orange-200 -translate-y-18 font-vazirmatn [text-shadow:0_0_5px_rgba(0,0,0,1),0_0_10px_rgba(0,0,0,1),0_0_20px_rgba(0,0,0,0.9),0_0_30px_rgba(0,0,0,0.8)]">
بـخــــاری
</span>
</div>
</div>
);
}

23
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,23 @@
import type { MouseEventHandler } from "react";
type Props = {
onClick?: MouseEventHandler<HTMLButtonElement>;
children: React.ReactNode;
};
export default function ModalButton({ onClick, children }: Props) {
return (
<button
type="button"
onClick={onClick}
className="relative px-5 py-1.5 rounded-lg overflow-hidden transition-all duration-300 mx-2 my-1.5
bg-linear-to-br from-orange-300/60 to-orange-400/90
hover:scale-105 active:scale-95
text-white font-medium shadow-md font-vazirmatn
before:absolute before:inset-0 before:bg-white/10 before:opacity-0 hover:before:opacity-100
before:transition-opacity before:duration-300"
>
{children}
</button>
);
}

View File

@@ -0,0 +1,55 @@
import { useState } from "react";
type Props = {
code: string;
fileName: string;
className?: string;
};
export default function CodeBlock({ code, fileName, className }: Props) {
const [copied, setCopied] = useState(false);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
} catch {
}
};
return (
<div
className={`
rounded-lg bg-gray-900 text-sm text-gray-100
${className}
`}
>
<div className="flex items-center justify-between border-b border-gray-800 px-4 py-2 bg-gray-800 rounded-t-lg">
<span className="text-xs font-medium text-gray-300">
{fileName}
</span>
<button
dir='rtl'
onClick={copyToClipboard}
className={`
flex items-center gap-1 px-2.5 py-0.5 text-xs font-medium
rounded transition-colors duration-200
${
copied
? "bg-green-600 hover:bg-green-500 text-white"
: "bg-gray-700 hover:bg-gray-600 text-gray-200"
}
`}
>
{copied ? "Copied" : "Copy"}
</button>
</div>
<pre className="overflow-x-auto whitespace-pre-wrap wrap-break-word p-4">
<code>{code}</code>
</pre>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Bokhary() {
return (
<a href="https://element.bokhary.ir" target="_blank" rel="noreferrer" className="">
Open in Element
</a>
);
}

View File

@@ -0,0 +1,30 @@
import type { MouseEventHandler } from "react";
type Props = {
onClick?: MouseEventHandler<HTMLButtonElement>;
text?: string;
}
export default function Button({ onClick, text }: Props) {
return (
<button
onClick={onClick}
className="group relative px-5 py-3 rounded-full overflow-hidden transition-all duration-300 hover:scale-105 active:scale-95 cursor-pointer mx-1"
>
<div className="absolute inset-0 bg-orange-500/10 backdrop-blur-xl border border-orange-400/30 rounded-full"></div>
<div className="absolute inset-0 bg-linear-to-br from-orange-400/20 via-orange-500/5 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="absolute inset-0 bg-linear-to-r from-transparent via-orange-300/30 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 rounded-full"></div>
<div className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl bg-orange-500/40"></div>
<div className="absolute inset-px rounded-full bg-linear-to-b from-orange-400/10 to-transparent"></div>
<span className="relative flex items-center gap-2 text-white font-medium text-lg font-vazirmatn">
{text}
<svg className="w-6 h-6 transition-transform group-hover:rotate-12 duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</span>
</button>
);
}

124
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,124 @@
import { useEffect } from "react";
import { createPortal } from "react-dom";
import Button from "./Button";
import CodeBlock from "./CodeBlock";
type Props = {
isOpen: boolean;
onClose: () => void;
title?: string;
};
const hosts = `
# Bokhary Hosts
37.32.13.184 bokhary.ir
37.32.13.184 cinny.bokhary.ir
37.32.13.184 element.bokhary.ir
`.trim();
export default function Modal({
isOpen,
onClose,
title = "راهنمای استفاده",
}: Props) {
useEffect(() => {
if (!isOpen) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [isOpen, onClose]);
return createPortal(
<div
className={`
fixed inset-0 z-50 flex items-center justify-center
bg-black/30 backdrop-blur-sm transition-opacity duration-300
${isOpen ? "opacity-100" : "opacity-0 pointer-events-none"}
`}
onClick={onClose}
>
<div
className={`
relative w-xl max-w-[90%] bg-linear-to-br from-orange-600/90 to-orange-300/80
rounded-xl pb-6 text-center text-white shadow-xl
transform transition-all duration-300
${isOpen ? "scale-100" : "scale-95"}
max-h-[80vh]
`}
onClick={(e) => e.stopPropagation()}
style={{
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
<div
className="flex items-center justify-between px-4 py-3 bg-black/20 rounded-t-xl"
style={{ background: "inherit" }}
>
<p className="text-lg font-bold font-vazirmatn" dir="rtl">
{title}
</p>
<button
type="button"
onClick={onClose}
aria-label="Close"
className="text-2xl hover:text-orange-200 transition-colors"
>
×
</button>
</div>
<div
className={`
overflow-y-auto overflow-x-hidden
max-h-[calc(79vh-3rem)] p-8 pt-0
`}
>
<style>{`
div::-webkit-scrollbar { display: none; }
`}</style>
<p className="mt-8 text-lg leading-relaxed font-vazirmatn text-center font-bold" dir="rtl">
کلاینتهای تحت وب
</p>
<Button onClick={() => open("https://element.bokhary.ir", "_blank")}>المنت وب</Button>
<Button onClick={() => open("https://cinny.bokhary.ir", "_blank")}>سینی</Button>
<p className="mt-8 text-lg leading-relaxed font-vazirmatn text-center font-bold" dir="rtl">
کلاینتهای اندروید
</p>
<Button onClick={() => open("https://bokhary.ir/apps/Element.apk", "_blank")}>المنت</Button>
<Button onClick={() => open("https://bokhary.ir/apps/elementx.apk", "_blank")}>المنت ایکس</Button>
<Button onClick={() => open("https://bokhary.ir/apps/fluffy.apk", "_blank")}>فلافی چت</Button>
<p className="mt-8 text-md leading-relaxed font-vazirmatn text-center" dir="rtl">
برای رفع اختلالات DNS مقادیر زیر را به فایل hosts سیستمعامل خود اضافه کنید.
</p>
<CodeBlock fileName="Your Hosts file" code={hosts} className="mt-3 text-left" />
<p className="mt-8 text-md leading-relaxed font-vazirmatn text-center" dir="rtl">
یا در اندروید از نسخه تغییر یافته AdAway استفاده کنید.
</p>
<Button
onClick={() =>
open(
"https://s34.picofile.com/file/8489250218/AdAway_6_1_4.apk.html",
"_blank"
)
}
>
AdAway
</Button>
</div>
</div>
</div>,
document.body
);
}

View File

@@ -0,0 +1,57 @@
import { useEffect, useState, memo } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadAll } from "@tsparticles/all";
import type { ISourceOptions } from "@tsparticles/engine";
function BackgroundParticles() {
const [init, setInit] = useState(false);
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadAll(engine);
}).then(() => setInit(true));
}, []);
const options: ISourceOptions = {
background: { color: { value: "#2b2b2b" } },
fpsLimit: 60,
particles: {
number: { value: 150, density: { enable: true, width: 1920, height: 1080 } },
color: { value: "#ff2e2e" },
shape: { type: "circle" },
opacity: { value: { min: 0.3, max: 0.8 }, animation: { enable: true, speed: 1 } },
size: { value: { min: 2, max: 5 } },
links: { enable: true, distance: 150, color: "#ffffff", opacity: 0.3, width: 1 },
move: { enable: true, speed: 1, outModes: { default: "bounce" } },
},
interactivity: {
events: {
onHover: { enable: true, mode: "grab" },
onClick: { enable: true, mode: "push" },
},
modes: {
grab: { distance: 100, links: { opacity: 0.7 } },
push: { quantity: 1 },
},
},
};
if (!init) return null;
return (
<div
style={{
position: "fixed",
inset: 0,
width: "100dvw",
height: "100dvh",
zIndex: -1,
pointerEvents: "none",
}}
>
<Particles id="tsparticles" options={options} />
</div>
);
}
export default memo(BackgroundParticles);

View File

@@ -0,0 +1,50 @@
import { useState, useEffect } from 'react';
export default function TimePassed({ startDate = "2026-01-08T21:00:00" }) {
const OUTAGE_START_DATE = new Date(startDate);
const [formattedString, setFormattedString] = useState('');
const calculateTimePassed = () => {
const now = new Date();
const difference = now.getTime() - OUTAGE_START_DATE.getTime();
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((difference % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds };
};
const generatePersianString = (time: Record<string, number>) => {
const { days, hours, minutes, seconds } = time;
const parts = [];
if (days > 0) parts.push(`${days.toLocaleString('fa-IR')} روز`);
if (hours > 0) parts.push(`${hours.toLocaleString('fa-IR')} ساعت`);
if (minutes > 0) parts.push(`${minutes.toLocaleString('fa-IR')} دقیقه`);
if (seconds > 0) parts.push(`${seconds.toLocaleString('fa-IR')} ثانیه`);
if (parts.length === 0) return "0 ثانیه";
return parts.join(" و ") + " " + "از قطعی سراسری اینترنت گذشته است...";
};
useEffect(() => {
const updateTimer = () => {
const calculatedTime = calculateTimePassed();
setFormattedString(generatePersianString(calculatedTime));
};
updateTimer();
const intervalId = setInterval(updateTimer, 1000);
return () => clearInterval(intervalId);
}, [startDate]);
return formattedString
};

27
src/components/Toast.tsx Normal file
View File

@@ -0,0 +1,27 @@
import type { ReactNode } from "react";
type Props = {
children: ReactNode;
onClose: () => void;
};
export default function Toast({ children, onClose }: Props) {
return (
<div className="group relative w-full px-5 py-2 rounded-lg overflow-hidden transition-all duration-300 hover:scale-105 active:scale-95 cursor-pointer">
<div className="absolute inset-0 bg-orange-500/10 backdrop-blur-xl border border-orange-400/30 rounded-lg"></div>
<div className="absolute inset-0 bg-linear-to-br from-orange-400/20 via-orange-500/5 to-transparent rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="absolute inset-0 bg-linear-to-r from-transparent via-orange-300/30 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 rounded-lg"></div>
<div className="absolute inset-0 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl bg-orange-500/40"></div>
<div className="absolute inset-px rounded-lg bg-linear-to-b from-orange-400/10 to-transparent"></div>
<span dir='rtl' className="relative flex items-center gap-2 text-white font-medium text-sm font-vazirmatn ml-4">
{children}
</span>
<button
onClick={onClose}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white opacity-70 hover:opacity-100 transition-opacity duration-200"
>
×
</button>
</div>
);
}

4
src/declarations.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '@fontsource/vazirmatn' {
export const style: unknown; // Or more specific types if known, but unknown is generally safe for imports like this
export default style;
}

42
src/index.css Normal file
View File

@@ -0,0 +1,42 @@
@import "tailwindcss";
@theme {
--font-vazirmatn: 'Vazirmatn', sans-serif;
--font-vazirmatn-sans: 'Vazirmatn', ui-sans-serif, system-ui, sans-serif;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
.animate-fade-in {
animation: fadeIn 0.3s forwards;
}
@layer utilities {
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #ff9f1c #00000033;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #ff9f1c;
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: #ff7f00;
}
}

47
src/index.tsx Normal file
View File

@@ -0,0 +1,47 @@
import { StrictMode, useState } from "react";
import { createRoot } from "react-dom/client";
import FloatingButton from "./components/FloatingButton";
import Modal from "./components/Modal";
import BackgroundParticles from "./components/Particles";
import Bokhary from "./components/Bokhary";
import Toast from "./components/Toast";
import TimePassed from "./components/TimePassed"
import "@fontsource/vazirmatn";
import "./index.css";
function App() {
const [modalOpen, setModalOpen] = useState(false);
const [toast, showToast] = useState(true);
return (
<StrictMode>
<BackgroundParticles />
<div className="relative min-h-dvh">
<div className="fixed m-2 top-0 left-0 right-0">
{toast && (
<Toast onClose={() => showToast(false)}>
<TimePassed></TimePassed>
</Toast>
)}
</div>
<div className="relative z-10">
<Bokhary />
</div>
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-20">
<FloatingButton text="اتصال" onClick={() => setModalOpen(true)} />
</div>
</div>
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
</StrictMode>
);
}
createRoot(document.getElementById("root")!).render(<App />);

6
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.css' {
const classes: { [key: string]: string };
export default classes;
}

28
tsconfig.app.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

9
vite.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})