Compare commits

..

2 Commits

80 changed files with 3848 additions and 8655 deletions

View File

@ -160,9 +160,9 @@ This document describes the exact implementation of the login page based on the
onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 text-[#4FD1C7] bg-white border-gray-300 rounded focus:ring-[#4FD1C7] focus:ring-2 accent-[#4FD1C7]"
/>
// <Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
// همیشه متصل بمانم
// </Label>
<Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
همیشه متصل بمانم
</Label>
</div>
{/* Submit Button */}

View File

@ -1,7 +1,13 @@
@import url(/font/fontiran.css);
@import "tailwindcss";
/* Persian/Farsi font support */
@import url("https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap");
@theme {
--font-sans:
"Vazirmatn", "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* Teal color scale */
--color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1;
@ -28,37 +34,21 @@
--color-slate-900: #0f172a;
--color-slate-950: #020617;
--color-pr-green: #3aea83;
--color-pr-blue: #69c8ea;
--color-pr-red: #f76276;
--color-pr-gray: #3f415a;
--color-pr-green : #3AEA83;
--color-pr-blue : #69C8EA;
--color-pr-red : #F76276;
--color-pr-gray : #3F415A;
}
html,
body {
@apply bg-background text-foreground;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
body {
font-family: IRANYekanX;
direction: rtl;
background-color: #cdcdcd;
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
input,
textarea {
font-family: IRANYekanX;
}
/* RTL Support */
html[dir="rtl"] {
direction: rtl;
@ -78,7 +68,6 @@ html[dir="rtl"] body {
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-dark-blue: var(--dark-blue);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
@ -98,13 +87,13 @@ html[dir="rtl"] body {
:root {
--radius: 0.5rem;
--color-green: #3aea83;
--color-blue: #69c8ea;
--color-red: #f76276;
--color-green: #3AEA83;
--color-blue: #69C8EA;
--color-red: #F76276;
/* primary colors */
--color-pr-gray: #3f415a;
--color-pr-green: var(--color-green);
--color-pr-gray : #3F415A;
--color-pr-green : var(--color-green);
/* Light theme colors */
--background: #ffffff;
@ -126,7 +115,6 @@ html[dir="rtl"] body {
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #22c55e;
--dark-blue: #33364d;
/* Primary color scale */
--color-primary-50: #f0fdf4;
@ -258,11 +246,12 @@ html[dir="rtl"] body {
body {
@apply bg-background text-foreground;
}
}
/* Persian/Farsi font class */
.font-persian {
font-family: "IRANYekanX";
font-family: "Vazirmatn", "Inter", ui-sans-serif, system-ui, sans-serif;
}
/* Custom utility classes */
@ -422,13 +411,9 @@ html[dir="rtl"] body {
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(
to bottom,
rgba(16, 185, 129, 0.6),
rgba(16, 185, 129, 0.9)
); /* emerald */
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.6), rgba(16, 185, 129, 0.9)); /* emerald */
border-radius: 9999px;
border: 0.5px solid transparent;
border: .5px solid transparent;
background-clip: padding-box;
}
@ -444,10 +429,11 @@ html[dir="rtl"] body {
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
}
:root {
--form-control-color: #3f415a;
--form-control-disabled: ##5f6284;
--form-background: #3aea83;
--form-control-color: #3F415A;
--form-control-disabled: ##5F6284;
--form-background: #3AEA83;
}
input[type="checkbox"] {
@ -455,11 +441,11 @@ input[type="checkbox"] {
appearance: none;
margin: 0;
font: inherit;
color: #5f6284;
color: #5F6284;
background-color: transparent;
width: 1.15em;
height: 1.15em;
border: 1px solid #5f6284;
border: 1px solid #5F6284;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
@ -483,7 +469,7 @@ input[type="checkbox"]:checked::before {
}
input[type="checkbox"]:checked {
background-color: #3aea83;
background-color: #3AEA83 ;
border: 1px solid transparent;
}

View File

@ -176,16 +176,16 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
/>
{/* Remember Me Checkbox */}
{/* <div className="flex justify-end">
<div className="flex justify-end">
<CheckboxField
id="remember"
label="همیشه متصل بمان"
label="همیشه متصل بمانم"
checked={formData.rememberMe}
onChange={(checked) => updateField("rememberMe", checked)}
disabled={isLoading}
size="md"
/>
</div> */}
</div>
{/* Login Button */}
<Button
@ -212,9 +212,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
{/* Right Side - Branding */}
<LoginSidebar>
<LoginBranding
// brandName="پتروشیمی آپادانا"
brandName="پتروشیمی نوری"
// brandName="پتروشیمی بندر امام"
brandName="پتروشیمی بندر امام"
engSub="Inception by Fara"
companyName="توسعه‌یافته توسط شرکت رهپویان دانش و فناوری فرا"
logo={<img src="/brand2.svg"/>}

View File

@ -1,7 +1,6 @@
import React from "react";
import { cn } from "~/lib/utils";
interface LoginLayoutProps {
children: React.ReactNode;
className?: string;
@ -107,26 +106,14 @@ export function LoginBranding({
}: LoginBrandingProps) {
return (
<>
<div className="flex justify-end">
{/* Top Logo */}
<div className="flex justify-end">
<div className="text-slate-800 font-persian">
<div className="text-lg font-bold leading-tight">
<img
src="/brand.svg?v=1"
alt="Brand Logo"
className="w-auto h-8"
onError={(e) => {
e.target.style.display = 'none';
console.log('Image failed to load');
}}
/>
</div>
<div className="text-lg font-bold leading-tight">
<img src="/brand.svg" />
</div>
</div>
</div>
</div>
{/* Bottom Section */}
<div className="flex flex-col gap-2 mb-4 items-end justify-end">

View File

@ -1,5 +1,3 @@
//این فایل مخصوص
//شماتیک نوری
import React from "react";
import { formatNumber } from "~/lib/utils";
@ -27,12 +25,17 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
<div className={`info-box`} style={style}>
<div className="info-box-content">
<div className="info-row">
<div className="info-label">هزینه عملیاتی:</div>
<div className="info-label">درآمد:</div>
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">هزینه:</div>
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">افزایش ظرفیت:</div>
<div className="info-label">ظرفیت:</div>
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div>
</div>
@ -42,41 +45,25 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
};
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
const sample = [
{ id: "PX", name: "PX", imageUrl: "/abniro.png" },
{ id: "LLTE&HLTE", name: "LLTE&HLTE", imageUrl: "/besparan.png" },
{ id: "BTX", name: "BTX", imageUrl: "/khwarazmi.png" },
{ id: "Utility", name: "Utility", imageUrl: "/faravash1.png" },
{ id: "Reforming", name: "Reforming", imageUrl: "/faravash2.png" },
{ id: "Storage Tank", name: "Storage Tank", imageUrl: "/kimia.png" }
];
const merged = sample.map(company => {
const found = companies.find(item => item.id == company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0 };
});
const displayCompanies = merged;
// Ensure we have exactly 6 companies
const displayCompanies = companies;
// Positions inside a 5x4 grid (col, row)
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
const gridPositions = [
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "LLTE&HLTE"}, // left - top band
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "BTX"}, // middle top (image sits in row 2, info box goes to row 1)
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "Utility"}, // right - top band
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "Storage Tank"}, // left - bottom band
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "PX"}, // middle bottom (image sits in row 3, info box goes to row 4)
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "Reforming"}, // right - bottom band
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
];
return (
<div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name);
const gp = gridPositions.find(v => v.name === company.name) ;
return (
<>
<div
@ -137,10 +124,10 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
align-self : center;
justify-self : center;
padding : .2rem 1.2rem;
min-width : 8rem;
background-color: transparent;
}
.info-box-content {
display: flex;
flex-direction: column;
@ -154,12 +141,11 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
gap : .5rem;
justify-content : space-between;
direction: rtl;
}
.info-row:has(.info-value.cost) {
border-bottom: 1px solid #F76276;
}
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
}
.info-label {
color: #FFFFFF;
@ -177,6 +163,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
margin-bottom : .5rem;
}
.info-value.revenue { color: #fff;}
.info-value.cost { color: #fff; }
.info-value.capacity { color: #fff; }

View File

@ -1,213 +0,0 @@
//این فایل مخصوص
//شماتیک بندر امام
import React from "react";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
id: string;
imageUrl: string;
name: string;
costReduction: number;
revenue?: number;
capacity?: number;
costI : number,
capacityI : number,
revenueI : number,
cost : number | string,
};
export type D3ImageInfoProps = {
companies: CompanyInfo[];
width?: number;
height?: number;
};
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
const hideCapacity = company.name === "خوارزمی"; // اگر خوارزمی بود ظرفیت مخفی شود
return (
<div className={`info-box`} style={style}>
<div className="info-box-content">
<div className="info-row">
<div className="info-label">درآمد:</div>
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">هزینه:</div>
{
(hideCapacity ?
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
:
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
)
}
<div className="info-unit">میلیون ریال</div>
</div>
{!hideCapacity && (
<div className="info-row">
<div className="info-label">ظرفیت:</div>
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div>
</div>
)}
</div>
</div>
);
};
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
// Ensure we have exactly 6 companies
const sample = [
{ id: "آب نیرو", name: "آب نیرو", imageUrl: "/abniro.png" },
{ id: "بسپاران", name: "بسپاران", imageUrl: "/besparan.png" },
{ id: "خوارزمی", name: "خوارزمی", imageUrl: "/khwarazmi.png" },
{ id: "فراورش 1", name: "فراورش 1", imageUrl: "/faravash1.png" },
{ id: "فراورش 2", name: "فراورش 2", imageUrl: "/faravash2.png" },
{ id: "کیمیا", name: "کیمیا", imageUrl: "/kimia.png" }
];
const merged = sample.map(company => {
const found = companies.find(item => item.id == company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0 };
});
const displayCompanies = merged;
console.log(displayCompanies)
// Positions inside a 5x4 grid (col, row)
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
const gridPositions = [
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
];
return (
<div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name) ;
return (
<>
<div
key={company.id}
className={`company-item`}
style={{ gridColumn: gp.col, gridRow: gp.row }}
>
<div className="company-image-containe">
<img
src={company.imageUrl}
alt={company.name}
className="company-image"
/>
</div>
{company.name}
</div>
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
</>);
})}
</div>
<style jsx>{`
.company-grid-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 5px;
width: 100%;
height: 500px;
}
.company-item {
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.company-image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.company-image {
object-fit: contain;
height : 100px;
}
.info-box {
border: 1px solid #3F415A;
border-radius: 10px;
height: max-content;
align-self : center;
justify-self : center;
padding : .2rem 1.2rem;
min-width : 8rem;
background-color: transparent;
}
.info-box-content {
display: flex;
flex-direction: column;
justify-content: center;
}
.info-row {
position : relative;
margin: .1rem 0;
display: flex;
gap : .5rem;
justify-content : space-between;
direction: rtl;
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
}
.info-label {
color: #FFFFFF;
font-size: 11px;
font-weight: 300;
text-align: right;
margin : auto 0;
}
.info-value {
color: #34D399;
font-size: 14px;
font-weight: 500;
text-align: right;
margin-bottom : .5rem;
}
.info-value.revenue { color: #fff;}
.info-value.cost { color: #fff; }
.info-value.cost2 { color: #fff; }
.info-value.capacity { color: #fff; }
.info-unit {
position: absolute;
left: 0;
bottom: 2px;
color: #ACACAC;
font-size: 6px;
font-weight: 400;
}
`}</style>
</div>
);
}

View File

@ -1,211 +0,0 @@
//این فایل مخصوص
//شماتیک آپادانا
import React from "react";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
id: string;
imageUrl: string;
name: string;
costReduction: number;
revenue?: number;
capacity?: number;
costI: number;
capacityI: number;
revenueI: number;
cost: number | string;
};
export type D3ImageInfoProps = {
companies: CompanyInfo[];
width?: number;
height?: number;
};
const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
// const hideCapacity = company.name === "واحد 300"; // اگر واحد 300 بود ظرفیت مخفی شود
const hideCapacity = false;
return (
<div className={`info-box`} style={style}>
<div className="info-box-content">
<div className="info-row">
<div className="info-label">درآمد:</div>
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">هزینه:</div>
{hideCapacity ? (
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
) : (
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
)}
<div className="info-unit">میلیون ریال</div>
</div>
{!hideCapacity && (
<div className="info-row">
<div className="info-label">ظرفیت:</div>
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div>
</div>
)}
</div>
</div>
);
};
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
// واحدهای جدید - 4 واحد
const sample = [
{ id: "واحد 100", name: "واحد 100", imageUrl: "/abniro.png" },
{ id: "واحد 200", name: "واحد 200", imageUrl: "/besparan.png" },
{ id: "واحد 300", name: "واحد 300", imageUrl: "/khwarazmi.png" },
{ id: "واحد 400", name: "واحد 400", imageUrl: "/faravash1.png" }
];
const merged = sample.map(company => {
const found = companies.find(item => item.id === company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0, costReduction: 0, costI: 0, capacityI: 0, revenueI: 0 };
});
const displayCompanies = merged;
console.log(displayCompanies);
// موقعیت‌های جدید برای چیدمان لوزی شکل (3 ردیف - 1-2-1)
// گرید 5x4 نگه داشته شده اما موقعیت‌ها تغییر کرده
const gridPositions = [
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
];
return (
<div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name);
return (
<React.Fragment key={company.id}>
<div
className={`company-item`}
style={{ gridColumn: gp?.col, gridRow: gp?.row }}
>
<div className="company-image-container">
<img
src={company.imageUrl}
alt={company.name}
className="company-image"
/>
</div>
{company.name}
</div>
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
</React.Fragment>
);
})}
</div>
<style jsx>{`
.company-grid-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 5px;
width: 100%;
height: 500px;
}
.company-item {
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.company-image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.company-image {
object-fit: contain;
height: 100px;
}
.info-box {
border: 1px solid #3F415A;
border-radius: 10px;
height: max-content;
align-self: center;
justify-self: center;
padding: .2rem 1.2rem;
min-width: 8rem;
background-color: transparent;
}
.info-box-content {
display: flex;
flex-direction: column;
justify-content: center;
}
.info-row {
position: relative;
margin: .1rem 0;
display: flex;
gap: .5rem;
justify-content: space-between;
direction: rtl;
}
.info-row:has(.info-value.revenue) {
border-bottom: 1px solid #3AEA83;
}
.info-row:has(.info-value.cost) {
border-bottom: 1px solid #F76276;
}
.info-label {
color: #FFFFFF;
font-size: 11px;
font-weight: 300;
text-align: right;
margin: auto 0;
}
.info-value {
color: #34D399;
font-size: 14px;
font-weight: 500;
text-align: right;
margin-bottom: .5rem;
}
.info-value.revenue { color: #fff; }
.info-value.cost { color: #fff; }
.info-value.cost2 { color: #fff; }
.info-value.capacity { color: #fff; }
.info-unit {
position: absolute;
left: 0;
bottom: 2px;
color: #ACACAC;
font-size: 6px;
font-weight: 400;
}
`}</style>
</div>
);
}

View File

@ -1,10 +1,5 @@
import React from "react";
import { formatNumber } from "~/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip"
interface DataItem {
label: string;
@ -59,27 +54,12 @@ export function DashboardCustomBarChart({
<div className="flex-row-reverse items-center gap-2 flex min-h-6 h-10 rounded-lg overflow-hidden">
{/* Animated bar */}
<div
className={`h-auto gap-2 overflow-hidden ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
className={`h-auto gap-2 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
style={{ width: `${widthPercentage}%` }}
>
{ widthPercentage > 20 ? (
<span className="text-[#3F415A] min-w-max text-left font-persian font-medium text-sm py-1 w-max">
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max">
{item.label}
</span>
) : (
<Tooltip>
<TooltipTrigger className={`${item.color}`} asChild>
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1">
<span className="invisible">""</span>
</span>
</TooltipTrigger>
<TooltipContent className={`${item.color} ${item.color.replace("bg","fill")}`}>
<p className="font-persian text-sm">{item.label}</p>
</TooltipContent>
</Tooltip>
) }
</div>
<span className="text-white font-bold text-base">
{formatNumber(item.value)}

View File

@ -1,6 +1,38 @@
import { Book, CheckCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useState, useEffect } from "react";
import { DashboardLayout } from "./layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Progress } from "~/components/ui/progress";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
} from "recharts";
import apiService from "~/lib/api";
import toast from "react-hot-toast";
import {
Calendar,
TrendingUp,
TrendingDown,
Target,
Lightbulb,
DollarSign,
Minus,
CheckCircle,
Book,
} from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { D3ImageInfo } from "./d3-image-info";
import {
Label,
PolarGrid,
@ -8,21 +40,10 @@ import {
RadialBar,
RadialBarChart,
} from "recharts";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { ChartContainer } from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils";
import { MetricCard } from "~/components/ui/metric-card";
import { Progress } from "~/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { D3ImageInfo } from "./d3-image-info";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { DashboardLayout } from "./layout";
import { BaseCard } from "~/components/ui/base-card";
export function DashboardHome() {
const [dashboardData, setDashboardData] = useState<any | null>(null);
@ -30,54 +51,35 @@ export function DashboardHome() {
const [error, setError] = useState<string | null>(null);
// Chart and schematic data from select API
const [companyChartData, setCompanyChartData] = useState<
{
category: string;
capacity: number;
revenue: number;
cost: number;
costI: number;
capacityI: number;
revenueI: number;
}[]
{ category: string; capacity: number; revenue: number; cost: number , costI : number,
capacityI : number,
revenueI : number }[]
>([]);
const [date, setDate] = useStoredDate();
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
fetchDashboardData();
}, []);
useEffect(() => {
if (date?.end && date?.start) fetchDashboardData();
}, [date]);
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
// First authenticate if needed
const token = localStorage.getItem("auth_token");
if (!token) {
await apiService.login("inogen_admin", "123456");
}
// Fetch top cards data
const topCardsResponse = await apiService.call({
main_page_first_function: {
start_date: date.start || null,
end_date: date.end || null,
},
main_page_first_function: {},
});
// Fetch left section data
const leftCardsResponse = await apiService.call({
main_page_second_function: {
start_date: date.start || null,
end_date: date.end || null,
},
main_page_second_function: {},
});
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
@ -110,10 +112,6 @@ export function DashboardHome() {
"sum(pre_project_income)",
"sum(increased_income_after_innovation)",
],
Conditions: [
["start_date", ">=", date.start || null, "and"],
["start_date", "<=", date.end || null],
],
GroupBy: ["related_company"],
};
@ -132,49 +130,31 @@ export function DashboardHome() {
let incCapacityTotal = 0;
const chartRows = rows.map((r) => {
const rel = r?.related_company ?? "-";
const preFee =
Number(r?.pre_innovation_fee_sum ?? 0) >= 0
? r?.pre_innovation_fee_sum
: 0;
const costRed =
Number(r?.innovation_cost_reduction_sum ?? 0) >= 0
? r?.innovation_cost_reduction_sum
: 0;
const preCap =
Number(r?.pre_project_production_capacity_sum ?? 0) >= 0
? r?.pre_project_production_capacity_sum
: 0;
const incCap =
Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0
? r?.increased_capacity_after_innovation_sum
: 0;
const preInc =
Number(r?.pre_project_income_sum ?? 0) >= 0
? r?.pre_project_income_sum
: 0;
const incInc =
Number(r?.increased_income_after_innovation_sum ?? 0) >= 0
? r?.increased_income_after_innovation_sum
: 0;
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) > 0 ? r?.pre_innovation_fee_sum : 0;
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) > 0 ? r?.innovation_cost_reduction_sum : 0;
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) > 0 ? r?.pre_project_production_capacity_sum : 0;
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) > 0 ? r?.increased_capacity_after_innovation_sum : 0;
const preInc = Number(r?.pre_project_income_sum ?? 0) > 0 ? r?.pre_project_income_sum : 0;
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) > 0 ? r?.increased_income_after_innovation_sum : 0;
incCapacityTotal += incCap;
const capacityPct = preCap >= 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc >= 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee >= 0 ? (costRed / preFee) * 100 : 0;
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
return {
category: rel,
capacity: isFinite(capacityPct) ? capacityPct : 0,
revenue: isFinite(revenuePct) ? revenuePct : 0,
cost: isFinite(costPct) ? costPct : 0,
costI: costRed,
capacityI: incCap,
revenueI: incInc,
costI : costRed,
capacityI : incCap,
revenueI : incInc
};
});
setCompanyChartData(chartRows);
// setTotalIncreasedCapacity(incCapacityTotal);
setTotalIncreasedCapacity(incCapacityTotal);
} catch (error) {
console.error("Error fetching dashboard data:", error);
const errorMessage =
@ -187,24 +167,25 @@ export function DashboardHome() {
};
// RadialBarChart data for ideas visualization
// const getIdeasChartData = () => {
// if (!dashboardData?.topData)
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
const getIdeasChartData = () => {
if (!dashboardData?.topData)
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
// const registered = parseFloat(
// dashboardData.topData.registered_innovation_technology_idea || "0"
// );
// const ongoing = parseFloat(
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
// );
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
const registered = parseFloat(
dashboardData.topData.registered_innovation_technology_idea || "0",
);
const ongoing = parseFloat(
dashboardData.topData.ongoing_innovation_technology_ideas || "0",
);
const percentage =
registered > 0 ? Math.round((ongoing / registered) * 100) : 0;
// return [
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
// ];
// };
return [
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
];
};
// const chartData = getIdeasChartData();
const chartData = getIdeasChartData();
const chartConfig = {
visitors: {
@ -249,11 +230,11 @@ export function DashboardHome() {
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-pr-green rounded-t-sm"
className="w-full bg-green-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-pr-red rounded-t-sm"
className="w-full bg-red-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
</div>
@ -270,7 +251,7 @@ export function DashboardHome() {
if (loading) {
return (
<DashboardLayout>
<div className="grid grid-cols-3 gap-4 animate-pulse">
<div className="p-3 pb-0 grid grid-cols-3 gap-4 animate-pulse">
{/* Top Cards Row */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
<SkeletonCard />
@ -331,7 +312,7 @@ export function DashboardHome() {
return (
<DashboardLayout>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-3 p-3 pb-0 gap-4">
{/* Top Cards Row - Redesigned to match other components */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
{/* Ideas Card */}
@ -348,22 +329,23 @@ export function DashboardHome() {
visitors:
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
?.registered_innovation_technology_idea || "0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
?.ongoing_innovation_technology_ideas ||
"0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1"
"1",
)) *
100
100,
)
: 0,
fill: "var(--color-green)",
fill: "green",
},
]}
startAngle={90}
@ -371,18 +353,19 @@ export function DashboardHome() {
90 +
((parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
?.registered_innovation_technology_idea || "0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
?.ongoing_innovation_technology_ideas || "0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "1"
?.registered_innovation_technology_idea ||
"1",
)) *
100
100,
)
: 0) /
100) *
@ -395,10 +378,14 @@ export function DashboardHome() {
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-pr-red last:fill-[#24273A]"
className="first:fill-red-400 last:fill-[#111628]"
polarRadius={[38, 31]}
/>
<RadialBar dataKey="visitors" background cornerRadius={5} />
<RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
@ -424,22 +411,22 @@ export function DashboardHome() {
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"0"
"0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas ||
"0"
"0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1"
"1",
)) *
100
100,
)
: 0
: 0,
)}
</tspan>
</text>
@ -456,14 +443,14 @@ export function DashboardHome() {
<div className="font-light text-sm">ثبت شده :</div>
{formatNumber(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
?.registered_innovation_technology_idea || "0",
)}
</span>
<span className="flex items-center gap-1 font-bold text-base">
<div className="font-light text-sm">در حال اجرا :</div>
{formatNumber(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
?.ongoing_innovation_technology_ideas || "0",
)}
</span>
</div>
@ -473,34 +460,16 @@ export function DashboardHome() {
{/* Revenue Card */}
<MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
",",
""
) || "0"
}
percentValue={
dashboardData.topData
?.technology_innovation_based_revenue_growth_percent
}
value={dashboardData.topData?.technology_innovation_based_revenue_growth || "0"}
percentValue={Math.round(dashboardData.topData?.technology_innovation_based_revenue_growth_percent) || "0"}
percentLabel="درصد به کل درآمد"
/>
{/* Cost Reduction Card */}
<MetricCard
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
value={Math.round(
parseFloat(
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
""
) || "0"
)
)}
percentValue={
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent || "0"
}
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0") / 1000000)}
percentValue={Math.round(dashboardData.topData?.technology_innovation_based_cost_reduction_percent) || "0"}
percentLabel="درصد به کل هزینه"
/>
@ -517,9 +486,9 @@ export function DashboardHome() {
browser: "budget",
visitors: parseFloat(
dashboardData.topData
?.innovation_budget_achievement_percent || "0"
?.innovation_budget_achievement_percent || "0",
),
fill: "var(--color-green)",
fill: "green",
},
]}
startAngle={90}
@ -537,10 +506,14 @@ export function DashboardHome() {
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-pr-red last:fill-[#24273A]"
className="first:fill-red-400 last:fill-[#111628]"
polarRadius={[38, 31]}
/>
<RadialBar dataKey="visitors" background cornerRadius={5} />
<RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
@ -566,8 +539,8 @@ export function DashboardHome() {
Math.round(
dashboardData.topData
?.innovation_budget_achievement_percent ||
0
)
0,
),
)}
</tspan>
</text>
@ -587,10 +560,10 @@ export function DashboardHome() {
parseFloat(
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
/,/g,
""
) || "0"
)
)
"",
) || "0",
) / 1000000000,
),
)}
</span>
<span className="flex items-center gap-1 text-base font-bold mr-auto">
@ -600,10 +573,10 @@ export function DashboardHome() {
parseFloat(
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
/,/g,
""
) || "0"
)
)
"",
) || "0",
) / 1000000000,
),
)}
</span>
</div>
@ -614,21 +587,18 @@ export function DashboardHome() {
{/* Main Content with Tabs */}
<Tabs
defaultValue="canvas"
defaultValue="charts"
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
>
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
<p className="p-6 font-persian font-semibold text-lg ">
تحقق ارزش ها
</p>
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
<TabsTrigger value="canvas" className="cursor-pointer">
<TabsList className="bg-transparent py-2 border m-6 border-gray-600">
<TabsTrigger value="canvas" className="">
شماتیک
</TabsTrigger>
<TabsTrigger
value="charts"
className=" text-white cursor-pointer font-light "
>
<TabsTrigger value="charts" className=" text-white font-light ">
مقایسه ای
</TabsTrigger>
</TabsList>
@ -641,41 +611,17 @@ export function DashboardHome() {
<TabsContent value="canvas" className="w-ful h-full">
<div className="p-4 h-full w-full">
<D3ImageInfo
//پتروشیمی بندر امام
// companies={companyChartData.map((item) => {
// const imageMap: Record<string, string> = {
// بسپاران: "/besparan.png",
// خوارزمی: "/khwarazmi.png",
// "فراورش 1": "/faravash1.png",
// "فراورش 2": "/faravash2.png",
// کیمیا: "/kimia.png",
// "آب نیرو": "/abniro.png",
// };
//پتروشیمی آپادانا
// companies={companyChartData.map((item) => {
// const imageMap: Record<string, string> = {
// "واحد 100": "/abniro.png" ,
// "واحد 200": "/besparan.png" ,
// "واحد 300": "/khwarazmi.png" ,
// "واحد 400": "/faravash1.png"
// };
//پتروشیمی نوری
companies={companyChartData.map((item) => {
companies={
companyChartData.map((item) => {
const imageMap: Record<string, string> = {
"LLTE&HLTE": "/besparan.png",
BTX: "/khwarazmi.png",
Utility: "/faravash1.png",
Reforming: "/faravash2.png",
"Storage Tank": "/kimia.png",
PX: "/abniro.png",
"بسپاران": "/besparan.png",
"خوارزمی": "/khwarazmi.png",
"فراورش 1": "/faravash1.png",
"فراورش 2": "/faravash2.png",
"کیمیا": "/kimia.png",
"آب نیرو": "/abniro.png",
};
return {
id: item.category,
name: item.category,
@ -684,7 +630,8 @@ export function DashboardHome() {
capacity: item?.capacityI || 0,
revenue: item?.revenueI || 0,
};
})}
})
}
/>
</div>
</TabsContent>
@ -699,10 +646,17 @@ export function DashboardHome() {
<CardTitle className="text-white text-sm min-w-[100px]">
شدت فناوری
</CardTitle>
<p className="text-base text-left">
%
{formatNumber(
Math.round(
dashboardData.leftData?.technology_intensity || 0,
),
)}
</p>
<Progress
value={parseFloat(
dashboardData.leftData?.technology_intensity
dashboardData.leftData?.technology_intensity || "0",
)}
className="h-4 flex-1"
/>
@ -720,21 +674,21 @@ export function DashboardHome() {
{
label: "اجرا شده",
value: parseFloat(
dashboardData?.leftData?.executed_project || "0"
dashboardData?.leftData?.executed_project || "0",
),
color: "bg-pr-green",
},
{
label: "در حال اجرا",
value: parseFloat(
dashboardData?.leftData?.in_progress_project || "0"
dashboardData?.leftData?.in_progress_project || "0",
),
color: "bg-pr-blue",
},
{
label: "برنامه‌ریزی شده",
value: parseFloat(
dashboardData?.leftData?.planned_project || "0"
dashboardData?.leftData?.planned_project || "0",
),
color: "bg-pr-red",
},
@ -759,7 +713,7 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.printed_books_count || "0"
dashboardData.leftData?.printed_books_count || "0",
)}
</span>
</div>
@ -770,7 +724,7 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.registered_patents_count || "0"
dashboardData.leftData?.registered_patents_count || "0",
)}
</span>
</div>
@ -781,18 +735,18 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.published_reports_count || "0"
dashboardData.leftData?.published_reports_count || "0",
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-pr-green" />
<Book className="w-4 h-4 text-green-400" />
<span className="text-sm">مقاله:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.printed_articles_count || "0"
dashboardData.leftData?.printed_articles_count || "0",
)}
</span>
</div>
@ -816,7 +770,7 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_conferences_count || "0"
dashboardData.leftData?.attended_conferences_count || "0",
)}
</span>
</div>
@ -827,7 +781,7 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_events_count || "0"
dashboardData.leftData?.attended_events_count || "0",
)}
</span>
</div>
@ -838,18 +792,18 @@ export function DashboardHome() {
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_exhibitions_count || "0"
dashboardData.leftData?.attended_exhibitions_count || "0",
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-pr-green" />
<Book className="w-4 h-4 text-green-400" />
<span className="text-sm">برگزاری رویداد:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.organized_events_count || "0"
dashboardData.leftData?.organized_events_count || "0",
)}
</span>
</div>
@ -859,6 +813,7 @@ export function DashboardHome() {
</div>
</div>
</DashboardLayout>
);
}

View File

@ -1,403 +1,52 @@
import { saveAs } from "file-saver";
import jalaali from "jalaali-js";
import {
Calendar,
ChevronLeft,
FileChartColumnIncreasing,
Menu,
PanelLeft,
Server,
User,
} from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import XLSX from "xlsx-js-style";
import { Button } from "~/components/ui/button";
import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
import { useEffect, useState } from "react";
import { useAuth } from "~/contexts/auth-context";
import { Link } from "react-router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import {
PanelLeft,
Settings,
User,
Menu,
ChevronDown,
Server,
} from "lucide-react";
import apiService from "~/lib/api";
import { cn, EventBus, handleDataValue } from "~/lib/utils";
interface HeaderProps {
onToggleSidebar?: () => void;
className?: string;
title?: string;
titleIcon?: React.ComponentType<{ className?: string }> | null;
}
interface MonthItem {
id: string;
label: string;
start: string;
end: string;
}
interface CurrentDay {
start?: string;
end?: string;
sinceMonth?: string;
fromMonth?: string;
}
interface SelectedDate {
since?: number;
until?: number;
}
const monthList: Array<MonthItem> = [
{
id: "month-1",
label: "بهار",
start: "01/01",
end: "03/31",
},
{
id: "month-2",
label: "تابستان",
start: "04/01",
end: "06/31",
},
{
id: "month-3",
label: "پاییز",
start: "07/01",
end: "09/31",
},
{
id: "month-4",
label: "زمستان",
start: "10/01",
end: "12/30",
},
];
const columns: Array<any> = [
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
{
key: "importance_project",
label: "میزان اهمیت",
sortable: true,
width: "160px",
},
{
key: "strategic_theme",
label: "مضمون راهبردی",
sortable: true,
width: "200px",
},
{
key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری",
sortable: true,
width: "220px",
},
{
key: "type_of_innovation",
label: "انواع نوآوری",
sortable: true,
width: "160px",
},
{
key: "innovation",
label: "میزان نوآوری",
sortable: true,
width: "140px",
},
{
key: "person_executing",
label: "مسئول اجرا",
sortable: true,
width: "180px",
},
{
key: "excellent_observer",
label: "ناطر عالی",
sortable: true,
width: "180px",
},
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
{ key: "moderator", label: "مجری", sortable: true, width: "180px" },
{
key: "executive_phase",
label: "فاز اجرایی",
sortable: true,
width: "160px",
},
{
key: "start_date",
label: "تاریخ شروع",
sortable: true,
width: "120px",
},
{
key: "remaining_time",
label: "زمان باقی مانده",
sortable: true,
width: "140px",
computed: true,
},
{
key: "end_date",
label: "تاریخ پایان (برنامه‌ریزی)",
sortable: true,
width: "160px",
},
{
key: "renewed_duration",
label: "مدت زمان تمدید",
sortable: true,
width: "140px",
},
{
key: "done_date",
label: "تاریخ پایان (واقعی)",
sortable: true,
width: "160px",
},
{
key: "deviation_from_program",
label: "متوسط انحراف برنامه‌ای",
sortable: true,
width: "160px",
},
{
key: "approved_budget",
label: "بودجه مصوب",
sortable: true,
width: "150px",
},
{
key: "budget_spent",
label: "بودجه صرف شده",
sortable: true,
width: "150px",
},
{
key: "cost_deviation",
label: "متوسط انحراف هزینه‌ای",
sortable: true,
width: "160px",
},
];
export function Header({
onToggleSidebar,
className,
title = "صفحه اول",
titleIcon,
}: HeaderProps) {
const { user } = useAuth();
const { jy } = jalaali.toJalaali(new Date());
const calendarRef = useRef<HTMLDivElement>(null);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
const [excelLoading, setExcelLoading] = useState<boolean>(false);
const location = useLocation();
const projectManagerRoute = "/dashboard/project-management";
const [currentYear, setCurrentYear] = useState<SelectedDate>({
since: jy,
until: jy,
});
const [selectedDate, setSelectedDate] = useState<CurrentDay>({});
useEffect(() => {
const storedDate = localStorage.getItem("dateSelected");
if (storedDate) {
const parsedDate = JSON.parse(storedDate);
setSelectedDate(parsedDate);
const sinceYear = parsedDate.start
? parseInt(parsedDate.start.split("/")[0], 10)
: jy;
const untilYear = parsedDate.end
? parseInt(parsedDate.end.split("/")[0], 10)
: jy;
setCurrentYear({ since: sinceYear, until: untilYear });
} else {
const defaultDate = {
sinceMonth: "بهار",
fromMonth: "زمستان",
start: `${jy}/01/01`,
end: `${jy}/12/30`,
};
setSelectedDate(defaultDate);
localStorage.setItem("dateSelected", JSON.stringify(defaultDate));
setCurrentYear({ since: jy, until: jy });
}
}, []);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
const redirectHandler = async () => {
try {
const getData = await apiService.post("/GenerateSsoCode");
//بندر امام
const getData = await apiService.post('/GenerateSsoCode')
const url = `http://localhost:3000/redirect/${getData.data}`;
// const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
//آپادانا
// const url = `https://APADANA-IATM-bpms.pelekan.org/redirect/${getData.data}`;
//نوری
const url = `https://NOPC-IATM-bpms.pelekan.org/redirect/${getData.data}`;
window.open(url, "_blank");
} catch (error) {
console.log(error);
}
};
const changeSinceYear = (delta: number) => {
if (!currentYear) return;
const newSince = (currentYear.since ?? 0) + delta;
if (newSince > (currentYear.until ?? Infinity) || newSince < 0) return;
const updatedYear = { ...currentYear, since: newSince };
setCurrentYear(updatedYear);
const updatedDate = {
...selectedDate,
start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`,
};
setSelectedDate(updatedDate);
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
EventBus.emit("dateSelected", updatedDate);
};
const nextFromYearHandler = () => changeSinceYear(1);
const prevFromYearHandler = () => changeSinceYear(-1);
const selectFromDateHandler = (val: MonthItem) => {
const data = {
...selectedDate,
start: `${currentYear.since}/${val.start}`,
sinceMonth: val.label,
};
setSelectedDate(data);
localStorage.setItem("dateSelected", JSON.stringify(data));
EventBus.emit("dateSelected", data);
};
const changeUntilYear = (delta: number) => {
if (!currentYear) return;
const newUntil = (currentYear.until ?? 0) + delta;
if (newUntil < (currentYear.since ?? 0)) return;
const updatedYear = { ...currentYear, until: newUntil };
setCurrentYear(updatedYear);
const updatedDate = {
...selectedDate,
end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`,
};
setSelectedDate(updatedDate);
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
EventBus.emit("dateSelected", updatedDate);
};
const nextUntilYearHandler = () => changeUntilYear(1);
const prevUntilYearHandler = () => changeUntilYear(-1);
const selectUntilDateHandler = (val: MonthItem) => {
const data = {
...selectedDate,
end: `${currentYear.until}/${val.end}`,
fromMonth: val.label,
};
setSelectedDate(data);
localStorage.setItem("dateSelected", JSON.stringify(data));
EventBus.emit("dateSelected", data);
toggleCalendar();
};
const toggleCalendar = () => {
setOpenCalendar(!openCalendar);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
calendarRef.current &&
!calendarRef.current.contains(event.target as Node)
) {
setOpenCalendar(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const exportToExcel = async () => {
let arr = [];
const data: any = await fetchExcelData();
for (let i = 0; i < data.length; i++) {
let obj: Record<string, any> = {};
const project = data[i];
Object.entries(project).forEach(([pKey, pValue]: [any, any]) => {
Object.values(columns).forEach((col) => {
if (pKey === col?.key) {
``;
obj[col?.label] = handleDataValue(
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
);
}
});
});
arr.push(obj);
}
const worksheet = XLSX.utils.json_to_sheet(arr);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, "reports.xls");
};
const fetchExcelData = async () => {
setExcelLoading(true);
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const response = await apiService.select({
ProcessName: "project",
OutputFields: outputFields,
Conditions: [
["start_date", ">=", selectedDate?.start || null, "and"],
["start_date", "<=", selectedDate?.end || null],
],
});
const parsedData = JSON.parse(response.data);
setExcelLoading(false);
return parsedData;
};
const handleDownloadFile = () => {
if (excelLoading) return null;
else exportToExcel();
};
return (
<header
className={cn(
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
className
className,
)}
>
{/* Left Section */}
@ -416,79 +65,8 @@ export function Header({
{/* Page Title */}
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
{/* Right-side icon for current page */}
{titleIcon ? (
<div className="flex items-center gap-2 mr-4">
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
</div>
) : (
<PanelLeft />
)}
{title.includes("-") ? (
<div className="flex row items-center gap-4">
<div className="flex items-center gap-1">
{title.split("-")[0]}
<ChevronLeft className="inline-block w-4 h-4" />
{title.split("-")[1]}
</div>
</div>
) : (
title
)}
<PanelLeft /> {title}
</h1>
<div ref={calendarRef} className="flex flex-col gap-3 relative">
<div
onClick={toggleCalendar}
className="flex flex-row w-full gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-64 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
>
<Calendar size={20} />
{selectedDate ? (
<div className="flex flex-row justify-between w-full min-w-36 font-bold gap-1">
<div className="flex flex-row gap-1.5 w-max">
<span className="text-md">از</span>
<span className="text-md">{selectedDate?.sinceMonth}</span>
<span className="text-md">
{handleDataValue(currentYear.since)}
</span>
</div>
<div className="flex flex-row gap-1.5 w-max">
<span className="text-md">تا</span>
<span className="text-md">{selectedDate?.fromMonth}</span>
<span className="text-md">
{handleDataValue(currentYear.until)}
</span>
</div>
</div>
) : (
"تاریخ مورد نظر خود را انتخاب نمایید"
)}
</div>
{openCalendar && (
<div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
<CustomCalendar
title="از"
nextYearHandler={prevFromYearHandler}
prevYearHandler={nextFromYearHandler}
currentYear={handleDataValue(currentYear?.since)}
monthList={monthList}
selectedDate={selectedDate?.sinceMonth}
selectDateHandler={selectFromDateHandler}
/>
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
<CustomCalendar
title="تا"
nextYearHandler={prevUntilYearHandler}
prevYearHandler={nextUntilYearHandler}
currentYear={handleDataValue(currentYear?.until)}
monthList={monthList}
selectedDate={selectedDate?.fromMonth}
selectDateHandler={selectUntilDateHandler}
/>
</div>
)}
</div>
</div>
{/* Right Section */}
@ -496,29 +74,14 @@ export function Header({
{/* User Menu */}
<div className="relative">
<div className="flex items-center gap-2">
{location.pathname === projectManagerRoute ? (
<div className="flex justify-end w-full mb-0 pl-2">
<span
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian ${excelLoading ? "!cursor-not-allowed !opacity-10" : ""}`}
onClick={handleDownloadFile}
>
<FileChartColumnIncreasing className="h-4 w-4" />
دانلود فایل اکسل
</span>
</div>
) : (
""
)}
{user?.id === 2041 && (
<button
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={redirectHandler}
>
{
user?.id === 2041 && <button
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={redirectHandler}>
<Server className="h-4 w-4" />
ورود به میزکار مدیریت
</button>
)}
ورود به میزکار مدیریت</button>
}
<Button
variant="ghost"
@ -526,6 +89,9 @@ export function Header({
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
className="flex items-center gap-2 text-gray-300"
>
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-full flex items-center justify-center">
<User className="h-4 w-4" />
</div>
<div className="hidden sm:block text-right">
<div className="text-sm font-medium font-persian">
{user?.name} {user?.family}
@ -534,12 +100,9 @@ export function Header({
{user?.username}
</div>
</div>
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
<User className="h-4 w-4" />
</div>
<ChevronDown className="h-3 w-3" />
</Button>
</div>
{/* Profile Dropdown */}
{isProfileMenuOpen && (
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
@ -551,7 +114,7 @@ export function Header({
{user?.email}
</div>
</div>
{/* <div className="py-1">
<div className="py-1">
<Link
to="/dashboard/profile"
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
@ -568,7 +131,7 @@ export function Header({
<Settings className="h-4 w-4" />
تنظیمات
</Link>
</div> */}
</div>
</div>
)}
</div>

View File

@ -27,22 +27,19 @@ const chartConfig = {
},
revenue: {
label: "افزایش درآمد",
color: "#4ADE80",
color: "#4ADE80", // Green-400
},
cost: {
// آپادانا بندرامام
//label: "کاهش هزینه",
label: "هزینه عملیاتی", // نوری
color: "#F87171",
label: "کاهش هزینه",
color: "#F87171", // Red-400
},
} satisfies ChartConfig;
export function InteractiveBarChart({
data,
showRevenue = "نوری"!="نوری", // آپادانا بندرامام
}: {
data: CompanyChartDatum[];
showRevenue?: boolean;
}) {
return (
<Card className="py-0 bg-transparent mt-8 border-none h-full">
@ -69,67 +66,31 @@ export function InteractiveBarChart({
axisLine={false}
tickMargin={25}
style={{ fill: "#ACACAC", fontSize: 11 }}
tickFormatter={(value) =>
`${formatNumber(Math.round(value))}%`
}
tickFormatter={(value) => `${formatNumber(Math.round(value))}%`}
/>
<Bar
dataKey="capacity"
fill={chartConfig.capacity.color}
radius={[8, 8, 0, 0]}
>
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
<LabelList
dataKey="capacity"
position="top"
offset={15}
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) =>
`${formatNumber(Math.round(v))}%`
}
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
{showRevenue && (
<Bar
dataKey="revenue"
fill={chartConfig.revenue.color}
radius={[8, 8, 0, 0]}
>
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
<LabelList
dataKey="revenue"
position="top"
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) =>
`${formatNumber(Math.round(v))}%`
}
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
)}
<Bar
dataKey="cost"
fill={chartConfig.cost.color}
radius={[8, 8, 0, 0]}
>
<Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
<LabelList
dataKey="cost"
position="top"
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) =>
`${formatNumber(Math.round(v))}%`
}
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
</BarChart>
@ -142,32 +103,23 @@ export function InteractiveBarChart({
className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.capacity.color }}
></div>
<span className="text-xs text-white">
{chartConfig.capacity.label}
</span>
<span className="text-xs text-white">{chartConfig.capacity.label}</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.cost.color }}
></div>
<span className="text-xs text-white">
{chartConfig.cost.label}
</span>
<span className="text-xs text-white">{chartConfig.cost.label}</span>
</div>
{showRevenue && (
<div className="flex items-center gap-2">
<div
className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.revenue.color }}
></div>
<span className="text-xs text-white">
{chartConfig.revenue.label}
</span>
<span className="text-xs text-white">{chartConfig.revenue.label}</span>
</div>
)}
</div>
</CardContent>
</Card>

View File

@ -1,8 +1,9 @@
import { useState } from "react";
import { cn } from "~/lib/utils";
import { Header } from "./header";
import { Sidebar } from "./sidebar";
import { Header } from "./header";
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
import apiService from "~/lib/api";
interface DashboardLayoutProps {
children: React.ReactNode;
@ -17,14 +18,7 @@ export function DashboardLayout({
}: DashboardLayoutProps) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] =
useState(false);
const [currentTitle, setCurrentTitle] = useState<string | undefined>(
title ?? "صفحه اول"
);
const [currentTitleIcon, setCurrentTitleIcon] = useState<
React.ComponentType<{ className?: string }> | null | undefined
>(undefined);
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(!isSidebarCollapsed);
@ -34,6 +28,8 @@ export function DashboardLayout({
setIsMobileSidebarOpen(!isMobileSidebarOpen);
};
return (
<div
className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden"
@ -57,20 +53,15 @@ export function DashboardLayout({
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
isMobileSidebarOpen
? "translate-x-0"
: "translate-x-full lg:translate-x-0"
: "translate-x-full lg:translate-x-0",
)}
>
<Sidebar
isCollapsed={isSidebarCollapsed}
onToggleCollapse={toggleSidebarCollapse}
className="h-full flex-shrink-0 relative z-10"
onStrategicAlignmentClick={() =>
setIsStrategicAlignmentPopupOpen(true)
}
onTitleChange={(info) => {
setCurrentTitle(info.title);
setCurrentTitleIcon(info.icon ?? null);
}}
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
/>
</div>
@ -80,26 +71,22 @@ export function DashboardLayout({
<Header
onToggleSidebar={toggleMobileSidebar}
className="flex-shrink-0"
title={currentTitle}
titleIcon={currentTitleIcon}
title={title}
/>
{/* Main content */}
<main
className={cn(
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
className
className,
)}
>
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden p-5">
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden">
{children}
</div>
</main>
</div>
<StrategicAlignmentPopup
open={isStrategicAlignmentPopupOpen}
onOpenChange={setIsStrategicAlignmentPopupOpen}
/>
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
</div>
);
}

View File

@ -12,10 +12,10 @@ import {
Zap,
} from "lucide-react";
import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import { formatNumber } from "~/lib/utils";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
@ -34,10 +34,8 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { formatCurrency } from "~/lib/utils";
import { DashboardLayout } from "../layout";
moment.loadPersian({ usePersianDigits: true });
@ -155,7 +153,7 @@ export function DigitalInnovationPage() {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true);
const [date, setDate] = useStoredDate();
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
const [rating, setRating] = useState<ListItem[]>([]);
@ -183,8 +181,6 @@ export function DigitalInnovationPage() {
// const [avarage, setAvarage] = useState<number>(0);
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Selection handlers
const handleSelectAll = () => {
@ -215,7 +211,7 @@ export function DigitalInnovationPage() {
value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts),
description: "میلیون ریال کاهش یافته",
icon: <TrendingDown />,
color: "text-pr-green",
color: "text-emerald-400",
},
{
id: "bottleneck-removal",
@ -223,7 +219,7 @@ export function DigitalInnovationPage() {
value: formatNumber(stats.increasedRevenue),
description: "میلیون ریال افزایش یافته",
icon: <TrendingUp />,
color: "text-pr-green",
color: "text-emerald-400",
},
{
@ -234,7 +230,7 @@ export function DigitalInnovationPage() {
),
description: "هزار تن صرفه جوریی شده",
icon: <Database />,
color: "text-pr-green",
color: "text-emerald-400",
},
{
id: "frequent-failures-reduction",
@ -245,7 +241,7 @@ export function DigitalInnovationPage() {
),
description: "مگاوات کاهش یافته",
icon: <Zap />,
color: "text-pr-green",
color: "text-emerald-400",
},
];
@ -284,11 +280,7 @@ export function DigitalInnovationPage() {
"reduce_costs_percent",
],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
@ -301,16 +293,16 @@ export function DigitalInnovationPage() {
if (reset) {
setProjects(parsedData);
// calculateAverage(parsedData);
// setTotalCount(parsedData.length);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
// setTotalCount((prev) => prev + parsedData.length);
setTotalCount((prev) => prev + parsedData.length);
}
setHasMore(parsedData.length === pageSize);
} else {
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -318,14 +310,14 @@ export function DigitalInnovationPage() {
console.error("Error parsing project data:", parseError);
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
} else {
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -333,7 +325,7 @@ export function DigitalInnovationPage() {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -342,7 +334,7 @@ export function DigitalInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
} finally {
@ -354,75 +346,45 @@ export function DigitalInnovationPage() {
};
const loadMore = useCallback(() => {
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading, loadingMore]);
}, [loadingMore, hasMore, loading]);
useEffect(() => {
if (date?.start && date?.end) {
fetchTable(true);
fetchTotalCount();
fetchStats();
}
}, [sortConfig, date]);
}, [sortConfig]);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
useEffect(() => {
if (currentPage > 1 && date?.start && date?.end) {
if (currentPage > 1) {
fetchTable(false);
}
}, [currentPage]);
// Infinite scroll observer with debouncing
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
return;
if (!scrollContainer || !hasMore || loadingMore) return;
// Clear previous timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// Debounce scroll events
scrollTimeoutRef.current = setTimeout(() => {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 95% of the container
if (scrollPercentage >= 0.95) {
if (scrollPercentage >= 0.9) {
loadMore();
}
}, 150);
};
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, {
passive: true,
});
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
}
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [loadMore, hasMore, loadingMore]);
@ -433,23 +395,19 @@ export function DigitalInnovationPage() {
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
fetchTotalCount(date?.start, date?.end);
fetchTotalCount();
fetchStats();
setCurrentPage(1);
setProjects([]);
setHasMore(true);
};
const fetchTotalCount = async (startDate?: string, endDate?: string) => {
const fetchTotalCount = async () => {
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
});
if (response.state === 0) {
@ -476,12 +434,11 @@ export function DigitalInnovationPage() {
try {
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_digital_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
innovation_digital_function: {},
});
// let payload: DigitalInnovationMetrics = raw?.data;
// console.log("*-*-*-*" +payload);
// if (typeof payload === "string") {
@ -510,6 +467,8 @@ export function DigitalInnovationPage() {
}
}
const parseNum = (v: unknown): number => {
if (v == null) return 0;
if (typeof v === "number") return v;
@ -557,33 +516,33 @@ export function DigitalInnovationPage() {
// fetchStats();
// };
// const renderProgress = useMemo(() => {
// const total = 10;
// for (let i = 0; i < rating.length; i++) {
// const currentElm = rating[i];
// currentElm.house = [];
// const greenBoxes = Math.floor((total * currentElm.development) / 100);
// const partialPercent =
// (total * currentElm.development) / 100 - greenBoxes;
// for (let j = 0; j < greenBoxes; j++) {
// currentElm.house.push({
// index: j,
// color: "!bg-emerald-400",
// });
// }
// if (partialPercent != 0 && greenBoxes != 10)
// currentElm.house.push({
// index: greenBoxes + 1,
// style: `linear-gradient(
// to right,
// oklch(76.5% 0.177 163.223) 0%,
// oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
// oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
// oklch(55.1% 0.027 264.364) 100%
// )`,
// });
// }
// }, [rating]);
const renderProgress = useMemo(() => {
const total = 10;
for (let i = 0; i < rating.length; i++) {
const currentElm = rating[i];
currentElm.house = [];
const greenBoxes = Math.floor((total * currentElm.development) / 100);
const partialPercent =
(total * currentElm.development) / 100 - greenBoxes;
for (let j = 0; j < greenBoxes; j++) {
currentElm.house.push({
index: j,
color: "!bg-emerald-400",
});
}
if (partialPercent != 0 && greenBoxes != 10)
currentElm.house.push({
index: greenBoxes + 1,
style: `linear-gradient(
to right,
oklch(76.5% 0.177 163.223) 0%,
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
oklch(55.1% 0.027 264.364) 100%
)`,
});
}
}, [rating]);
const statusColor = (status: projectStatus): any => {
let el = null;
@ -627,14 +586,14 @@ export function DigitalInnovationPage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
>
جزئیات بیشتر
</Button>
);
case "amount_currency_reduction":
return (
<span className="font-medium text-pr-green">
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
@ -645,9 +604,7 @@ export function DigitalInnovationPage() {
</Badge>
);
case "title":
return (
<span className="font-light text-sm text-white">{String(value)}</span>
);
return <span className="font-medium text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center gap-1">
@ -682,7 +639,7 @@ export function DigitalInnovationPage() {
return (
<DashboardLayout title="نوآوری دیجیتال">
<div className="space-y-4 grid justify-between gap-7 pl-6 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
<div className="p-6 space-y-4 grid justify-between gap-8 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
{/* Stats Cards */}
<div className="flex flex-col gap-6 w-full mb-0">
<div className="space-y-6 w-full">
@ -755,49 +712,50 @@ export function DigitalInnovationPage() {
</div>
{/* Process Impacts Chart */}
<BaseCard className="rounded-xl w-full overflow-hidden">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg w-full overflow-hidden h-full ">
{/* <CardContent > */}
<CustomBarChart
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
loading={statsLoading}
// height="100%"
height="100%"
data={[
{
label: DigitalCardLabel.decreasCost,
value: stats.reduceCostsPercent || 0,
color: "bg-pr-green",
color: "bg-emerald-400",
labelColor: "text-white",
},
{
label: DigitalCardLabel.increaseRevenue,
value: stats.increasedRevenuePercent || 0,
color: "bg-pr-green",
color: "bg-emerald-400",
labelColor: "text-white",
},
{
label: DigitalCardLabel.performance,
value: stats.resourceProductivityPercent || 0,
color: "bg-pr-green",
color: "bg-emerald-400",
labelColor: "text-white",
},
{
label: DigitalCardLabel.decreaseEnergy,
value: stats.reduceEnergyConsumptionPercent || 0,
color: "bg-pr-green",
color: "bg-emerald-400",
labelColor: "text-white",
},
]}
barHeight="h-5"
showAxisLabels={true}
/>
</BaseCard>
{/* </CardContent> */}
</Card>
</div>
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-[39.7rem]">
<CardContent className="p-0">
<div className="relative h-full">
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[calc(100vh-160px)] ">
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[36.8rem] ">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
@ -976,13 +934,13 @@ export function DigitalInnovationPage() {
{/* Project Details Dialog */}
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[80vh] overflow-y-auto">
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="body grid grid-cols-[40%_20%_40%] pb-6">
<div className="body grid grid-cols-[40%_20%_40%]">
<div className="border-l-2 border-l-gray-600 px-6">
<span className="title text-lg font-bold">
{dialogInfo?.title}
@ -1034,7 +992,7 @@ export function DigitalInnovationPage() {
</div>
<div className="digitalAbilityDevelopment flex flex-col gap-10 border-l-2 border-l-gray-600 px-5">
<div className="flex flex-col gap-4">
<span className="text-lg font-bold">
<span className="text-md font-bold">
توسعه قابلیت های دیجیتال:{" "}
</span>
<div className="flex flex-col gap-2">
@ -1097,10 +1055,10 @@ export function DigitalInnovationPage() {
</div>
</div>
</div>
<div className="flex flex-col px-6 gap-4">
<div className="flex flex-col pr-7 gap-4">
<div className="costBoard mx-auto w-full">
<div className="board o border border-gray-600 rounded-xl overflow-hidden flex flex-col">
<span className="text-sm bg-[#3F415A] text-white w-full p-2.5 pr-4 ">
<span className="title bg-[#3F415A] text-white w-full p-2.5 pr-4 ">
کاهش هزینه ها
</span>

View File

@ -1,4 +1,6 @@
// import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import { formatNumber } from "~/lib/utils";
import {
Bar,
BarChart,
@ -25,7 +27,6 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { EventBus, formatNumber } from "~/lib/utils";
import {
Building2,
@ -42,17 +43,12 @@ import {
UsersIcon,
Zap,
} from "lucide-react";
import moment from "moment-jalaali";
import toast from "react-hot-toast";
import { MetricCard } from "~/components/ui/metric-card";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout";
moment.loadPersian({ usePersianDigits: true });
// moment.loadPersian({ usePersianDigits: true });
interface GreenInnovationData {
WorkflowID: string;
approved_budget: string;
@ -170,8 +166,6 @@ export function GreenInnovationPage() {
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
const [date, setDate] = useStoredDate();
const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
@ -294,11 +288,7 @@ export function GreenInnovationPage() {
"observer",
],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [
["type_of_innovation", "=", "نوآوری سبز", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
if (response.state === 0) {
@ -360,34 +350,20 @@ export function GreenInnovationPage() {
}
};
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
const loadMore = useCallback(() => {
if (hasMore && !loading) {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
}, [loadingMore, hasMore, loading]);
useEffect(() => {
if (date.end && date.start) {
fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]);
}, [sortConfig]);
useEffect(() => {
if (date.end && date.start) fetchStats();
}, [selectedProjects, date]);
fetchStats();
}, [selectedProjects]);
useEffect(() => {
if (currentPage > 1) {
@ -399,12 +375,12 @@ export function GreenInnovationPage() {
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore) return;
if (!scrollContainer || !hasMore || loadingMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
if (scrollPercentage == 1) {
if (scrollPercentage >= 0.9) {
loadMore();
}
};
@ -440,11 +416,7 @@ export function GreenInnovationPage() {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [
["type_of_innovation", "=", "نوآوری سبز", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
});
if (response.state === 0) {
const dataString = response.data;
@ -476,8 +448,6 @@ export function GreenInnovationPage() {
selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
start_date: date?.start || null,
end_date: date?.end || null,
},
});
let payload: any = raw?.data;
@ -524,13 +494,13 @@ export function GreenInnovationPage() {
},
pollution: {
value: parseNum(stats.pollution_reduction),
percent: parseNum(stats.pollution_reduction_percent),
value: formatNumber(parseNum(stats.pollution_reduction)),
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
},
waste: {
value: parseNum(stats.waste_reduction),
percent: parseNum(stats.waste_reductionn_percent),
value: formatNumber(parseNum(stats.waste_reduction)),
percent: formatNumber(parseNum(stats.waste_reductionn_percent)),
},
avarage: stats.average_project_score,
countInnovationGreenProjects: stats.count_innovation_green_projects,
@ -548,6 +518,7 @@ export function GreenInnovationPage() {
setStatsLoading(false);
}
};
const setPageData = (normalized: any) => {
setSustainabilityStats((prev) => ({
...prev,
@ -631,14 +602,14 @@ export function GreenInnovationPage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
>
جزئیات بیشتر
</Button>
);
case "amount_currency_reduction":
return (
<span className="font-medium text-pr-green">
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
@ -649,9 +620,7 @@ export function GreenInnovationPage() {
</Badge>
);
case "title":
return (
<span className="font-light text-sm text-white">{String(value)}</span>
);
return <span className="font-medium text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center gap-1">
@ -717,7 +686,7 @@ export function GreenInnovationPage() {
return (
<DashboardLayout title="نوآوری سبز">
<div className="space-y-4 h-[23.5rem]">
<div className="p-6 space-y-4 h-[23.5rem]">
{/* Stats Cards */}
<div className="flex gap-6 mb-5 md:flex-col xl:flex-row">
<div className="flex flex-col justify-between xl:w-1/2 sm:w-full sm:gap-2">
@ -751,14 +720,39 @@ export function GreenInnovationPage() {
</Card>
))
: Object.entries(sustainabilityStats).map(([key, value]) => (
<MetricCard
<Card
key={key}
title={value.title}
value={Math.round(value.total.value || 0)}
valueLabel={value.total?.description}
percentValue={value.percent?.value || 0}
percentLabel={value.percent?.description}
/>
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
>
<CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
<h3 className="text-lg font-bold text-white font-persian p-4">
{value.title}
</h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
@ -819,10 +813,7 @@ export function GreenInnovationPage() {
<div className="params flex flex-col gap-3.5">
{Object.entries(recycleParams).map((el, index) => {
return (
<div
key={index}
className="param flex flex-row justify-between items-center"
>
<div className="param flex flex-row justify-between items-center">
<div className="flex flex-row gap-2">
{el[1].icon}
<span className="font-normal text-sm font-persian">
@ -904,7 +895,7 @@ export function GreenInnovationPage() {
</Card>
)}
<Card className="w-1/2 bg-pr-gray backdrop-blur-sm rounded-lg overflow-hidden">
<Card className="w-1/3 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden">
<CardContent className="p-0">
<div className="border-b-2 border-gray-500/20">
<div className="flex flex-row justify-between w-full p-4">
@ -955,7 +946,7 @@ export function GreenInnovationPage() {
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar h-full">
<Table containerClassName="overflow-auto custom-scrollbar h-[25rem]">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
@ -1119,7 +1110,7 @@ export function GreenInnovationPage() {
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="space-y-4 flex justify-between text-right p-6">
<div className="space-y-4 flex justify-between text-right px-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
@ -1165,7 +1156,7 @@ export function GreenInnovationPage() {
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<UsersIcon className="h-4 text-green-500" />
هزینه برآورد شده(میلیون ریال):
هزینه برآورد شده:
</h4>
<span className="text-white font-bold font-persian">
{formatNumber(

View File

@ -39,11 +39,8 @@ import {
ResponsiveContainer,
XAxis,
} from "recharts";
import { MetricCard } from "~/components/ui/metric-card";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { formatCurrency, formatNumber } from "~/lib/utils";
import DashboardLayout from "../layout";
interface innovationBuiltInDate {
@ -155,8 +152,8 @@ enum projectStatus {
const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "400px" },
{
key: "project_status",
label: "وضعیت پروژه",
@ -167,7 +164,7 @@ const columns = [
key: "project_rating",
label: "امتیاز پروژه",
sortable: true,
width: "120px",
width: "140px",
},
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
];
@ -194,8 +191,6 @@ export function InnovationBuiltInsidePage() {
field: "start_date",
direction: "asc",
});
const [date, setDate] = useStoredDate();
const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] =
useState<Set<string | number>>();
@ -315,11 +310,7 @@ export function InnovationBuiltInsidePage() {
"technology_maturity_level",
],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [
["type_of_innovation", "=", "نوآوری ساخت داخل", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
if (response.state === 0) {
@ -420,30 +411,18 @@ export function InnovationBuiltInsidePage() {
};
const loadMore = useCallback(() => {
if (hasMore && !loading) {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
}, [loadingMore, hasMore, loading]);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
fetchProjects(true);
}, [sortConfig]);
useEffect(() => {
if (date.start && date.end) fetchProjects(true);
}, [sortConfig, date]);
useEffect(() => {
if (date.end && date.start) fetchStats();
}, [selectedProjects, date]);
fetchStats();
}, [selectedProjects]);
useEffect(() => {
if (currentPage > 1) {
@ -455,12 +434,12 @@ export function InnovationBuiltInsidePage() {
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore) return;
if (!scrollContainer || !hasMore || loadingMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
if (scrollPercentage == 1) {
if (scrollPercentage >= 0.9) {
loadMore();
}
};
@ -501,8 +480,6 @@ export function InnovationBuiltInsidePage() {
selectedProjects && selectedProjects?.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
start_date: date?.start || null,
end_date: date?.end || null,
},
});
let payload: any = raw?.data;
@ -528,13 +505,15 @@ export function InnovationBuiltInsidePage() {
const stats = data[0];
const normalized: any = {
currencySaving: {
value: parseNum(stats?.foreign_currency_saving),
percent: parseNum(stats?.foreign_currency_saving_percent),
value: formatNumber(parseNum(stats?.foreign_currency_saving)),
percent: formatNumber(
parseNum(stats?.foreign_currency_saving_percent)
),
},
investmentAmount: {
value: parseNum(stats?.investment_amount),
percent: parseNum(stats?.investment_amount_percent),
value: formatNumber(parseNum(stats?.investment_amount)),
percent: formatNumber(parseNum(stats?.investment_amount_percent)),
},
technology: {
@ -645,14 +624,14 @@ export function InnovationBuiltInsidePage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
className="text-emerald-500 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
>
جزئیات بیشتر
</Button>
);
case "amount_currency_reduction":
return (
<span className="font-medium text-pr-green">
<span className="font-medium text-emerald-500">
{formatCurrency(String(value))}
</span>
);
@ -663,9 +642,7 @@ export function InnovationBuiltInsidePage() {
</Badge>
);
case "title":
return (
<span className="font-light text-sm text-white">{String(value)}</span>
);
return <span className="font-medium text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center gap-1">
@ -724,10 +701,10 @@ export function InnovationBuiltInsidePage() {
return (
<DashboardLayout title="نوآوری ساخت داخل">
<div className="space-y-4 justify-between gap-8 grid pl-6 sm:grid-cols-1 xl:grid-cols-[35%_65%]">
<div className="p-6 space-y-4 justify-between gap-8 grid sm:grid-cols-1 xl:grid-cols-[40%_60%]">
{/* Stats Cards */}
<div className="flex w-full mb-0">
<div className="flex flex-col w-full justify-between gap-2">
<div className="flex gap-6 w-full mb-0">
<div className="flex flex-col justify-between w-full gap-6">
{statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 2 }).map((_, index) => (
@ -758,47 +735,39 @@ export function InnovationBuiltInsidePage() {
</Card>
))
: Object.entries(sustainabilityStats).map(([key, value]) => (
<MetricCard
<Card
key={key}
title={value.title}
value={Math.round(value.total.value || 0)}
valueLabel={value.total?.description}
percentValue={value.percent?.value || 0}
percentLabel={value.percent?.description}
/>
// <Card
// key={key}
// className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
// >
// <CardContent className="p-0 h-full">
// <div className="flex flex-col justify-between gap-2 h-full">
// <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
// <h3 className="text-lg font-semibold text-white p-4">
// {value.title}
// </h3>
// </div>
// <div className="flex items-center justify-between p-6 flex-row-reverse">
// <div className="flex flex-col">
// <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
// % {value.percent?.value}
// </span>
// <span className="text-sm text-gray-400 font-persian">
// {value.percent?.description}
// </span>
// </div>
// <b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
// <div className="flex flex-col">
// <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
// {value.total?.value}
// </span>
// <span className="text-sm text-gray-400 font-persian">
// {value.total?.description}
// </span>
// </div>
// </div>
// </div>
// </CardContent>
// </Card>
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
>
<CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
<h3 className="text-lg font-semibold text-white p-4">
{value.title}
</h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
{statsLoading ? (
@ -898,7 +867,7 @@ export function InnovationBuiltInsidePage() {
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
<CardContent className="p-0">
<div className="relative ">
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(100vh-160px)]">
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(90vh-15px)]">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
@ -1061,7 +1030,7 @@ export function InnovationBuiltInsidePage() {
{/* Project Details Dialog */}
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
شرح پروژه
@ -1123,6 +1092,7 @@ export function InnovationBuiltInsidePage() {
<div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-white"></span>
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
{" "}
سطح تکنولوژی
</span>
</div>
@ -1286,7 +1256,7 @@ export function InnovationBuiltInsidePage() {
))}
</div>
) : (
<ResponsiveContainer width="100%" height={400}>
<ResponsiveContainer width="100%" height={420}>
<LineChart
data={dialogChartData}
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}

View File

@ -15,9 +15,8 @@ import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { BaseCard } from "~/components/ui/base-card";
import { Checkbox } from "~/components/ui/checkbox";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import {
@ -34,11 +33,10 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
import { Card , CardContent} from "~/components/ui/card";
moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData {
@ -67,11 +65,9 @@ interface ProjectStats {
percent_reduction_value_currency: string;
percent_sum_stopping_production: string;
percent_throat_removal: string;
percent_operating_cost_before_innovation: string;
sum_reducing_breakdowns: number;
sum_reduction_value_currency: number;
sum_stopping_production: number;
sum_operating_cost_reduction: number;
}
interface SortConfig {
@ -96,11 +92,9 @@ interface InnovationStats {
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
percentProductionStops: number | string; // درصد مقایسه‌ای جلوگیری از توقفات تولید
reductionCostOprationSum: number; // مجموع کاهش هزینه عملیاتی
percentBottleneckRemoval: number | string; // درصد مقایسه‌ای رفع گلوگاه
percentCurrencyReduction: number | string; // درصد مقایسه‌ای کاهش ارز بری
percentFailuresReduction: number | string; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار
percentOperatingCostBeforeInnovation: number | string; // درصد مقایسه‌ای کاهش هزینه عملیاتی
}
const columns = [
@ -129,14 +123,13 @@ export function ProcessInnovationPage() {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true);
const [date, setDate] = useStoredDate();
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<InnovationStats>({
totalProjects: 0,
averageScore: 0,
productionStopsPreventionSum: 0,
reductionCostOprationSum: 0,
bottleneckRemovalCount: 0,
currencyReductionSum: 0,
frequentFailuresReductionSum: 0,
@ -144,7 +137,6 @@ export function ProcessInnovationPage() {
percentBottleneckRemoval: 0,
percentCurrencyReduction: 0,
percentFailuresReduction: 0,
percentOperatingCostBeforeInnovation: 0,
});
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
@ -160,60 +152,58 @@ export function ProcessInnovationPage() {
const [stateCard, setStateCard] = useState({
productionstopsprevention: {
id: "productionstopsprevention",
title: "توقفات تولید",
title: "جلوگیری از توقفات تولید",
value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum
),
description: "تن افزایش یافته",
icon: CirclePause,
color: "text-pr-green",
color: "text-emerald-400",
},
bottleneckremoval: {
id: "bottleneckremoval",
title: "گلوگاه ها",
title: "رفع گلوگاه",
value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه",
icon: Funnel,
color: "text-pr-green",
color: "text-emerald-400",
},
currencyreduction: {
id: "currencyreduction",
title: "ارز بری",
title: "کاهش ارز بری",
value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
),
description: "دلار کاهش یافته",
icon: DollarSign,
color: "text-pr-green",
},
decreaseCurrencyOperation: {
id: "decreaseCurrencyOperation",
title: "هزینه های عملیاتی",
value: formatNumber(
stats.reductionCostOprationSum.toFixed?.(0) ??
stats.reductionCostOprationSum
),
description: "میلیون ریال کاهش یافته",
icon: DollarSign,
color: "text-pr-green",
icon: DollarSign ,
color: "text-emerald-400",
},
frequentfailuresreduction: {
id: "frequentfailuresreduction",
title: "خرابی های پرتکرار",
title: "کاهش خرابی های پرتکرار",
value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum
),
description: "خرابی پر تکرار کاهش یافته",
description: "مجموع درصد کاهش خرابی",
icon: Wrench,
color: "text-pr-green",
color: "text-emerald-400",
},
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
// Selection handlers
const handleSelectAll = () => {
if (selectedProjects.size === projects.length) {
setSelectedProjects(new Set());
} else {
setSelectedProjects(new Set(projects.map((p) => p.project_no)));
}
};
const handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) {
@ -266,14 +256,11 @@ export function ProcessInnovationPage() {
"observer",
],
Sorts: [["start_date", "asc"]],
Conditions: [
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
console.log(JSON.parse(response.data));
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
@ -282,16 +269,16 @@ export function ProcessInnovationPage() {
if (Array.isArray(parsedData)) {
if (reset) {
setProjects(parsedData);
// setTotalCount(parsedData.length);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
// setTotalCount((prev) => prev + parsedData.length);
setTotalCount((prev) => prev + parsedData.length);
}
setHasMore(parsedData.length === pageSize);
} else {
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -299,14 +286,14 @@ export function ProcessInnovationPage() {
console.error("Error parsing project data:", parseError);
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
} else {
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -314,7 +301,7 @@ export function ProcessInnovationPage() {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
}
@ -323,7 +310,7 @@ export function ProcessInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
// setTotalCount(0);
setTotalCount(0);
}
setHasMore(false);
} finally {
@ -334,33 +321,19 @@ export function ProcessInnovationPage() {
};
const loadMore = useCallback(() => {
if (hasMore && !loading) {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
}, [loadingMore, hasMore, loading]);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
useEffect(() => {
if (date?.start && date?.end) {
fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]);
}, [sortConfig]);
useEffect(() => {
if (date?.start && date?.end) fetchStats();
}, [selectedProjects, date]);
fetchStats();
}, [selectedProjects]);
useEffect(() => {
if (currentPage > 1) {
@ -372,12 +345,12 @@ export function ProcessInnovationPage() {
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore) return;
if (!scrollContainer || !hasMore || loadingMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
if (scrollPercentage == 1) {
if (scrollPercentage >= 0.9) {
loadMore();
}
};
@ -410,11 +383,7 @@ export function ProcessInnovationPage() {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
});
if (response.state === 0) {
@ -448,8 +417,6 @@ export function ProcessInnovationPage() {
selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
start_date: date?.start || null,
end_date: date?.end || null,
},
});
@ -480,13 +447,10 @@ export function ProcessInnovationPage() {
totalProjects: parseNum(stats?.count_innovation_process_projects),
averageScore: parseFloat(data[0].average_project_score),
productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
reductionCostOprationSum: parseNum(stats?.sum_operating_cost_reduction),
bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
percentProductionStops: stats?.percent_sum_stopping_production,
percentOperatingCostBeforeInnovation:
stats?.percent_operating_cost_before_innovation,
percentBottleneckRemoval: stats?.percent_throat_removal,
percentCurrencyReduction: stats?.percent_reduction_value_currency,
percentFailuresReduction: stats?.percent_reducing_breakdowns,
@ -509,10 +473,6 @@ export function ProcessInnovationPage() {
...prev.currencyreduction,
value: formatNumber(normalized.currencyReductionSum),
},
decreaseCurrencyOperation: {
...prev.decreaseCurrencyOperation,
value: formatNumber(normalized.reductionCostOprationSum),
},
}));
setStats(normalized);
} catch (error) {
@ -565,7 +525,7 @@ export function ProcessInnovationPage() {
<Checkbox
checked={selectedProjects.has(item.project_id)}
onCheckedChange={() => handleSelectProject(item.project_id)}
className="data-[state=checked]:bg-pr-green data-[state=checked]:border-pr-green"
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
);
case "details":
@ -574,14 +534,14 @@ export function ProcessInnovationPage() {
variant="ghost"
size="sm"
onClick={() => handleProjectDetails(item)}
className="text-pr-green underline-offset-4 underline font-normal p-2 h-auto"
className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
);
case "amount_currency_reduction":
return (
<span className="font-medium text-pr-green">
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
@ -592,11 +552,7 @@ export function ProcessInnovationPage() {
</Badge>
);
case "title":
return (
<span className="font-normal text-sm text-white">
{String(value)}
</span>
);
return <span className="font-normal text-sm text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center gap-1">
@ -612,10 +568,7 @@ export function ProcessInnovationPage() {
);
case "project_rating":
return (
<Badge
variant="outline"
className="text-base font-semibold text-center border-none"
>
<Badge variant="outline" className="text-base font-semibold text-center border-none">
{formatNumber(String(value))}
</Badge>
);
@ -634,27 +587,23 @@ export function ProcessInnovationPage() {
return (
<DashboardLayout title="نوآوری در فرآیند">
<div className="flex flex-col gap-4">
<div className="p-6 space-y-4">
{/* Stats Cards */}
<div className="flex gap-4">
<div className="space-y-4 w-full">
<div className="flex gap-6">
<div className="space-y-6 w-full">
{/* Stats Grid */}
<div className="h-full">
{loading || statsLoading ? (
// Skeleton cards
<div className="flex flex-wrap justify-between gap-3">
{Array.from({ length: 6 }).map((_, index) => (
<BaseCard
key={`skeleton-${index}`}
className="rounded-2xl overflow-hidden w-full sm:w-[48%] md:w-[30%]"
>
<div className="grid grid-cols-2 gap-3">
{loading || statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 4 }).map((_, index) => (
<BaseCard key={`skeleton-${index}`} className="rounded-2xl overflow-hidden">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<div
className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }}
/>
<div className="p-3 rounded-full w-fit">
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
</div>
</div>
@ -670,129 +619,50 @@ export function ProcessInnovationPage() {
</div>
</div>
</BaseCard>
))}
</div>
) : (
<div className="flex flex-col h-full gap-5">
<div className="flex flex-row gap-4 h-full">
<BaseCard
key={stateCard.productionstopsprevention.id}
title={stateCard.productionstopsprevention.title}
className="border-gray-700/50 w-full"
icon={stateCard.productionstopsprevention.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.productionstopsprevention.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.productionstopsprevention.description}
</div>
</div>
</div>
</div>
</BaseCard>
))
: Object.entries(stateCard).map(([key, card]) => {
// map percent values for each card key
const percentMap: Record<string, number | string | undefined> = {
productionstopsprevention: stats.percentProductionStops,
bottleneckremoval: stats.percentBottleneckRemoval,
currencyreduction: stats.percentCurrencyReduction,
frequentfailuresreduction: stats.percentFailuresReduction,
};
const percentValue = percentMap[key];
return (
<BaseCard
key={stateCard.frequentfailuresreduction.id}
title={stateCard.frequentfailuresreduction.title}
className="border-gray-700/50 w-full"
icon={stateCard.frequentfailuresreduction.icon}
key={card.id}
title={card.title}
className="border-gray-700/50"
icon={card.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.frequentfailuresreduction.value}
{(card.value)}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.frequentfailuresreduction.description}
{card.description}
</div>
</div>
</div>
</div>
</BaseCard>
</div>
<div className="flex flex-row gap-4 h-full">
<BaseCard
key={stateCard.currencyreduction.id}
title={stateCard.currencyreduction.title}
className="border-gray-700/50 w-full"
icon={stateCard.currencyreduction.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.currencyreduction.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.currencyreduction.description}
</div>
</div>
</div>
</div>
</BaseCard>
<BaseCard
key={stateCard.decreaseCurrencyOperation.id}
title={stateCard.decreaseCurrencyOperation.title}
className="border-gray-700/50 w-full"
icon={stateCard.decreaseCurrencyOperation.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.decreaseCurrencyOperation.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.decreaseCurrencyOperation.description}
</div>
</div>
</div>
</div>
</BaseCard>
<BaseCard
key={stateCard.bottleneckremoval.id}
title={stateCard.bottleneckremoval.title}
className="border-gray-700/50 w-full"
icon={stateCard.bottleneckremoval.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.bottleneckremoval.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.bottleneckremoval.description}
</div>
</div>
</div>
</div>
</BaseCard>
</div>
</div>
)}
);
})}
</div>
</div>
{/* Process Impacts Chart */}
{/* نمودار با الگوریتم Nice Numbers:
مثلاً اگر دادهها [10, 35, 63, 18] باشند:
- حداکثر: 63، با حاشیه 5% = 66.15
- Nice Max: 75 (گرد و خوانا)
- Ticks: [0, 20, 40, 60, 75]
این باعث میشود نمودار زیباتر و خواناتر باشد */}
<BaseCard className="rounded-xl w-full overflow-hidden">
<BaseCard className="rounded-2xl w-full overflow-hidden">
<CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
loading={statsLoading}
data={[
{
label: "توقفات تولید",
label: "کاهش توقفات تولید",
value: Number(stats.percentProductionStops) || 0,
labelColor: "text-white",
},
@ -802,23 +672,17 @@ export function ProcessInnovationPage() {
labelColor: "text-white",
},
{
label: "ارز بری",
label: "کاهش ارز بری",
value: Number(stats.percentCurrencyReduction) || 0,
labelColor: "text-white",
},
{
label: "خرابی پر تکرار",
label: "کاهش خرابی پر تکرار",
value: Number(stats.percentFailuresReduction) || 0,
labelColor: "text-white",
},
{
label: "هزینه های عملیاتی",
value:
Number(stats.percentOperatingCostBeforeInnovation) || 0,
labelColor: "text-white",
},
]}
barHeight="h-5"
barHeight="h-6"
showAxisLabels={true}
/>
</BaseCard>
@ -828,7 +692,7 @@ export function ProcessInnovationPage() {
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-420px)]">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
@ -839,7 +703,14 @@ export function ProcessInnovationPage() {
>
{column.key === "select" ? (
<div className="flex items-center justify-center">
<span></span>
<Checkbox
checked={
selectedProjects.size === projects.length &&
projects.length > 0
}
onCheckedChange={handleSelectAll}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
) : column.sortable ? (
<button
@ -875,7 +746,7 @@ export function ProcessInnovationPage() {
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-pr-green py-1 px-2"
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
@ -908,7 +779,7 @@ export function ProcessInnovationPage() {
{columns.map((column) => (
<TableCell
key={column.key}
className={`text-right whitespace-nowrap border-pr-green py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
>
{renderCellContent(project, column)}
</TableCell>
@ -925,7 +796,7 @@ export function ProcessInnovationPage() {
{loadingMore && (
<div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-pr-green" />
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300 text-xs"></span>
</div>
</div>
@ -970,12 +841,10 @@ export function ProcessInnovationPage() {
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="space-y-4 flex justify-between text-right p-6">
<div className="space-y-4 flex justify-between text-right px-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold text-base">
{selectedProjectDetails?.title}
</h2>
<h2 className="font-bold text-base">{selectedProjectDetails?.title}</h2>
<p className="text-white font-normal text-base font-persian px-2 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>
@ -987,7 +856,7 @@ export function ProcessInnovationPage() {
<div className="flex items-center justify-between">
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<Building2 className="h-4 text-pr-green text-sm font-light" />
<Building2 className="h-4 text-green-500 text-sm font-light" />
زمان شروع:
</h4>
<span className="text-white font-normal text-base font-persian">
@ -1002,7 +871,7 @@ export function ProcessInnovationPage() {
<div className="flex items-center justify-between">
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<PickaxeIcon className="h-4 text-pr-green text-sm font-light" />
<PickaxeIcon className="h-4 text-green-500 text-sm font-light" />
زمان پایان:
</h4>
<span className="text-white font-normal text-base font-persian">
@ -1017,8 +886,8 @@ export function ProcessInnovationPage() {
<div className="flex items-center justify-between">
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<UsersIcon className="h-4 text-pr-green text-sm font-light" />
هزینه برآورد شده(میلیون ریال):
<UsersIcon className="h-4 text-green-500 text-sm font-light" />
هزینه برآورد شده:
</h4>
<span className="text-white font-normal text-base font-persian">
{selectedProjectDetails?.approved_budget
@ -1035,7 +904,7 @@ export function ProcessInnovationPage() {
</div>
<div className="flex items-center justify-between">
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<UserIcon className="h-4 text-pr-green text-sm font-light" />
<UserIcon className="h-4 text-green-500 text-sm font-light" />
نفر مرتبط:
</h4>
<span className="text-white font-normal text-base font-persian">

View File

@ -1,8 +1,6 @@
import { saveAs } from "file-saver";
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import toast from "react-hot-toast";
import XLSX from "xlsx-js-style";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card";
import {
@ -14,15 +12,9 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import {
EventBus,
formatCurrency,
formatNumber,
handleDataValue,
} from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { formatCurrency } from "~/lib/utils";
import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
interface ProjectData {
@ -175,14 +167,6 @@ export function ProjectManagementPage() {
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// const [date, setDate] = useState<CalendarDate>({
// start: `${jy}/01/01`,
// end: `${jy}/12/30`,
// });
const [date, setDate] = useStoredDate();
const fetchProjects = async (reset = false) => {
// Prevent concurrent API calls
@ -214,10 +198,7 @@ export function ProjectManagementPage() {
OutputFields: outputFields,
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [],
});
if (response.state === 0) {
@ -282,29 +263,16 @@ export function ProjectManagementPage() {
}
};
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
const loadMore = useCallback(() => {
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading, loadingMore]);
}, [loadingMore, hasMore, loading]);
useEffect(() => {
if (date.end && date.start) {
fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]);
}, [sortConfig]);
useEffect(() => {
if (currentPage > 1) {
@ -312,44 +280,30 @@ export function ProjectManagementPage() {
}
}, [currentPage]);
// Infinite scroll observer with debouncing
// Infinite scroll observer
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
return;
if (!scrollContainer || !hasMore || loadingMore) return;
// Clear previous timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// Debounce scroll events
scrollTimeoutRef.current = setTimeout(() => {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 95% of the container
if (scrollPercentage >= 0.95) {
// Trigger load more when scrolled to 90% of the container
if (scrollPercentage >= 0.9) {
loadMore();
}
}, 150);
};
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, {
passive: true,
});
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
}
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [loadMore, hasMore, loadingMore]);
@ -370,10 +324,7 @@ export function ProjectManagementPage() {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Conditions: [],
});
if (response.state === 0) {
@ -394,14 +345,14 @@ export function ProjectManagementPage() {
}
};
// const handleRefresh = () => {
// fetchingRef.current = false; // Reset fetching state on refresh
// setCurrentPage(1);
// setProjects([]);
// setHasMore(true);
// fetchProjects(true);
// fetchTotalCount();
// };
const handleRefresh = () => {
fetchingRef.current = false; // Reset fetching state on refresh
setCurrentPage(1);
setProjects([]);
setHasMore(true);
fetchProjects(true);
fetchTotalCount();
};
// ...existing code...
@ -603,10 +554,7 @@ export function ProjectManagementPage() {
// Compute counts and totals for each category so footer segments can be proportional
const categoryStats = useMemo(() => {
const stats: Record<
string,
{ counts: Record<string, number>; total: number }
> = {};
const stats: Record<string, { counts: Record<string, number>; total: number }> = {};
categoryDefs.forEach((cat) => {
const counts: Record<string, number> = {};
let total = 0;
@ -665,9 +613,7 @@ export function ProjectManagementPage() {
.map((p) => calculateRemainingDays((p as any).end_date))
.filter((v) => v !== null) as number[];
res["remaining_time"] = remainingValues.length
? Math.round(
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length
)
? Math.round(remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length)
: null;
// For other keys, parse numeric values
@ -677,17 +623,11 @@ export function ProjectManagementPage() {
.map((p) => {
const raw = (p as any)[k];
if (raw == null) return NaN;
const num = Number(
String(raw)
.toString()
.replace(/[^0-9.-]/g, "")
);
const num = Number(String(raw).toString().replace(/[^0-9.-]/g, ""));
return Number.isFinite(num) ? num : NaN;
})
.filter((n) => !Number.isNaN(n));
res[k] = vals.length
? vals.reduce((a, b) => a + b, 0) / vals.length
: null;
res[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
});
return res;
@ -728,13 +668,11 @@ export function ProjectManagementPage() {
const color = getCategoryColor(column.key, value);
return (
<span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
<span className="text-gray-300">
{!!value ? String(value) : "-"}
</span>
<span className="text-gray-300">{!!value ? String(value) : "-"}</span>
<span
style={{
backgroundColor: color,
display: !value ? "none" : "block",
display : !value ? "none" : "block",
}}
className="inline-block w-2 h-2 rounded-full"
/>
@ -751,30 +689,25 @@ export function ProjectManagementPage() {
case "deviation_from_program":
case "cost_deviation":
return (
<span className="text-sm font-normal">
{formatNumber(value as any)}
</span>
<span className="text-sm font-normal">{formatNumber(value as any)}</span>
);
case "start_date":
case "end_date":
case "done_date":
return (
<span className=" text-sm font-normal">
{formatDate(String(value))}
</span>
<span className=" text-sm font-normal">{formatDate(String(value))}</span>
);
case "project_no":
return (
<Badge variant="teal" className="border-emerald-500/50">
<Badge
variant="teal"
className="border-emerald-500/50"
>
{String(value)}
</Badge>
);
case "title":
return (
<span className="text-sm font-normal text-white">
{String(value)}
</span>
);
return <span className="text-sm font-normal text-white">{String(value)}</span>;
case "importance_project":
return (
<Badge
@ -797,94 +730,16 @@ export function ProjectManagementPage() {
}
};
// const totalPages = Math.ceil(totalCount / pageSize);
const exportToExcel = async () => {
let arr = [];
const data = await fetchExcelData();
debugger;
for (let i = 0; i < data.length; i++) {
let obj: Record<string, any> = {};
const project = data[i];
Object.entries(project).forEach(([pKey, pValue]) => {
Object.values(columns).forEach((col) => {
if (pKey === col.key) {
``;
obj[col.label] = handleDataValue(
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
);
}
});
});
arr.push(obj);
}
// تبدیل داده‌ها به worksheet
const worksheet = XLSX.utils.json_to_sheet(arr);
// ساخت workbook
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
// تبدیل به فایل باینری
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, "people.xls");
};
const fetchExcelData = async () => {
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const sortCol = columns.find((c) => c.key === sortConfig.field);
const sortField = sortCol?.computed
? undefined
: (sortCol?.apiField ?? sortCol?.key);
const response = await apiService.select({
ProcessName: "project",
OutputFields: outputFields,
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
});
const parsedData = JSON.parse(response.data);
return parsedData;
};
const totalPages = Math.ceil(totalCount / pageSize);
return (
<DashboardLayout title="مدیریت پروژه‌ها">
<div className="space-y-6">
{/* <div className="flex justify-end w-full mb-0 pl-2">
<Button
className="flex w-max justify-center rounded-xl mb-4 border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-3 text-center items-center gap-3 "
variant="ghost"
size="sm"
onClick={exportToExcel}
>
<FileChartColumnIncreasing />
دانلود فایل اکسل
</Button>
</div> */}
<div className="p-6 space-y-6">
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
{/* <div onClick={exportToExcel}>DownloadExcle</div> */}
<CardContent className="p-0">
<div className="relative">
<div
ref={scrollContainerRef}
className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"
>
<div className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]">
<Table className="table-fixed">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]">
@ -892,7 +747,7 @@ export function ProjectManagementPage() {
<TableHead
key={column.key}
className={` text-right font-persian whitespace-nowrap text-white font-semibold bg-[#3F415A] sticky top-0 z-20`}
style={{ width: column.width }}
style={{ width: column.width}}
>
{column.sortable ? (
<button
@ -912,7 +767,8 @@ export function ProjectManagementPage() {
</button>
) : (
column.label
)}
)
}
</TableHead>
))}
</TableRow>
@ -935,9 +791,7 @@ export function ProjectManagementPage() {
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div
className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{
width: `${Math.random() * 60 + 40}%`,
}}
style={{ width: `${Math.random() * 60 + 40}%` }}
/>
</div>
</TableCell>
@ -980,10 +834,7 @@ export function ProjectManagementPage() {
// First column: show total projects text similar to API count
if (colIndex === 0) {
return (
<TableCell
key={column.key}
className="p-3 text-sm text-white font-semibold font-persian"
>
<TableCell key={column.key} className="p-3 text-sm text-white font-semibold font-persian">
کل پروژهها: {formatNumber(actualTotalCount)}
</TableCell>
);
@ -1009,18 +860,15 @@ export function ProjectManagementPage() {
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{order.map((k) => {
const cnt = imp.counts[k] || 0;
const widthPercent =
imp.total > 0 ? (cnt / imp.total) * 100 : 0;
const widthPercent = imp.total > 0 ? (cnt / imp.total) * 100 : 0;
return (
<div
key={k}
title={`${k} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium"
style={{
width: `${widthPercent}%`,
backgroundColor: colorFor(k),
}}
></div>
style={{ width: `${widthPercent}%`, backgroundColor: colorFor(k) }}
>
</div>
);
})}
</div>
@ -1036,37 +884,26 @@ export function ProjectManagementPage() {
"executive_phase",
];
if (categoryLike.includes(column.key)) {
const stat = categoryStats[column.key] || {
counts: {},
total: 0,
};
const stat = categoryStats[column.key] || { counts: {}, total: 0 };
const entries = Object.entries(stat.counts);
return (
<TableCell key={column.key} className="p-1">
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{entries.length > 0 ? (
entries.map(([val, cnt]) => {
let color =
categoryColorMaps[column.key]?.[val] ||
"#6B7280";
let color = categoryColorMaps[column.key]?.[val] || "#6B7280";
if (column.key === "executive_phase") {
color =
(phaseColors as any)[val] || color;
color = (phaseColors as any)[val] || color;
}
const widthPercent =
stat.total > 0
? (cnt / stat.total) * 100
: 0;
const widthPercent = stat.total > 0 ? (cnt / stat.total) * 100 : 0;
return (
<div
key={val}
title={`${val} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium"
style={{
width: `${widthPercent}%`,
backgroundColor: color,
}}
></div>
style={{ width: `${widthPercent}%`, backgroundColor: color }}
>
</div>
);
})
) : (
@ -1084,23 +921,10 @@ export function ProjectManagementPage() {
// remaining_time: show average days with color (green/red/white)
if (column.key === "remaining_time") {
const avg = numericAverages["remaining_time"] as
| number
| null;
const color =
avg == null
? "#9CA3AF"
: avg > 0
? "#3AEA83"
: avg < 0
? "#F76276"
: "#FFFFFF";
const avg = numericAverages["remaining_time"] as number | null;
const color = avg == null ? "#9CA3AF" : avg > 0 ? "#3AEA83" : avg < 0 ? "#F76276" : "#FFFFFF";
return (
<TableCell
key={column.key}
className="p-2 text-right font-medium"
style={{ color }}
>
<TableCell key={column.key} className="p-2 text-right font-medium" style={{ color }}>
{avg == null ? "-" : `${formatNumber(avg)} روز`}
</TableCell>
);
@ -1119,15 +943,10 @@ export function ProjectManagementPage() {
const avg = numericAverages[mapped] as number | null;
let display = "-";
if (avg != null) {
display = mapped.includes("budget")
? formatCurrency(String(Math.round(avg)))
: formatNumber(Math.round(avg));
display = mapped.includes("budget") ? formatCurrency(String(Math.round(avg))) : formatNumber(Math.round(avg));
}
return (
<TableCell
key={column.key}
className="p-2 text-right font-medium text-gray-200"
>
<TableCell key={column.key} className="p-2 text-right font-medium text-gray-200">
{display}
</TableCell>
);
@ -1154,6 +973,8 @@ export function ProjectManagementPage() {
)}
</div>
</CardContent>
</Card>
</div>
</DashboardLayout>

View File

@ -1,33 +1,32 @@
import {
Box,
Building2,
ChevronDown,
ChevronRight,
FolderKanban,
GalleryVerticalEnd,
House,
LightbulbIcon,
ListTodo,
Globe,
LayoutDashboard,
Leaf,
Lightbulb,
LogOut,
Radar,
MonitorSmartphone,
Package,
Settings,
Star,
Workflow,
DiscAlbum,
LucideLightbulb
DiscAlbum
} from "lucide-react";
import React, { useState } from "react";
import { Link, useLocation } from "react-router";
import { useAuth } from "~/contexts/auth-context";
import { cn } from "~/lib/utils";
interface TitleInfo {
title: string;
icon?: React.ComponentType<{ className?: string }> | null;
}
interface SidebarProps {
isCollapsed?: boolean;
onToggleCollapse?: () => void;
className?: string;
onStrategicAlignmentClick?: () => void;
onTitleChange?: (info: TitleInfo) => void;
}
interface MenuItem {
@ -40,51 +39,52 @@ interface MenuItem {
}
const menuItems: MenuItem[] = [
{
id: "dashboard",
label: "صفحه اصلی",
icon: House,
icon: LayoutDashboard,
href: "/dashboard",
},
{
id: "project-management",
label: "مدیریت اجرای پروژه‌ها",
icon: ListTodo,
icon: FolderKanban,
href: "/dashboard/project-management",
},
{
id: "innovation-basket",
label: "سبد فناوری و نوآوری",
icon: LightbulbIcon,
icon: Box,
children: [
{
id: "product-innovation",
label: "نوآوری در محصول",
icon: null,
icon: Package,
href: "/dashboard/innovation-basket/product-innovation",
},
{
id: "process-innovation",
label: "نوآوری در فرآیند",
icon: null,
icon: Workflow,
href: "/dashboard/innovation-basket/process-innovation",
},
{
id: "digital-innovation",
label: "نوآوری دیجیتال",
icon: null,
icon: MonitorSmartphone,
href: "/dashboard/innovation-basket/digital-innovation",
},
{
id: "green-innovation",
label: "نوآوری سبز",
icon: null,
icon: Leaf,
href: "/dashboard/innovation-basket/green-innovation",
},
{
id: "internal-innovation",
label: "نوآوری ساخت داخل",
icon: null,
icon: Building2,
href: "/dashboard/innovation-basket/internal-innovation",
},
],
@ -92,30 +92,37 @@ const menuItems: MenuItem[] = [
{
id: "ecosystem",
label: "زیست بوم فناوری و نوآوری",
icon: Radar,
icon: Globe,
href: "/dashboard/ecosystem",
},
{
id: "ideas",
label: "ایده‌های فناوری و نوآوری",
icon: LucideLightbulb,
icon: Lightbulb,
href: "/dashboard/manage-ideas-tech",
},
{
id: "top-innovations",
label: "نوآور برتر",
icon: Star,
href: "/dashboard/top-innovations",
},
{
id: "strategic-alignment",
label: "میزان انطباق راهبردی",
icon: null,
href: "#", // This is not a route, it opens a popup
},
];
const bottomMenuItems: MenuItem[] = [
// {
// id: "settings",
// label: "تنظیمات",
// icon: Settings,
// href: "/dashboard/settings",
// },
{
id: "settings",
label: "تنظیمات",
icon: Settings,
href: "/dashboard/settings",
},
{
id: "logout",
label: "خروج",
@ -129,7 +136,6 @@ export function Sidebar({
onToggleCollapse,
className,
onStrategicAlignmentClick,
onTitleChange,
}: SidebarProps) {
const location = useLocation();
const [expandedItems, setExpandedItems] = useState<string[]>([]);
@ -152,35 +158,6 @@ export function Sidebar({
});
setExpandedItems(newExpandedItems);
// Update header title based on current route
// If a child route is active, use that child's label prefixed by parent label
let activeTitle: string | undefined = undefined;
let activeIcon:
| React.ComponentType<{ className?: string }>
| null
| undefined = undefined;
menuItems.forEach((item) => {
if (item.children) {
const activeChild = item.children.find(
(child) => child.href && location.pathname === child.href
);
if (activeChild) {
activeTitle = `${item.label}-${activeChild.label}`;
// prefer child icon for the page; fallback to parent
activeIcon = activeChild.icon ?? item.icon ?? null;
}
}
if (!activeTitle && item.href && location.pathname === item.href) {
activeTitle = item.label;
activeIcon = item.icon ?? null;
}
});
if (onTitleChange) {
onTitleChange({
title: activeTitle ?? "صفحه اول",
icon: activeIcon ?? null,
});
}
};
autoExpandParents();
@ -230,13 +207,8 @@ export function Sidebar({
const ItemIcon = item.icon;
const handleClick = () => {
// Only update header title for navigable items (those with href)
if (item.href && item.href !== "#") {
const icon = item.icon ?? null;
onTitleChange?.({ title: item.label, icon });
}
if (item.id === "strategic-alignment") {
console.log("test")
onStrategicAlignmentClick?.();
} else if (item.id === "logout") {
logout();
@ -250,7 +222,7 @@ export function Sidebar({
<button
key={item.id}
className={cn(
"flex items-center justify-center w-full px-2 rounded-none mt-4 transition-all duration-200 group"
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
)}
onClick={handleClick}
>
@ -260,7 +232,8 @@ export function Sidebar({
</span>
</div>
</button>
);
)
}
return (
@ -269,24 +242,22 @@ export function Sidebar({
<Link to={item.href} className="block">
<div
className={cn(
"flex items-center justify-between rounded-none w-full py-2 px-3 transition-all duration-200 group",
"flex items-center justify-between w-full py-2 px-3 rounded-lg transition-all duration-200 group",
level === 0 ? "mb-1" : "mb-0.5 mr-4",
isActive
? " text-pr-green border-r-2 border-pr-green"
: "text-gray-300 hover:text-pr-green",
? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && "hover:text-pr-red"
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{ItemIcon && (
<ItemIcon
className={cn(
"w-5 h-5 flex-shrink-0",
isActive ? "text-pr-green" : "text-current"
isActive ? "text-emerald-400" : "text-current"
)}
/>
)}
{!isCollapsed && (
<span className="font-persian text-sm font-medium truncate">
{item.label}
@ -297,7 +268,7 @@ export function Sidebar({
{!isCollapsed && (
<div className="flex items-center gap-2 flex-shrink-0">
{item.badge && (
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-pr-green text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
{item.badge}
</span>
)}
@ -328,24 +299,22 @@ export function Sidebar({
>
<div
className={cn(
"flex items-center justify-between w-full py-2 px-3 rounded-none transition-all duration-200 group",
"flex items-center justify-between w-full py-2 px-3 rounded-lg transition-all duration-200 group",
level === 0 ? "mb-1" : "mb-0.5 mr-4",
isActive
? " text-pr-green border-r-2 border-pr-green"
: "text-gray-300 cursor-pointer hover:text-pr-green",
? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && "hover:text-pr-red"
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{ItemIcon && (
<ItemIcon
className={cn(
"w-5 h-5 flex-shrink-0",
isActive ? "text-pr-green" : "text-current"
isActive ? "text-emerald-400" : "text-current"
)}
/>
)}
{!isCollapsed && (
<span className="font-persian text-sm font-medium truncate">
{item.label}
@ -356,7 +325,7 @@ export function Sidebar({
{!isCollapsed && (
<div className="flex items-center gap-2 flex-shrink-0">
{item.badge && (
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/10 text-pr-green text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/10 text-emerald-400 text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
{item.badge}
</span>
)}
@ -371,7 +340,7 @@ export function Sidebar({
(child) =>
child.href && location.pathname === child.href
)
? "text-pr-green"
? "text-emerald-400"
: "text-current"
)}
/>
@ -415,42 +384,18 @@ export function Sidebar({
<div className="flex items-center justify-start">
{!isCollapsed ? (
<div className="flex items-center gap-3">
{/* آپادانا بندرامام */}
{"نوری" == "نوری" ? (
<img
src="/brand.svg"
alt="logo"
className="bg-green-400 p-1.5 rounded-lg w-auto max-h-[30px] object-contain"
/>
) : (<GalleryVerticalEnd
<GalleryVerticalEnd
color="black"
size={32}
strokeWidth={1}
className="bg-green-400 p-1.5 rounded-lg"
/>
)}
<div className="font-persian">
{/* آپادانا بندرامام */}
{"نوری" == "نوری" ? (
<div>
<div className="text-sm font-semibold text-white">
پتروشیمی نوری
داشبورد اینوژن
</div>
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
</div>
):(
<div className="text-sm font-semibold text-white">
داشبورد مدیریت فناوری و نوآوری
</div>
)}
</div>
</div>
) : (
<div className="flex justify-center w-full">
@ -486,7 +431,7 @@ export function Sidebar({
</div>
{/* Collapse Toggle */}
{/* {onToggleCollapse && (
{onToggleCollapse && (
<div className="p-3 border-t border-gray-500/30">
<button
onClick={onToggleCollapse}
@ -503,7 +448,7 @@ export function Sidebar({
)}
</button>
</div>
)} */}
)}
</div>
);
}

View File

@ -1,80 +1,33 @@
import { useEffect, useReducer, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
LabelList,
ResponsiveContainer,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LabelList,
Cell,
} from "recharts";
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
import { Skeleton } from "~/components/ui/skeleton";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { Skeleton } from "~/components/ui/skeleton";
import { formatNumber } from "~/lib/utils";
import { ChartContainer } from "../ui/chart";
import {
DropdownMenu,
DropdownMenuButton,
DropdownMenuContent,
DropdownMenuItem,
} from "../ui/dropdown-menu";
import { TruncatedText } from "../ui/truncatedText";
interface StrategicAlignmentData {
strategic_theme: string;
operational_fee_count: number;
operational_fee_sum: number;
percentage?: number;
}
interface DropDownConfig {
isOpen: boolean;
selectedValue: string;
dropDownItems: Array<string>;
}
type Action =
| { type: "OPEN" }
| { type: "CLOSE" }
| { type: "SETVALUE"; value: Array<string> }
| { type: "SELECT"; value: string };
// const DropDownItems = [
// {
// id: 0,
// key: "همه مضامین",
// Value: "همه مضامین",
// },
// {
// id: 1,
// key: "ارزش های هم افزایی نوآورانه",
// Value: "همه مضامین",
// },
// {
// id: 2,
// key: "ارزش های خودکفایی نوآوورانه",
// Value: "همه مضامین",
// },
// {
// id: 3,
// key: "ارزش های فناوری های نوین",
// Value: "همه مضامین",
// },
// {
// id: 4,
// key: "ارزش های توسعه منابع انسانی",
// Value: "همه مضامین",
// },
// {
// id: 5,
// key: "ارزش های نوآوری سبز",
// Value: "همه مضامین",
// },
// ];
interface StrategicAlignmentPopupProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -88,10 +41,11 @@ const chartConfig = {
},
};
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const ChartSkeleton = () => (
<div className="flex justify-center h-96 w-full p-4">
{/* Chart bars */}
<div className=" w-full flex items-end gap-10">
@ -120,14 +74,6 @@ export function StrategicAlignmentPopup({
}: StrategicAlignmentPopupProps) {
const [data, setData] = useState<StrategicAlignmentData[]>([]);
const [loading, setLoading] = useState(false);
const contentRef = useRef<HTMLDivElement | null>(null);
const [state, dispatch] = useReducer(reducer, {
isOpen: false,
selectedValue: "همه مضامین",
dropDownItems: [],
});
const [date, setDate] = useStoredDate();
useEffect(() => {
if (open) {
@ -135,132 +81,33 @@ export function StrategicAlignmentPopup({
}
}, [open]);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
const fetchData = async () => {
setLoading(true);
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["strategic_theme", "count(operational_fee)"],
GroupBy: ["strategic_theme"],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
});
const responseData =
typeof response.data === "string"
? JSON.parse(response.data)
: response.data;
setBarItems(responseData);
const dropDownItems = responseData.map(
(item: any) => item.strategic_theme
);
setDropDownValues(["همه مضامین", ...dropDownItems]);
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
setLoading(false);
}
};
const fetchDropDownItems = async (item: string) => {
try {
if (item !== "همه مضامین") {
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"value_technology_and_innovation",
"count(operational_fee)",
"strategic_theme",
"sum(operational_fee) as operational_fee_sum",
],
Conditions: [
["strategic_theme", "=", item, "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
GroupBy: ["value_technology_and_innovation"],
GroupBy: ["strategic_theme"],
});
const responseData =
typeof response.data === "string"
? JSON.parse(response.data)
: response.data;
setBarItems(responseData);
} else fetchData();
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
setLoading(false);
}
};
function reducer(state: DropDownConfig, action: Action): DropDownConfig {
switch (action.type) {
case "OPEN":
return { ...state, isOpen: true };
case "CLOSE":
return { ...state, isOpen: false };
case "SETVALUE":
return { ...state, dropDownItems: action.value };
case "SELECT":
return { ...state, selectedValue: action.value };
default:
return state;
}
}
const toggleMenuHandler = () => {
dispatch({
type: "OPEN",
});
};
const selectItem = (item: string) => {
dispatch({
type: "SELECT",
value: item,
});
dispatch({
type: "CLOSE",
});
fetchDropDownItems(item);
};
const setDropDownValues = (items: Array<string>) => {
dispatch({
type: "SETVALUE",
value: items,
});
};
const setBarItems = (responseData: any) => {
const processedData = responseData
.map((item: any) => ({
strategic_theme:
item.strategic_theme || item.value_technology_and_innovation || "N/A",
operational_fee_count: Math.max(0, Number(item.operational_fee_count)),
strategic_theme: item.strategic_theme || "N/A",
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
}))
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
const total = processedData.reduce(
(acc: number, item: StrategicAlignmentData) =>
acc + item.operational_fee_count,
acc + item.operational_fee_sum,
0
);
@ -269,72 +116,23 @@ export function StrategicAlignmentPopup({
...item,
percentage:
total > 0
? Math.round((item.operational_fee_count / total) * 100)
? Math.round((item.operational_fee_sum / total) * 100)
: 0,
})
);
setData(dataWithPercentage || []);
};
const dialogHandler = (status: boolean) => {
if (onOpenChange) onOpenChange(status);
dispatch({
type: "SELECT",
value: "همه مضامین",
});
};
useEffect(() => {
if (!open) return;
const handleClickOutside = (event: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(event.target as Node)
) {
dispatch({
type: "CLOSE",
});
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
setLoading(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [open]);
return (
<Dialog open={open} onOpenChange={dialogHandler}>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
<DialogHeader className="mb-10 w-full border-b-2 border-gray-500/20">
<div>
<div className="flex">
<DropdownMenu
modal={true}
open={state.isOpen}
onOpenChange={toggleMenuHandler}
>
<DropdownMenuButton>{state.selectedValue}</DropdownMenuButton>
<DropdownMenuContent
ref={contentRef}
forceMount={true}
className="w-56"
>
{state.dropDownItems.map((item: string, key: number) => (
<div
onClick={() => selectItem(item)}
key={`${key}-${item}`}
>
<DropdownMenuItem selected={state.selectedValue === item}>
{item}
</DropdownMenuItem>
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
</DialogHeader>
{loading ? (
@ -342,10 +140,7 @@ export function StrategicAlignmentPopup({
) : (
<>
<ResponsiveContainer width="100%" height={400}>
<ChartContainer
config={chartConfig}
className="aspect-auto h-96 w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
<BarChart
data={data}
margin={{ left: 12, right: 12 }}
@ -366,7 +161,10 @@ export function StrategicAlignmentPopup({
return (
<g transform={`translate(${x},${y})`}>
<foreignObject width={80} height={20} x={-45} y={0}>
<TruncatedText maxWords={2} text={payload.value} />
<TruncatedText
maxWords={2}
text={payload.value}
/>
</foreignObject>
</g>
);
@ -381,8 +179,10 @@ export function StrategicAlignmentPopup({
tickFormatter={(value) =>
`${formatNumber(Math.round(value))}`
}
label={{
value: "تعداد برنامه ها",
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
@ -395,24 +195,21 @@ export function StrategicAlignmentPopup({
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={chartConfig.percentage.color}
/>
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
))}
<LabelList
dataKey="percentage"
position="top"
offset={15}
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) =>
`${formatNumber(Math.round(v))}`
}
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
/>
</Bar>
</BarChart>
</ChartContainer>

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Area,
AreaChart,
@ -10,12 +11,9 @@ import {
XAxis,
YAxis,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { formatNumber } from "~/lib/utils";
export interface CompanyDetails {
id: string;
@ -64,50 +62,27 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
const [isLoading, setIsLoading] = useState(true);
// const [date, setDate] = useState<CalendarDate>();
const [date, setDate] = useStoredDate();
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
useEffect(() => {
if (date.end && date.start) fetchCounts();
}, [date]);
const fetchCounts = async () => {
setIsLoading(true);
try {
const [countsRes, processRes] = await Promise.all([
apiService.call<EcosystemCounts>({
ecosystem_count_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
ecosystem_count_function: {},
}),
apiService.call<ProcessActorsResponse[]>({
process_creating_actors_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
process_creating_actors_function: {},
}),
]);
setCounts(
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0]
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
);
// Process the years data and fill missing years
const processedData = processYearsData(
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors)
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
);
setProcessData(processedData);
} catch (err) {
@ -116,6 +91,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
setIsLoading(false);
}
};
fetchCounts();
}, []);
// Helper function to safely parse numbers
const parseNumber = (value: string | undefined): number => {
@ -126,7 +103,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
// Helper function to process years data and fill missing years
const processYearsData = (
data: ProcessActorsResponse[]
data: ProcessActorsResponse[],
): ProcessActorsData[] => {
if (!data || data.length === 0) return [];
@ -144,7 +121,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
acc[item.start_year] = item.total_count;
return acc;
},
{} as Record<string, number>
{} as Record<string, number>,
);
for (let year = minYear; year <= maxYear; year++) {
@ -190,7 +167,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
{ label: "تامین کننده", value: parseNumber(counts.company_count) },
{ label: "شرکت", value: parseNumber(counts.company_count) },
]
: [];
@ -279,7 +256,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
@ -310,7 +287,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4">
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
<div className="w-16 h-8 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
<div className="w-16 h-8 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</CardHeader>
{/* Bar Chart Skeleton */}
@ -385,7 +362,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
@ -401,7 +378,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
<div className="w-12 h-6 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
<div className="w-12 h-6 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</div>
</CardContent>
</Card>
@ -455,7 +432,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
<div className="w-full">
<CustomBarChart
hasPercent={false}
data={barData.map((item) => ({
label: item.label,
value: item.value,
@ -485,13 +461,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient
id="fillDesktop"
x1="0"
y1="0"
x2="0"
y2="1"
>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
</linearGradient>
@ -530,14 +500,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
activeDot={({ cx, cy, payload }) => (
<g>
{/* Small circle */}
<circle
cx={cx}
cy={cy}
r={5}
fill="#3AEA83"
stroke="#fff"
strokeWidth={2}
/>
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
{/* Year label above point */}
<text
x={cx}
@ -553,7 +516,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
)}
/>
</AreaChart>
</ResponsiveContainer>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
دادهای برای نمایش وجود ندارد
@ -561,6 +525,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
)}
</div>
</CardContent>
</Card>
</div>
);

View File

@ -1,19 +1,11 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as d3 from "d3";
import { useCallback, useEffect, useRef, useState } from "react";
import { useStoredDate } from "~/hooks/useStoredDate";
import { EventBus } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { useAuth } from "../../contexts/auth-context";
import apiService from "../../lib/api";
import { useAuth } from "../../contexts/auth-context";
// Get API base URL at module level to avoid process.env access in browser
const API_BASE_URL =
//بندر امام
// import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
//آپادانا
// import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
//نوری
import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
export interface Node {
id: string;
@ -52,9 +44,9 @@ export interface CompanyDetails {
export interface NetworkGraphProps {
onNodeClick?: (node: CompanyDetails) => void;
onLoadingChange?: (loading: boolean) => void;
}
// Helper to robustly parse backend response
function parseApiResponse(raw: any): any[] {
let data = raw;
try {
@ -64,14 +56,12 @@ function parseApiResponse(raw: any): any[] {
return Array.isArray(data) ? data : [];
}
// Check if we're in browser environment
function isBrowser(): boolean {
return typeof window !== "undefined";
}
export function NetworkGraph({
onNodeClick,
onLoadingChange,
}: NetworkGraphProps) {
export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null);
const [nodes, setNodes] = useState<Node[]>([]);
const [links, setLinks] = useState<Link[]>([]);
@ -80,21 +70,7 @@ export function NetworkGraph({
const [error, setError] = useState<string | null>(null);
const { token } = useAuth();
// const [date, setDate] = useState<CalendarDate>();
const [date, setDate] = useStoredDate();
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
// Ensure component only renders on client side
useEffect(() => {
if (isBrowser()) {
const timer = setTimeout(() => setIsMounted(true), 100);
@ -102,27 +78,7 @@ export function NetworkGraph({
}
}, []);
const getImageUrl = useCallback(
(stageid: number) => {
if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
},
[token?.accessToken]
);
const callAPI = useCallback(
async (stage_id: number) => {
return await apiService.call<any>({
get_values_workflow_function: {
stage_id: stage_id,
// start_date: date?.start || null,
// end_date: date?.end || null,
},
});
},
[date]
);
// Fetch data from API
useEffect(() => {
if (!isMounted) return;
@ -133,45 +89,28 @@ export function NetworkGraph({
setIsLoading(true);
try {
const res = await apiService.call<any[]>({
graph_production_function: {
start_date: date.start || null,
end_date: date.end || null,
},
graph_production_function: {},
});
if (aborted) return;
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
console.log(
"All available fields in first item:",
Object.keys(data[0] || {})
Object.keys(data[0] || {}),
);
// نود مرکزی
// Create center node
const centerNode: Node = {
id: "center",
// label: "پتروشیمی بندر امام",
label: "پتروشیمی نوری",
// label: "پتروشیمی آپادانا",
label: "پتروشیمی بندر امام", //مرکز زیست بوم
category: "center",
stageid: 0,
isCenter: true,
};
// دسته‌بندی‌ها
const categories = Array.from(
new Set(data.map((item: any) => item.category))
);
const categoryNodes: Node[] = categories.map((cat, index) => ({
id: `cat-${index}`,
label: cat,
category: cat,
stageid: -1,
}));
// نودهای نهایی
const finalNodes: Node[] = data.map((item: any) => ({
id: `node-${item.stageid}`,
// Create ecosystem nodes
const ecosystemNodes: Node[] = data.map((item: any) => ({
id: String(item.stageid),
label: item.title,
category: item.category,
stageid: item.stageid,
@ -179,16 +118,13 @@ export function NetworkGraph({
rawData: item,
}));
// لینک‌ها: مرکز → دسته‌بندی‌ها → نودهای نهایی
const graphLinks: Link[] = [
...categoryNodes.map((cat) => ({ source: "center", target: cat.id })),
...finalNodes.map((node) => {
const catIndex = categories.indexOf(node.category);
return { source: `cat-${catIndex}`, target: node.id };
}),
];
// Create links (all nodes connected to center)
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
source: "center",
target: node.id,
}));
setNodes([centerNode, ...categoryNodes, ...finalNodes]);
setNodes([centerNode, ...ecosystemNodes]);
setLinks(graphLinks);
} catch (err: any) {
if (err.name !== "AbortError") {
@ -206,19 +142,43 @@ export function NetworkGraph({
aborted = true;
controller.abort();
};
}, [isMounted, token, getImageUrl, date]);
}, [isMounted, token]);
// Get image URL for a node
const getImageUrl = useCallback(
(stageid: number) => {
if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
},
[token?.accessToken],
);
// Import apiService for the onClick handler
const callAPI = useCallback(async (stage_id: number) => {
return await apiService.call<any>({
get_values_workflow_function: {
stage_id: stage_id,
},
});
}, []);
// Initialize D3 graph
useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
return;
}
const svg = d3.select(svgRef.current);
const width = svgRef.current.clientWidth;
const height = svgRef.current.clientHeight;
// Clear previous content
svg.selectAll("*").remove();
// Create defs for patterns and filters
const defs = svg.append("defs");
// Add glow filter for hover effect
const filter = defs
.append("filter")
.attr("id", "glow")
@ -236,27 +196,33 @@ export function NetworkGraph({
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
const container = svg.append("g");
// Create zoom behavior
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.3, 2.5])
.on("zoom", (event) => container.attr("transform", event.transform));
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
.on("zoom", (event) => {
container.attr("transform", event.transform);
});
svg.call(zoom);
// Create container group
const container = svg.append("g");
// Category colors
const categoryToColor: Record<string, string> = {
دانشگاه: "#3B82F6",
مشاور: "#10B981",
"دانش بنیان": "#F59E0B",
استارتاپ: "#EF4444",
"تامین کننده": "#8B5CF6",
شرکت: "#8B5CF6",
صندوق: "#06B6D4",
شتابدهنده: "#9333EA",
"مرکز نوآوری": "#F472B6",
center: "#34D399",
};
// Create force simulation
const simulation = d3
.forceSimulation<Node>(nodes)
.force(
@ -265,21 +231,16 @@ export function NetworkGraph({
.forceLink<Node, Link>(links)
.id((d) => d.id)
.distance(150)
.strength(0.2)
.strength(0.1),
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"radial",
d3.forceRadial((d) => (d.isCenter ? 0 : 300), width / 2, height / 2)
)
.force(
"collision",
d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35))
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
);
// Initial zoom to show entire graph
const initialScale = 0.6;
const initialScale = 0.85;
const initialTranslate = [
width / 2 - (width / 2) * initialScale,
height / 2 - (height / 2) * initialScale,
@ -288,69 +249,37 @@ export function NetworkGraph({
zoom.transform,
d3.zoomIdentity
.translate(initialTranslate[0], initialTranslate[1])
.scale(initialScale)
.scale(initialScale),
);
// Fix center node
// Fix center node position
const centerNode = nodes.find((n) => n.isCenter);
const categoryNodes = nodes.filter((n) => !n.isCenter && n.stageid === -1);
if (centerNode) {
const centerX = width / 2;
const centerY = height / 2;
centerNode.fx = centerX;
centerNode.fy = centerY;
const baseRadius = 450; // شعاع پایه
const variation = 100; // تغییر طول یکی در میان
const angleStep = (2 * Math.PI) / categoryNodes.length;
categoryNodes.forEach((catNode, i) => {
const angle = i * angleStep;
const radius = baseRadius + (i % 2 === 0 ? -variation : variation);
catNode.fx = centerX + radius * Math.cos(angle);
catNode.fy = centerY + radius * Math.sin(angle);
});
centerNode.fx = width / 2;
centerNode.fy = height / 2;
}
// نودهای نهایی **هیچ fx/fy نداشته باشند**
// فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// categoryNodes.forEach((catNode) => {
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
// const childCount = childNodes.length;
// const radius = 100; // فاصله از دسته
// const angleStep = (2 * Math.PI) / childCount;
// childNodes.forEach((node, i) => {
// const angle = i * angleStep;
// node.fx = catNode.fx! + radius * Math.cos(angle);
// node.fy = catNode.fy! + radius * Math.sin(angle);
// });
// });
// Curved links
// Create links
const link = container
.selectAll(".link")
.data(links)
.enter()
.append("path")
.append("line")
.attr("class", "link")
.attr("stroke", "#E2E8F0")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.6)
.attr("fill", "none");
.attr("stroke-opacity", 0.6);
// Create node groups
const nodeGroup = container
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.style("cursor", (d) => (d.stageid === -1 ? "default" : "pointer"));
.style("cursor", "pointer");
// Add drag behavior
const drag = d3
.drag<SVGGElement, Node>()
.on("start", (event, d) => {
@ -372,65 +301,18 @@ export function NetworkGraph({
nodeGroup.call(drag);
// Add node circles/rectangles
nodeGroup.each(function (d) {
const group = d3.select(this);
// if (d.isCenter) {
// const rect = group
// .append("rect")
// .attr("width", 200)
// .attr("height", 80)
// .attr("x", -100) // نصف عرض جدید منفی
// .attr("y", -40) // نصف ارتفاع جدید منفی
// .attr("rx", 8)
// .attr("ry", 8)
// .attr("fill", categoryToColor[d.category] || "#94A3B8")
// .attr("stroke", "#FFFFFF")
// .attr("stroke-width", 3)
// .style("pointer-events", "none");
// if (d.imageUrl || d.isCenter) {
// const pattern = defs
// .append("pattern")
// .attr("id", `image-${d.id}`)
// .attr("x", 0)
// .attr("y", 0)
// .attr("width", 1)
// .attr("height", 1);
// pattern
// .append("image")
// .attr("x", 0)
// .attr("y", 0)
// .attr("width", 200) // ← هم‌اندازه با مستطیل
// .attr("height", 80)
// .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
// .attr("preserveAspectRatio", "xMidYMid slice");
// rect.attr("fill", `url(#image-${d.id})`);
// }
// }
// راه حل ساده‌تر - ابعاد ثابت با حفظ نسبت
if (d.isCenter) {
//آپادانا
// const fixedWidth = 196;
// const fixedHeight = 200; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
//بندر امام
// const fixedWidth = 100;
// const fixedHeight = 80; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
//نوری
const fixedWidth = 186;
const fixedHeight = 70; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
if (d.isCenter) {
// Center node as rectangle
const rect = group
.append("rect")
.attr("width", fixedWidth)
.attr("height", fixedHeight)
.attr("x", -fixedWidth / 2)
.attr("y", -fixedHeight / 2)
.attr("width", 150)
.attr("height", 60)
.attr("x", -75)
.attr("y", -30)
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8")
@ -438,6 +320,8 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
.attr("stroke-width", 3)
.style("pointer-events", "none");
// Add center image if available
if (d.imageUrl || d.isCenter) {
const pattern = defs
.append("pattern")
.attr("id", `image-${d.id}`)
@ -450,22 +334,23 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
.append("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", fixedWidth)
.attr("height", fixedHeight)
.attr("width", 150)
.attr("height", 60)
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
.attr("preserveAspectRatio", "xMidYMid meet"); // حفظ نسبت تصویر
.attr("preserveAspectRatio", "xMidYMid slice");
rect.attr("fill", `url(#image-${d.id})`);
}
else {
}
} else {
// Regular nodes as circles
const circle = group
.append("circle")
.attr("r", 25)
.attr("fill", categoryToColor[d.category] || "#fff")
.attr("fill", categoryToColor[d.category] || "8#fff")
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 3);
// Add node image if available
if (d.imageUrl) {
const pattern = defs
.append("pattern")
@ -482,8 +367,10 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
.attr("width", 50)
.attr("height", 50)
.attr("href", d.imageUrl)
.attr("backgroundColor", "#fff")
.attr("preserveAspectRatio", "xMidYMid slice");
// Create circular clip path
defs
.append("clipPath")
.attr("id", `clip-${d.id}`)
@ -497,26 +384,12 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
}
});
// Add labels below nodes
const labels = nodeGroup
.append("text")
.text((d) => d.label)
.attr("text-anchor", "middle")
.attr("dy", (d) => {
if (d.isCenter) {
//آپادانا
// const centerNodeHeight = 200; // ارتفاع نود مرکزی
//بندر امام
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
//نوری
const centerNodeHeight = 80; // ارتفاع نود مرکزی
return centerNodeHeight / 2 + 20; // نصف ارتفاع + فاصله 20px
}
return 45; // برای نودهای دیگر
})
.attr("dy", (d) => (d.isCenter ? 50 : 45))
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
.attr("font-weight", "bold")
.attr("fill", "#F9FAFB")
@ -524,6 +397,7 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
.attr("stroke-width", 4)
.attr("paint-order", "stroke");
// Add hover effects
nodeGroup
.on("mouseenter", function (event, d) {
if (d.isCenter) return;
@ -545,45 +419,34 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
.attr("stroke-width", 3);
});
// Add click handlers
nodeGroup.on("click", async function (event, d) {
event.stopPropagation();
// جلوگیری از کلیک روی مرکز و دسته‌بندی‌ها
if (d.isCenter || d.stageid === -1) return;
// Don't handle center node clicks
if (d.isCenter) return;
if (onNodeClick && d.stageid) {
// Open dialog immediately with basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
category: d.category,
stageid: d.stageid,
fields: [],
};
onNodeClick(basicDetails);
// Start loading
onLoadingChange?.(true);
try {
if (date.start && date.end) {
// Fetch detailed company data
const res = await callAPI(d.stageid);
const responseData = JSON.parse(res.data);
const fieldValues =
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
// Filter out image fields and find description
const filteredFields = fieldValues.filter(
(field: any) =>
!["image", "img", "full_name", "about_collaboration"].includes(
field.F.toLowerCase()
)
field.F.toLowerCase(),
),
);
const descriptionField = fieldValues.find(
(field: any) =>
field.F.toLowerCase().includes("description") ||
field.F.toLowerCase().includes("about_collaboration") ||
field.F.toLowerCase().includes("about")
field.F.toLowerCase().includes("about"),
);
const companyDetails: CompanyDetails = {
@ -596,37 +459,39 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
};
onNodeClick(companyDetails);
}
} catch (error) {
console.error("Failed to fetch company details:", error);
// Keep the basic details already shown
} finally {
// Stop loading
onLoadingChange?.(false);
// Fallback to basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
category: d.category,
stageid: d.stageid,
fields: [],
};
onNodeClick(basicDetails);
}
}
});
// Update positions on simulation tick
simulation.on("tick", () => {
link.attr("d", (d: any) => {
const sx = (d.source as Node).x!;
const sy = (d.source as Node).y!;
const tx = (d.target as Node).x!;
const ty = (d.target as Node).y!;
const dx = tx - sx;
const dy = ty - sy;
const dr = Math.sqrt(dx * dx + dy * dy) * 1.2; // منحنی
return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;
});
link
.attr("x1", (d) => (d.source as Node).x!)
.attr("y1", (d) => (d.source as Node).y!)
.attr("x2", (d) => (d.target as Node).x!)
.attr("y2", (d) => (d.target as Node).y!);
nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
// Cleanup function
return () => {
simulation.stop();
};
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI, date]);
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
// Show error message
if (error) {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
@ -640,6 +505,7 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
);
}
// Don't render on server side
if (!isMounted) {
return (
<div className="w-full h-full flex items-center justify-center bg-transparent">
@ -653,11 +519,14 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
if (isLoading) {
return (
<div className="w-full h-full relative bg-transparent">
{/* Skeleton Graph Container */}
<div className="w-full h-full flex items-center justify-center relative">
{/* Center Node Skeleton */}
<div className="w-12 h-12 rounded-lg bg-gray-600 animate-pulse relative z-10">
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
</div>
{/* Outer Ring Nodes Skeleton */}
{Array.from({ length: 8 }).map((_, i) => {
const angle = (i * 2 * Math.PI) / 8;
const radius = 120;
@ -678,25 +547,40 @@ const fixedHeight = 70; // یا می‌توانید براساس نسبت تصو
<div
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
style={{
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
transformOrigin: "left center",
left: "50%",
top: "40px",
transform: "translateX(-50%)",
animationDelay: `${i * 200 + 100}ms`,
}}
></div>
<div
className="absolute w-0.5 bg-gray-600 animate-pulse opacity-30"
style={{
left: "50%",
top: "50%",
height: `${radius - 16}px`,
transformOrigin: "top",
transform: `translateX(-50%) rotate(${angle + Math.PI}rad)`,
animationDelay: `${i * 100}ms`,
}}
></div>
</div>
);
})}
</div>
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2">
<div className="text-white font-persian text-sm animate-pulse">
در حال بارگذاری نمودار...
</div>
</div>
</div>
);
}
return (
<div className="w-full h-full">
<svg
ref={svgRef}
className="w-full h-full bg-transparent"
style={{ cursor: "grab" }}
/>
<div className="w-full h-full relative bg-transparent overflow-hidden">
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
</div>
);
}

View File

@ -1,67 +0,0 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import React from "react";
interface MonthItem {
id: string;
label: string;
start: string;
end: string;
}
// interface CurrentDay {
// start: string;
// end: string;
// month: string;
// }
interface CalendarProps {
title: string;
nextYearHandler: () => void;
prevYearHandler: () => void;
currentYear?: number;
monthList: Array<MonthItem>;
selectedDate?: string;
selectDateHandler: (item: MonthItem) => void;
}
export const Calendar: React.FC<CalendarProps> = ({
title,
nextYearHandler,
prevYearHandler,
currentYear,
monthList,
selectedDate,
selectDateHandler,
}) => {
return (
<div className="filter-box bg-pr-gray w-full px-1">
<header className="flex flex-row border-b border-[#5F6284] pb-1.5 justify-center">
<span className="font-light">{title}</span>
<div className="flex flex-row items-center gap-3">
<ChevronRight
className="inline-block w-6 h-6 cursor-pointer"
onClick={nextYearHandler}
/>
<span className="font-light">{currentYear}</span>
<ChevronLeft
className="inline-block w-6 h-6 cursor-pointer"
onClick={prevYearHandler}
/>
</div>
</header>
<div className="content flex flex-col gap-2 text-center pt-1 cursor-pointer">
{monthList.map((item, index) => (
<span
key={`${item.id}-${index}`}
className={`text-lg hover:bg-[#33364D] p-1 rounded-xl transition-all duration-300 ${
selectedDate === item.label ? `bg-[#33364D]` : ""
}`}
onClick={() => selectDateHandler(item)}
>
{item.label}
</span>
))}
</div>
</div>
);
};

View File

@ -7,7 +7,7 @@ interface BaseCardProps {
headerClassName?: string;
contentClassName?: string;
children: React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
icon ?: React.ComponentType<{ className?: string }>;
withHeader?: boolean;
}
@ -18,42 +18,30 @@ export function BaseCard({
contentClassName,
children,
withHeader = false,
icon: Icon,
icon : Icon,
}: BaseCardProps) {
return (
<Card
className={cn(
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-2 pb-0 grid items-center",
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center",
className
)}
>
{Icon && title ? (
<CardHeader
className={cn(
"border-b-2 border-gray-500/20 py-2 px-0 pb-4",
headerClassName
)}
>
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">
{title} {<Icon />}{" "}
</CardTitle>
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle>
</CardHeader>
) : withHeader && title ? (
<CardHeader
className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}
>
<CardTitle className="text-white text-sm text-right font-persian px-4">
{title}
</CardTitle>
) :
withHeader && title ? (
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
</CardHeader>
) : title ? (
<div className="border-b-2 border-gray-500/20 pb-2">
<h3 className="text-sm font-bold text-white text-right font-persian px-4">
{title}
</h3>
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
</div>
) : null}
<CardContent className={cn("py-2 px-4 ", contentClassName)}>
<CardContent className={cn("py-2 px-4", contentClassName)}>
{children}
</CardContent>
</Card>

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-lg border bg-card text-card-foreground shadow-sm ",
className
)}
{...props}

View File

@ -1,4 +1,4 @@
import { calculateNiceRange, formatNumber } from "~/lib/utils";
import { formatNumber } from "~/lib/utils";
export interface BarChartData {
label: string;
@ -18,7 +18,6 @@ interface CustomBarChartProps {
showAxisLabels?: boolean;
className?: string;
loading?: boolean;
hasPercent?: boolean;
}
export function CustomBarChart({
@ -29,17 +28,16 @@ export function CustomBarChart({
showAxisLabels = true,
className = "",
loading = false,
hasPercent = true,
}: CustomBarChartProps) {
// استفاده از nice numbers برای محاسبه دامنه مناسب
const values = data.map((item) => item.maxValue || item.value);
const { niceMax, ticks } = calculateNiceRange(values, 0, 5);
const globalMaxValue = niceMax;
// Calculate the maximum value across all data points for consistent scaling
const globalMaxValue = Math.max(
...data.map((item) => item.maxValue || item.value)
);
// Loading skeleton
if (loading) {
return (
<div className={`space-y-6 p-4 pt-0 ${className}`} style={{ height }}>
<div className={`space-y-6 p-4 ${className}`} style={{ height }}>
{title && (
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
)}
@ -69,24 +67,22 @@ export function CustomBarChart({
return (
<div className={`space-y-6 ${className}`} style={{ height }}>
{title && (
<div className="border-b-[#3F415A] border-b-2">
<h3 className="text-sm font-semibold text-white font-persian text-right px-4 pb-3">
{title && <div className="border-b-[#3F415A] border-b-2">
<h3 className="text-sm font-semibold text-white font-persian text-right p-4">
{title}
</h3>
</div>
)}
</div>}
<div className="space-y-4 px-4 pb-4">
{data.map((item, index) => {
// محاسبه درصد بر اساس nice max value
const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const displayValue: any = item.value;
return (
<div key={index} className="flex items-center gap-3">
<span
className={`font-persian text-sm font-normal min-w-[120px] text-left ${
className={`font-persian text-sm font-normal min-w-[120px] text-right ${
item.labelColor || "text-white"
}`}
>
@ -105,12 +101,11 @@ export function CustomBarChart({
>
<div className="inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
</div>
<span className={`text-base font-normal text-left text-white`}>
<span
className={`text-base font-normal text-left text-white`}
>
{item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))}
{hasPercent ? "%" : ""}
{item.valueSuffix || ""}
</span>
</div>
@ -118,16 +113,24 @@ export function CustomBarChart({
);
})}
{/* Axis Labels با استفاده از nice numbers */}
{/* Axis Labels */}
{showAxisLabels && globalMaxValue > 0 && (
<div className="flex w-full items-center gap-3 mt-6">
<span className="min-w-[120px]"></span>
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
{ticks.map((tick, index) => (
<span key={index} className="text-gray-400 text-xs">
{formatNumber(tick)}%
<span className="text-gray-400 text-xs">{formatNumber(0)}</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 2))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round((globalMaxValue * 3) / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue))}
</span>
))}
</div>
<span className="min-w-[0px]"></span>
</div>

View File

@ -1,18 +1,18 @@
"use client";
"use client"
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "~/lib/utils";
import { cn } from "~/lib/utils"
const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
@ -59,13 +59,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col p-4 space-y-1.5 text-center sm:text-left",
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
@ -78,8 +78,8 @@ const DialogFooter = ({
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@ -105,18 +105,18 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogOverlay,
DialogClose,
DialogTrigger,
};
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -1,27 +1,27 @@
"use client";
"use client"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronDown, Circle } from "lucide-react";
import * as React from "react";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "~/lib/utils";
import { cn } from "~/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
@ -34,10 +34,11 @@ const DropdownMenuSubTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -51,9 +52,9 @@ const DropdownMenuSubContent = React.forwardRef<
)}
{...props}
/>
));
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -64,34 +65,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden mt-1 rounded-xl border border-gray-500 bg-pr-gray p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
selected?: boolean;
inset?: boolean
}
>(({ className, inset, selected, ...props }, ref) => (
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"cursor-pointer select-none rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-dark-blue data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
selected && "bg-dark-blue text-white",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@ -113,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -136,13 +135,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
@ -154,8 +153,8 @@ const DropdownMenuLabel = React.forwardRef<
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@ -166,8 +165,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
@ -178,43 +177,24 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
const DropdownMenuButton = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.Trigger
ref={ref}
className={cn(
"flex items-center justify-between gap-2 text-sm outline-none border border-gray-500 p-3 rounded-xl min-w-50 max-w-72 group",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 transition-transform duration-200 group-data-[state=open]:rotate-180" />
</DropdownMenuPrimitive.Trigger>
));
DropdownMenuButton.displayName = "DropdownMenuButton";
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuButton,
DropdownMenuCheckboxItem,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};
DropdownMenuRadioGroup,
}

View File

@ -13,7 +13,6 @@ interface FunnelChartProps {
title?: string;
className?: string;
}
const greenColors = ["#3C9F71","#3BC47A","#3BC47A","#3BD77E","#3AEA83"]
export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
const maxValue = Math.max(...data.map(d => d.value));
@ -25,18 +24,18 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
return (
<div className={`w-full ${className}`}>
{title && (
<h3 className="text-sm px-4 font-semibold text-white mb-4 py-2 text-right border-b-2 border-gray-400/20">
<h3 className="text-lg font-semibold text-white mb-4 py-2 text-right border-b-2 border-gray-400/20">
{title}
</h3>
)}
<div className="flex px-4 flex-col items-center gap-2 space-y-2">
<div className="flex flex-col items-center gap-2 space-y-2">
{/* Start Process Line */}
<div className="flex items-center w-full gap-10 mt-6 px-4">
<div className="text-sm font-normal text-[#5F6284] min-w-[max-content]">ابتدا فرآیند</div>
<div className="flex items-center w-full gap-10 mt-6">
<div className="text-lg text-gray-600 min-w-[max-content]">ابتدا فرآیند</div>
<div className="flex items-center w-full gap-4">
<div className="w-full h-0.5 bg-gray-600 relative">
<div className="text-base text-white font-semibold absolute left-1/2 -translate-x-1/2 top-[-1rem] -translate-y-1/2">۱۰۰%</div>
<div className="text-2xl text-white absolute left-1/2 -translate-x-1/2 top-[-1rem] -translate-y-1/2">۱۰۰%</div>
<div className="absolute -top-1 left-0 w-1 h-3 bg-gray-600"></div>
<div className="absolute -top-1 right-0 w-1 h-3 bg-gray-600"></div>
</div>
@ -51,17 +50,17 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
return (
<div key={index} className="grid grid-cols-[6rem_1fr] gap-2 w-full">
<div className="text-sm font-light text-white font-persian cols-start-1 justify-self-start min-w-[max-content] text-center">
<div className="text-lg text-white cols-start-1 justify-self-start font-thin min-w-[max-content] text-center">
{item.label}
</div>
<div className="flex items-center gap-10 w-full cols-start-2 justify-center">
<div className="flex items-center gap-10 w-full cols-start-2 flex items-center justify-center w-full">
<div className="flex items-center w-full">
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
<div
className="bg-[#3BC47A] h-8 rounded-2xl flex items-center justify-center text-lg relative"
style={{ width: `${barWidth}%` ,backgroundColor : `${greenColors[index]}`}}
style={{ width: `${barWidth}%` }}
>
<span className="text-pr-gray text-base font-semibold">
<span className="text-[#3F415A] font-semibold">
{item.value.toLocaleString('fa-IR')}
</span>
</div>
@ -74,15 +73,15 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
</div>
{/* End Process Line */}
<div className="flex items-center w-full gap-10 px-4">
<div className="text-sm text-[#5F6284] min-w-[max-content]">انتها فرآیند</div>
<div className="flex items-center w-full gap-10">
<div className="text-lg text-gray-600 min-w-[max-content]">انتها فرآیند</div>
<div className="flex items-center w-full gap-4">
{(() => {
const lastValue = data[data.length - 1]?.value ?? 0;
const percent = toPercent(lastValue);
return (
<div style={{ width: `${percent}%` }} className={`mx-auto h-0.5 bg-gray-600 relative ${percent === 0 ? "hidden" : ""}`}>
<div className="text-base font-semibold text-white absolute left-1/2 -translate-x-1/2 bottom-[-2.5rem] -translate-y-1">{formatNumber(percent)}%</div>
<div className="text-2xl text-white absolute left-1/2 -translate-x-1/2 bottom-[-2.5rem] -translate-y-1">{formatNumber(percent)}%</div>
<div className="absolute -top-1 left-0 w-1 h-3 bg-gray-600"></div>
<div className="absolute -top-1 right-0 w-1 h-3 bg-gray-600"></div>
</div>

View File

@ -17,9 +17,9 @@ export function MetricCard({
percentLabel = "درصد به کل",
}: MetricCardProps) {
return (
<BaseCard title={title} className="h-full">
<BaseCard title={title}>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4 h-full">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl font-bold text-green-400">
{formatNumber(value)}

View File

@ -1,51 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn, formatNumber } from "~/lib/utils"
import { cn } from "~/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => {
// Dynamic scaling logic based on value ranges
const getScaledValue = (inputValue: number) => {
const numValue = Number(inputValue);
if (numValue <= 1) {
return numValue * 100;
}
else if (numValue <= 10) {
return (numValue / 10) * 100;
} else if (numValue <= 50) {
return (numValue / 50) * 100;
}
else {
return numValue
}
};
const scaledValue = getScaledValue(Number(value) || 0);
const displayValue = Number(value) || 0;
return (
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]">
{formatNumber(displayValue.toFixed(2))}%
</span>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-pr-green transition-all z-20"
style={{ transform: `translateX(-${100-scaledValue}%)` }}
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
})
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -4,12 +4,11 @@ import { cn } from "~/lib/utils"
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
containerClassName?: string
containerRef?: React.RefObject<HTMLDivElement | null>
}
const Table = React.forwardRef<HTMLTableElement, TableProps>(
({ className, containerClassName, containerRef, ...props }, ref) => (
<div ref={containerRef} className={cn("relative w-full", containerClassName)}>
({ className, containerClassName, ...props }, ref) => (
<div className={cn("relative w-full", containerClassName)}>
<table
ref={ref}
className={cn("w-full caption-bottom text-sm h-full", className)}

View File

@ -81,7 +81,7 @@ export function TabsTrigger({
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive
? "bg-pr-gray text-foreground shadow-sm"
? "bg-gray-700 text-foreground shadow-sm"
: "hover:bg-muted/50",
className,
)}

View File

@ -52,7 +52,7 @@ function TooltipContent({
{...props}
>
{children}
<TooltipPrimitive.Arrow className={cn("bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",className)} />
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)

View File

@ -1,27 +0,0 @@
import jalaali from "jalaali-js";
import { useEffect, useState } from "react";
import type { CalendarDate } from "~/types/util.type";
const { jy } = jalaali.toJalaali(new Date());
export function useStoredDate(): [
CalendarDate,
React.Dispatch<React.SetStateAction<CalendarDate>>,
] {
const [date, setDate] = useState<CalendarDate>({});
useEffect(() => {
const storedDate = localStorage.getItem("dateSelected");
if (storedDate) {
setDate(JSON.parse(storedDate));
} else {
setDate({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
}
}, [jy]);
return [date, setDate];
}

View File

@ -162,17 +162,10 @@ class ApiService {
// Innovation process function call wrapper
public async call<T = any>(payload: any) {
//بندر امام
// const url = "https://inogen-back.pelekan.org/api/call";
//آپادانا
// const url = "https://APADANA-IATM-back.pelekan.org/api/call";
//نوری
const url = "https://NOPC-IATM-back.pelekan.org/api/call";
const url = "https://inogen-back.pelekan.org/api/call";
return this.postAbsolute<T>(url, payload);
}
// GET request
public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {

View File

@ -1,7 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import EventEmitter from "events";
import moment from "moment-jalaali";
import { twMerge } from "tailwind-merge";
import moment from "moment-jalaali";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@ -23,100 +22,10 @@ export const formatCurrency = (amount: string | number) => {
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
};
/**
* محاسبه دامنه nice numbers برای محور Y نمودارها
* @param values آرایه از مقادیر دادهها
* @param minValue حداقل مقدار (پیش‌فرض: 0 برای دادههای درصدی)
* @param marginPercent درصد حاشیه اضافی (پیش‌فرض: 5%)
* @returns شیء شامل حداکثر nice، فاصله tick ها، و آرایه tick ها
*/
export function calculateNiceRange(
values: number[],
minValue: number = 0,
marginPercent: number = 5
): {
niceMax: number;
tickInterval: number;
ticks: number[];
} {
if (values.length === 0) {
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
}
// پیدا کردن حداکثر مقدار در داده‌ها
const dataMax = Math.max(...values);
// اگر همه مقادیر صفر یا منفی هستند
if (dataMax <= 0) {
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
}
// اضافه کردن حاشیه
const maxWithMargin = dataMax * (1 + marginPercent / 100);
// محاسبه nice upper limit
const niceMax = calculateNiceNumber(maxWithMargin, true);
// محاسبه فاصله مناسب tick ها بر اساس niceMax
const range = niceMax - minValue;
const targetTicks = 5; // هدف: 5 tick
const roughTickInterval = range / (targetTicks - 1);
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
// ایجاد آرایه tick ها
const ticks: number[] = [];
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
ticks.push(Math.round(i));
}
// اطمینان از اینکه niceMax در آرایه tick ها باشد
if (ticks[ticks.length - 1] !== niceMax) {
ticks.push(niceMax);
}
return {
niceMax,
tickInterval: niceTickInterval,
ticks,
};
}
/**
* محاسبه عدد nice (گرد و خوانا) بر اساس الگوریتم nice numbers
* @param value مقدار ورودی
* @param round آیا به سمت بالا گرد شود یا نه
* @returns عدد nice
*/
function calculateNiceNumber(value: number, round: boolean): number {
if (value <= 0) return 0;
// پیدا کردن قدرت 10
const exponent = Math.floor(Math.log10(value));
const fraction = value / Math.pow(10, exponent);
let niceFraction: number;
if (round) {
// برای حداکثر: به سمت بالا گرد می‌کنیم با دقت بیشتر
if (fraction <= 1.0) niceFraction = 1;
else if (fraction <= 2.0) niceFraction = 2;
else if (fraction <= 2.5) niceFraction = 2.5;
else if (fraction <= 5.0) niceFraction = 5;
else if (fraction <= 7.5) niceFraction = 7.5;
else niceFraction = 10;
} else {
// برای فاصله tick ها: اعداد ساده‌تر
if (fraction <= 1.0) niceFraction = 1;
else if (fraction <= 2.0) niceFraction = 2;
else if (fraction <= 5.0) niceFraction = 5;
else niceFraction = 10;
}
return niceFraction * Math.pow(10, exponent);
}
export const handleDataValue = (val: any): any => {
moment.loadPersian({ usePersianDigits: true });
moment.loadPersian({ usePersianDigits: true });
if (val == null) return val;
if (
typeof val === "string" &&
@ -131,6 +40,4 @@ export const handleDataValue = (val: any): any => {
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
}
return val;
};
export const EventBus = new EventEmitter();
}

View File

@ -1,33 +1,27 @@
import moment from "moment-jalaali";
import type { Route } from "./+types/ecosystem";
import React from "react";
import { ProtectedRoute } from "~/components/auth/protected-route";
import { DashboardLayout } from "~/components/dashboard/layout";
import { InfoPanel } from "~/components/ecosystem/info-panel";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { Card, CardContent } from "~/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { InfoPanel } from "~/components/ecosystem/info-panel";
import { useAuth } from "~/contexts/auth-context";
import type { Route } from "./+types/ecosystem";
import moment from "moment-jalaali";
// Get API base URL at module level to avoid process.env access in browser
const API_BASE_URL =
//بندر امام
// import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
//آپادانا
// import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
//نوری
import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
// Import the CompanyDetails type
import { Hexagon } from "lucide-react";
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
import { formatNumber } from "~/lib/utils";
export function meta({}: Route.MetaArgs) {
return [
@ -61,20 +55,10 @@ function handleValue(val: any): any {
export default function EcosystemPage() {
const [selectedCompany, setSelectedCompany] =
React.useState<CompanyDetails | null>(null);
const [isDialogLoading, setIsDialogLoading] = React.useState(false);
const { token } = useAuth();
const closeDialog = () => {
setSelectedCompany(null);
setIsDialogLoading(false);
};
const handleNodeClick = (company: CompanyDetails) => {
setSelectedCompany(company);
};
const handleLoadingChange = (loading: boolean) => {
setIsDialogLoading(loading);
};
// Construct image URL
@ -85,7 +69,7 @@ export default function EcosystemPage() {
return (
<ProtectedRoute requireAuth={true}>
<DashboardLayout title="زیست بوم فناوری">
<div>
<div className="p-4 lg:p-6">
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
<div className="lg:col-span-4">
<InfoPanel selectedCompany={selectedCompany} />
@ -94,10 +78,7 @@ export default function EcosystemPage() {
<div className="lg:col-span-8 h-full">
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
<CardContent className="p-0 h-full bg-transparent">
<NetworkGraph
onNodeClick={handleNodeClick}
onLoadingChange={handleLoadingChange}
/>
<NetworkGraph onNodeClick={setSelectedCompany} />
</CardContent>
</Card>
</div>
@ -109,7 +90,7 @@ export default function EcosystemPage() {
open={!!selectedCompany}
onOpenChange={(open) => !open && closeDialog()}
>
<DialogContent className="font-persian max-w-6xl min-h-max bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogHeader>
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
معرفی
@ -117,44 +98,7 @@ export default function EcosystemPage() {
</DialogTitle>
</DialogHeader>
{isDialogLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-6">
{/* Right Column - Loading Skeleton */}
<div className="space-y-4 p-6 border-l-2 border-gray-600">
{/* Company Image & Title Skeleton */}
<div className="flex justify-between px-10 items-center mb-4">
<div className="h-8 bg-gray-600 rounded animate-pulse w-48"></div>
<div className="w-12 h-12 bg-gray-600 rounded-2xl animate-pulse"></div>
</div>
{/* Description Skeleton */}
<div className="p-4 rounded-lg space-y-2">
<div className="h-4 bg-gray-600 rounded animate-pulse w-full"></div>
<div className="h-4 bg-gray-600 rounded animate-pulse w-5/6"></div>
<div className="h-4 bg-gray-600 rounded animate-pulse w-4/6"></div>
<div className="h-4 bg-gray-600 rounded animate-pulse w-3/6"></div>
</div>
</div>
{/* Left Column - Loading Skeleton */}
<div className="space-y-2">
<div className="h-6 bg-gray-600 rounded animate-pulse w-32"></div>
<div className="space-y-3 px-2">
{Array.from({ length: 6 }).map((_, index) => (
<div
key={index}
className="flex justify-between items-center rounded-lg"
>
<div className="flex items-center gap-1">
<div className="h-4 w-4 bg-gray-600 rounded animate-pulse"></div>
<div className="h-4 bg-gray-600 rounded animate-pulse w-24"></div>
</div>
<div className="h-4 bg-gray-600 rounded animate-pulse w-20"></div>
</div>
))}
</div>
</div>
</div>
) : (
<div className="grid p-4 pb-6 grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Right Column - Description */}
<div className="space-y-4 p-6 border-l-2 border-gray-600">
{/* Company Image */}
@ -177,7 +121,7 @@ export default function EcosystemPage() {
/>
) : null}
<div
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-pr-green flex items-center justify-center"
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-green-400 flex items-center justify-center"
style={{
display:
selectedCompany?.stageid && token?.accessToken
@ -220,23 +164,18 @@ export default function EcosystemPage() {
</h3>
{selectedCompany?.fields &&
selectedCompany.fields.length > 0 ? (
<div className="space-y-3 px-2">
<div className="space-y-3 px-4">
{selectedCompany.fields.map((field, index) => (
<div
key={index}
className="flex justify-between items-center rounded-lg"
>
<span className="font-persian flex items-center gap-1 text-sm font-light">
<Hexagon className="text-pr-green h-4 w-4" />
<span className="font-persian text-sm font-light">
{field.N}:
</span>
<span className="text-right min-w-1/3">
<span className="font-persian text-sm font-normal text-right">
<span className="font-persian text-sm font-light text-right">
{handleValue(field.V)}
{field.U && (
<span className="mr-1">({field.U})</span>
)}
</span>
{field.U && <span className="mr-1">({field.U})</span>}
</span>
</div>
))}
@ -248,7 +187,6 @@ export default function EcosystemPage() {
)}
</div>
</div>
)}
</DialogContent>
</Dialog>
</DashboardLayout>

View File

@ -3,7 +3,7 @@ import { ManageIdeasTechPage } from "~/components/dashboard/project-management/m
export function meta() {
return [
{ title: "مدیریت فناوری و ایده ها" },
{ title: "مدیریت فنواری و ایده ها" },
{ name: "description", content: "مدیریت پروژه‌های فناوری و نوآوری" },
];
}

View File

@ -1,6 +0,0 @@
export interface CalendarDate {
start?: string;
end?: string;
sinceMonth?: string;
untilMonth?: string;
}

621
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"file-saver": "^2.0.5",
"graphology": "^0.26.0",
"isbot": "^5.1.27",
"lucide-react": "^0.525.0",
@ -36,13 +35,11 @@
"react-hot-toast": "^2.5.2",
"react-router": "^7.7.0",
"recharts": "^2.15.4",
"tailwind-merge": "^3.3.1",
"xlsx-js-style": "^1.2.0"
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@react-router/dev": "^7.7.0",
"@tailwindcss/vite": "^4.1.4",
"@types/file-saver": "^2.0.7",
"@types/node": "^20",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,13 +1,23 @@
<svg width="60" height="16" viewBox="0 0 60 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2855_3816)">
<path d="M34.9888 10.8078C35.4446 11.1829 35.9075 11.4573 36.4274 11.6275C38.8345 12.4228 41.3485 11.103 41.993 8.71017C42.6304 6.33465 40.9497 3.89315 38.45 3.56322C36.1746 3.26107 34.1164 4.67457 33.6428 6.86602C33.5538 7.26888 33.5538 7.68217 33.5538 8.09545C33.5538 10.6446 33.5538 13.1938 33.5538 15.7429C33.5538 15.7985 33.5538 15.8506 33.5538 15.9062C33.5574 15.9999 33.5253 16.0312 33.4363 15.9791C32.8345 15.6249 32.19 15.3331 31.6239 14.9233C31.0577 14.5135 30.7408 13.9405 30.7194 13.2459C30.698 12.4506 30.7087 11.6553 30.7087 10.8599C30.7087 8.96369 30.7194 7.07092 30.7052 5.17468C30.698 4.16057 31.136 3.4243 32.0369 2.92419C33.5431 2.0872 35.0458 1.24327 36.5414 0.388917C37.4173 -0.107719 38.2755 -0.111192 39.1479 0.388917C40.6434 1.2398 42.1461 2.0872 43.6523 2.92419C44.5639 3.42777 44.9948 4.17793 44.9912 5.19552C44.9805 6.88685 44.9912 8.58167 44.9912 10.273C44.9912 11.0197 44.7348 11.6587 44.1402 12.1449C44.0084 12.2526 43.8624 12.3464 43.7129 12.4297C42.1817 13.2945 40.6506 14.1523 39.1194 15.0171C38.2648 15.4998 37.4173 15.4998 36.5663 15.0136C36.0891 14.7427 35.6084 14.4753 35.1313 14.2044C35.0601 14.1662 34.9888 14.1384 34.9888 14.0308C34.9924 12.9715 34.9888 11.9157 34.9888 10.8113V10.8078Z" fill="#2E3144"/>
<path d="M10.7145 11.4608V0.0486193C10.743 0.0486193 10.768 0.0416734 10.7822 0.0486193C11.3448 0.36466 11.9145 0.670282 12.4629 1.00369C13.1929 1.44823 13.5632 2.11851 13.5668 2.94855C13.5774 5.91448 13.5774 8.88387 13.5668 11.8498C13.5632 12.7493 13.1252 13.43 12.324 13.8815C11.505 14.3434 10.6825 14.7983 9.86706 15.2672C9.71039 15.3575 9.66053 15.3262 9.57507 15.1838C7.39585 11.4955 5.21306 7.81072 3.03027 4.12937C2.98754 4.05643 2.94125 3.9835 2.86647 3.85847V15.3123C2.79881 15.3506 2.76677 15.3123 2.73472 15.295C2.17211 14.9755 1.60237 14.6733 1.05757 14.333C0.409496 13.9301 0.0818991 13.3258 0.0142433 12.5826C0.00356083 12.4819 0.00712166 12.3812 0.00712166 12.2804C0 9.41176 0.00712166 6.53961 0 3.66746C0 2.67072 0.4273 1.93792 1.31751 1.44823C2.11869 1.00716 2.91276 0.559147 3.70326 0.100714C3.84926 0.0173625 3.90623 0.0277815 3.99169 0.173646C6.17448 3.86542 8.36083 7.55719 10.5472 11.249C10.5899 11.3219 10.6362 11.3948 10.6825 11.4678C10.6932 11.4643 10.7074 11.4608 10.7181 11.4573L10.7145 11.4608Z" fill="#2E3144"/>
<path d="M29.2664 7.69613C29.2664 8.5609 29.2664 9.4222 29.2664 10.287C29.27 11.2351 28.8498 11.9436 28.013 12.4159C26.4534 13.298 24.8937 14.1802 23.327 15.0519C22.5365 15.4895 21.7282 15.493 20.9377 15.0519C19.3709 14.1802 17.8148 13.2946 16.2516 12.4159C15.4112 11.9436 14.9946 11.2281 14.9946 10.287C14.9946 8.5609 14.9946 6.83483 14.9946 5.10877C14.9946 4.16064 15.4148 3.44174 16.2623 2.96594C17.8077 2.0977 19.3495 1.22598 20.8949 0.357734C21.7068 -0.0972257 22.5258 -0.111118 23.3377 0.340369C24.908 1.21903 26.4783 2.10117 28.0415 2.98678C28.8569 3.44868 29.2664 4.14675 29.2664 5.07056C29.2664 5.94575 29.2664 6.82442 29.2664 7.69961V7.69613ZM17.8504 7.68919C17.8326 9.96399 19.759 11.8706 22.1234 11.8672C24.4771 11.8672 26.4035 10.0022 26.4107 7.69961C26.4178 5.35187 24.4593 3.52162 22.1377 3.51814C19.7946 3.51814 17.8504 5.36924 17.8504 7.68919Z" fill="#2E3144"/>
<path d="M46.4083 7.6892C46.4083 6.8279 46.4083 5.96313 46.4083 5.10183C46.4083 4.15718 46.8214 3.44522 47.6618 2.96942C49.2214 2.09076 50.781 1.20862 52.3442 0.336907C53.1419 -0.107634 53.9537 -0.111107 54.7514 0.336907C56.3395 1.22252 57.9205 2.11854 59.5051 3.0111C59.6618 3.09792 59.8042 3.20558 59.9395 3.32019C60.0178 3.38618 60.0392 3.42785 59.9324 3.49731C59.0279 4.10508 58.127 4.7198 57.2297 5.33799C57.1193 5.4144 57.0908 5.35188 57.041 5.2859C56.2932 4.28221 55.2891 3.70917 54.0285 3.54594C52.1698 3.30977 50.222 4.44891 49.5632 6.16109C48.7442 8.28655 49.6701 10.5162 51.7674 11.4956C53.5585 12.3291 55.7983 11.763 56.9911 10.1689C57.0908 10.0369 57.1478 10.0196 57.2867 10.1168C58.1342 10.7107 58.9888 11.2941 59.8469 11.8741C60.0072 11.9818 59.9929 12.0408 59.854 12.1416C59.7259 12.2318 59.5977 12.3256 59.4624 12.402C57.9063 13.2772 56.3537 14.1524 54.7977 15.0276C53.9644 15.4964 53.1348 15.4999 52.3015 15.0276C50.7597 14.1594 49.2143 13.2911 47.6724 12.4229C46.825 11.9471 46.4048 11.2316 46.4048 10.28C46.4048 9.41874 46.4048 8.55397 46.4048 7.69267L46.4083 7.6892Z" fill="#2E3144"/>
<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1007_3463)">
<path d="M21.8536 12.3231C22.9134 12.3231 23.8351 12.2642 24.7464 12.3364C26.3737 12.4645 27.2042 11.1349 27.26 9.99516C27.2997 9.18825 27.3276 8.3725 27.26 7.56853C27.1792 6.60407 27.8745 6.18736 28.4771 5.76623C28.9931 5.40548 29.4811 5.73237 29.4664 6.36847C29.4223 8.29888 29.7472 10.2632 28.9137 12.117C31.0069 12.6471 32.1872 11.7842 32.2505 9.72128C32.2696 9.09696 32.3225 8.46085 32.2343 7.8483C32.0946 6.87206 32.6958 6.33461 33.3294 5.84722C33.9203 5.39223 34.4789 5.68966 34.4818 6.42001C34.4877 7.59651 34.4656 8.77301 34.4377 9.94952C34.4186 10.7211 34.2187 11.4617 33.8821 12.1744C35.355 12.435 36.378 11.5972 36.5089 10.0688C36.5647 9.41059 36.6132 8.73031 36.5059 8.08684C36.3295 7.02225 36.9778 6.46271 37.648 5.88403C37.9229 5.64696 38.2581 5.60132 38.5741 5.74856C38.8622 5.88403 38.7608 6.19619 38.752 6.43621C38.7005 7.86597 38.8872 9.30163 38.5991 10.727C38.4977 11.2306 38.2728 11.6914 38.1596 12.1921C39.4355 12.3923 40.4012 11.7003 40.5526 10.403C40.6541 9.53134 40.6746 8.6346 40.6026 7.76143C40.5203 6.77193 41.1494 6.26834 41.7962 5.81188C42.3739 5.40401 42.8663 5.70292 42.8619 6.40528C42.8546 7.76437 42.9619 9.12641 42.7664 10.4811C42.4209 12.8827 40.757 14.4067 38.361 14.414C31.6037 14.4332 24.8479 14.4258 18.0906 14.414C16.2781 14.4111 14.967 13.5335 14.0277 12.011C13.4809 11.1231 13.644 10.1616 13.6778 9.21917C13.6925 8.80688 14.3481 8.72442 14.7185 9.11315C15.1492 9.56373 15.4579 10.085 15.6696 10.699C16.1091 11.9742 17.2586 12.5322 18.5712 12.2171C19.4988 11.9933 20.3646 10.8801 20.3293 9.95688C20.2925 9.00861 19.2636 7.78351 18.3287 7.75554C16.459 7.69958 14.5833 7.70547 12.7136 7.75848C11.8728 7.78204 11.2392 8.55362 11.1878 9.49747C11.1422 10.3412 11.1672 11.1893 11.1687 12.0345C11.1687 12.5146 11.1363 12.9887 11.0261 13.4569C10.6557 15.034 9.33859 16.1368 7.71725 16.1795C6.68977 16.206 5.66082 16.2075 4.63333 16.1781C2.38874 16.1118 0.483709 14.3021 0.376404 12.061C0.299968 10.484 0.339656 8.89965 0.348475 7.31969C0.351415 6.74984 1.17311 5.74709 1.69934 5.60426C2.1903 5.47027 2.41814 5.64107 2.42255 6.21092C2.43284 7.90279 2.43872 9.59465 2.41226 11.2865C2.40197 11.9418 2.58571 12.5204 2.92527 13.0564C3.36478 13.75 3.99391 14.1667 4.82589 14.1961C5.55939 14.2211 6.29436 14.2197 7.02933 14.2005C8.33757 14.1667 9.02991 13.4496 9.05196 12.1111C9.06518 11.3763 9.02991 10.6401 9.05048 9.90387C9.06371 9.42826 9.1034 8.94824 9.19454 8.48146C9.49734 6.93096 10.763 5.77801 12.3417 5.72942C14.2849 5.66905 16.2341 5.64696 18.1758 5.72206C20.2822 5.80452 21.9859 7.35944 22.4034 9.42973C22.5151 9.98044 22.4548 10.5282 22.2814 11.0612C22.1579 11.4382 22.0271 11.8137 21.8521 12.3276L21.8536 12.3231Z" fill="#3F415A"/>
<path d="M54.2581 32.2824C57.7801 32.2824 61.1565 32.3074 64.5315 32.2706C65.9 32.2559 66.8084 31.2958 66.8951 29.8587C66.9466 29.0179 66.8834 28.1698 66.8775 27.3261C66.8731 26.6208 67.5904 25.7211 68.2739 25.5665C68.787 25.4501 69.006 25.6077 69.0045 26.1775C68.9986 27.6853 68.956 29.1917 68.9472 30.6995C68.9442 31.2296 68.5371 31.6404 68.5503 32.2397C69.6454 32.3354 70.7405 32.3722 71.8194 32.2485C72.9395 32.1204 73.7715 31.1368 73.8332 29.9588C73.8803 29.0783 73.8421 28.1948 73.8523 27.3113C73.8597 26.6001 74.4668 25.7932 75.1327 25.5885C75.6795 25.4207 75.9779 25.6077 75.9837 26.182C75.997 27.5425 76.0249 28.9045 75.9632 30.2636C75.9117 31.3989 75.3958 32.3972 74.6696 33.2439C74.1096 33.8962 73.4011 34.2614 72.4559 34.2584C63.9744 34.2305 55.4943 34.2437 47.0128 34.2364C46.7115 34.2364 46.2984 34.4042 46.1323 34.0258C45.9368 33.5811 46.2617 33.2896 46.566 32.9965C47.0613 32.518 47.5729 32.2058 48.3167 32.2809C49.0428 32.3545 49.7836 32.3045 50.5171 32.2839C51.258 32.2633 51.8607 31.9349 52.3134 31.3533C52.8411 30.6774 53.1277 29.9456 52.7029 29.1063C52.2781 28.2684 51.6372 27.6912 50.6612 27.6456C49.8557 27.6088 49.0457 27.6485 48.2387 27.6397C48.0021 27.6368 47.6875 27.7457 47.5832 27.4174C47.4935 27.1361 47.467 26.7989 47.6934 26.581C48.2123 26.0848 48.665 25.4501 49.5396 25.5724C49.8645 25.618 50.1996 25.5782 50.5304 25.5812C53.8495 25.6047 55.803 28.6586 54.4683 31.7361C54.411 31.8672 54.3654 32.0012 54.2581 32.2824Z" fill="#3F415A"/>
<path d="M21.6786 35.94C19.0724 35.94 16.4677 35.9445 13.8615 35.94C11.6581 35.9356 10.0235 34.5633 9.64575 32.387C9.56491 31.9202 9.50317 31.4328 9.54139 30.9631C9.68103 29.202 9.67662 27.4336 9.59871 25.6784C9.55462 24.6712 10.0897 24.1927 10.7585 23.7392C11.2891 23.3784 11.6522 23.5875 11.6566 24.2354C11.6684 26.073 11.6522 27.9107 11.6566 29.7483C11.6581 30.3726 11.6713 30.997 11.711 31.6198C11.7463 32.1808 11.9638 32.6829 12.2946 33.1409C12.7532 33.777 13.3044 34.098 14.1423 34.0906C19.1327 34.0523 24.1231 34.0568 29.1136 34.0685C30.2983 34.0715 31.1612 33.6283 31.661 32.5224C32.1063 31.5359 31.9138 30.6539 31.245 29.8602C30.6291 29.1284 29.5678 28.8251 28.7681 29.1093C27.6951 29.4892 27.0836 30.2975 27.0307 31.4093C26.991 32.2353 26.1796 33.1394 25.4284 33.1747C25.1844 33.1865 24.8508 33.2336 24.8405 32.8611C24.8037 31.5035 24.7229 30.1518 25.6019 28.9488C27.0733 26.9344 29.7471 26.3896 31.8594 27.7281C33.9467 29.0504 34.6332 31.7877 33.3631 33.9242C32.5782 35.245 31.392 35.9769 29.825 35.9827C27.11 35.9931 24.3936 35.9857 21.6786 35.9857C21.6786 35.971 21.6786 35.9562 21.6786 35.9415V35.94Z" fill="#3F415A"/>
<path d="M54.8078 8.86716C54.949 8.48873 55.0621 8.09705 55.2371 7.7363C55.8265 6.52151 56.7467 5.76614 58.149 5.72785C59.5807 5.68957 60.5876 6.40372 61.3152 7.56991C61.6519 8.10736 61.8077 8.71844 61.8459 9.35012C61.8959 10.1938 61.962 11.039 61.9503 11.8828C61.9429 12.4349 62.159 12.5837 62.6661 12.5586C63.3261 12.5262 63.9905 12.5896 64.6476 12.5424C66.0455 12.4423 66.9319 11.497 66.9686 10.0952C66.9907 9.25 66.9657 8.40333 66.9774 7.55813C66.9877 6.90583 67.7227 5.92075 68.3254 5.71607C68.8134 5.55116 69.0956 5.72932 69.1029 6.26825C69.1279 7.88502 69.125 9.50179 68.9912 11.1171C68.9515 11.5956 68.5899 11.9785 68.6046 12.5513C69.613 12.5513 70.5993 12.6116 71.5768 12.5365C72.9468 12.432 73.8156 11.4425 73.8626 10.0319C73.8905 9.18668 73.8655 8.34001 73.8817 7.49481C73.8935 6.87491 74.5961 5.92664 75.1605 5.73227C75.7044 5.54527 75.9719 5.72638 75.9749 6.34187C75.9793 7.88502 76.0631 9.437 75.8455 10.9684C75.6574 12.2906 75.0209 13.3803 73.8979 14.1607C73.5701 14.3889 73.2246 14.4198 72.8542 14.4198C68.7046 14.4257 64.5564 14.4228 60.4083 14.4493C59.8247 14.4537 59.6689 14.2741 59.691 13.6954C59.7409 12.3363 59.7189 10.9743 59.7115 9.6137C59.7101 9.28534 59.7013 8.94667 59.6219 8.63156C59.4705 8.03816 58.9839 7.63912 58.4562 7.60819C57.9329 7.57727 57.2715 8.01165 57.1009 8.56383C56.9378 9.09097 56.8981 9.6402 56.9025 10.1983C56.9157 11.9638 56.8922 13.7278 56.9143 15.4933C56.9201 15.9968 56.7937 16.2604 56.2425 16.2059C55.9515 16.1765 55.6516 16.175 55.3635 16.2148C54.7843 16.2928 54.6888 15.9321 54.6535 15.4933C54.48 13.2801 54.3757 11.067 54.8078 8.86863V8.86716Z" fill="#3F415A"/>
<path d="M0.0367746 31.7038C0.0367746 30.5641 0.0191354 29.4244 0.0397144 28.2862C0.111741 24.2369 4.32604 22.6864 7.06894 24.3738C8.68734 25.3692 9.5546 27.3467 9.11362 29.2109C8.63442 31.2385 7.10863 32.5887 5.11099 32.7271C4.5627 32.7654 4.01001 32.7463 3.46025 32.7419C3.25005 32.7404 2.97811 32.8081 2.87081 32.5755C2.74439 32.3031 2.73116 31.9673 2.96929 31.7524C3.47054 31.2988 3.86007 30.6848 4.69647 30.7305C5.43731 30.7717 6.03557 30.3844 6.4839 29.8028C7.60105 28.3539 7.18065 26.6635 5.51669 25.8802C4.04969 25.1896 2.26813 26.3675 2.17259 28.1271C2.11967 29.1166 2.15054 30.112 2.14907 31.103C2.1476 32.5357 2.13878 33.9699 2.15789 35.4026C2.16377 35.8576 2.03148 36.1079 1.53905 36.0579C1.46702 36.0505 1.39205 36.0579 1.31856 36.0579C-0.29984 36.0829 0.0353046 36.0814 0.0147256 34.7886C-0.00144371 33.7593 0.0117857 32.7301 0.0117857 31.7023H0.0367746V31.7038Z" fill="#3F415A"/>
<path d="M53.5321 11.8151C53.5321 12.9916 53.538 14.1681 53.5306 15.3446C53.5247 16.156 53.5159 16.1692 52.6663 16.1707C51.3272 16.1722 51.4683 16.3032 51.4609 15.0207C51.4477 12.8517 51.4271 10.6828 51.4418 8.51532C51.4477 7.60239 51.1243 6.87058 50.454 6.27717C49.6809 5.59542 48.5284 5.51885 47.7038 6.08575C46.7116 6.7675 46.253 7.98082 46.6601 8.96001C47.1026 10.0217 47.8655 10.6916 49.0576 10.7947C49.7749 10.8565 50.4599 11.4382 50.5893 12.0978C50.6569 12.438 50.6613 12.787 50.1645 12.8032C48.9841 12.84 47.817 12.8105 46.7336 12.2377C45.005 11.3248 44.1598 9.49304 44.5082 7.38299C44.8066 5.57333 46.3823 4.03755 48.2021 3.78134C51.0846 3.37493 53.4307 5.35541 53.5071 8.28415C53.538 9.45918 53.5115 10.6372 53.5115 11.8137C53.5189 11.8137 53.5262 11.8137 53.5336 11.8137L53.5321 11.8151Z" fill="#3F415A"/>
<path d="M36.9746 29.9427C36.9746 31.7435 36.9614 33.5443 36.9834 35.3452C36.9893 35.8473 36.8585 36.0711 36.3102 36.0696C34.2479 36.0637 34.6506 36.2316 34.6359 34.4882C34.6109 31.5845 34.6256 28.6808 34.6242 25.7771C34.6242 24.8686 35.2166 23.963 36.0191 23.642C36.6218 23.4005 36.9761 23.6361 36.9805 24.3178C36.9908 26.1923 36.9849 28.0668 36.9849 29.9412H36.9775L36.9746 29.9427Z" fill="#3F415A"/>
<path d="M46.0442 29.1549C46.0442 30.0722 46.0706 30.9925 46.0383 31.9084C45.9854 33.3588 45.3371 34.5132 44.1774 35.3687C43.6717 35.7412 43.1161 36.0372 42.4664 36.046C41.4389 36.0607 40.4129 36.0578 39.3854 36.0534C39.1796 36.0534 38.9238 36.1079 38.84 35.8207C38.7724 35.5896 38.8474 35.3834 39.0252 35.2259C39.6191 34.6972 40.1321 34.1421 41.0817 34.1451C43.0191 34.1509 43.8569 33.2174 43.8672 31.2723C43.8731 30.0604 43.9495 28.8398 43.8422 27.6367C43.7467 26.5663 44.4346 26.1054 45.1299 25.6666C45.6958 25.3088 46.078 25.593 46.0883 26.288C46.103 27.2436 46.0927 28.1978 46.0927 29.1534C46.078 29.1534 46.0633 29.1534 46.0471 29.1534L46.0442 29.1549Z" fill="#3F415A"/>
<path d="M36.6439 3.77984C35.6885 3.77984 34.733 3.79603 33.779 3.77542C32.9191 3.75628 32.8706 3.6856 32.8677 2.79476C32.8657 2.14981 33.1847 1.82096 33.8246 1.8082C34.0084 1.80526 34.195 1.78317 34.3758 1.80673C35.7693 1.98048 36.9747 1.73163 37.8375 0.4712C38.1198 0.0589084 39.6911 -0.126623 40.1086 0.0942477C40.2967 0.194376 40.3276 0.371072 40.3291 0.559548C40.3364 1.44303 40.3394 2.32504 40.3511 3.20852C40.357 3.64142 40.1262 3.77984 39.7293 3.77689C38.7004 3.76806 37.6729 3.77395 36.6439 3.77395V3.77984Z" fill="#3F415A"/>
<path d="M67.739 17.8287C67.9345 17.2868 67.4729 16.6478 67.8668 16.2532C68.2564 15.86 68.9164 16.1044 69.4558 16.0971C71.0301 16.0779 72.6059 16.0838 74.1802 16.0941C75.1886 16.1 75.193 16.1103 75.2003 17.1528C75.2048 17.8125 75.2003 18.4736 75.1886 19.1333C75.1856 19.3512 75.1739 19.6472 74.9078 19.6516C74.1008 19.6678 73.1939 20.0551 72.5677 19.204C71.7725 18.1217 70.7376 17.7492 69.4235 17.9288C68.8987 17.9995 68.3358 18.0319 67.739 17.8302V17.8287Z" fill="#3F415A"/>
<path d="M64.0935 3.77691C63.4335 3.77691 62.772 3.76365 62.112 3.78132C61.6534 3.7931 61.4167 3.6223 61.455 3.1408C61.4638 3.03184 61.4491 2.91993 61.4476 2.80949C61.4285 1.84944 61.4461 1.82 62.4104 1.80969C63.5849 1.79644 64.7594 1.79791 65.9338 1.80969C66.7114 1.81852 66.7247 1.84503 66.7555 2.61366C66.7614 2.7609 66.7482 2.90815 66.7614 3.05392C66.807 3.55898 66.6144 3.80341 66.0764 3.78132C65.4164 3.75482 64.755 3.77543 64.095 3.77396L64.0935 3.77691Z" fill="#3F415A"/>
<path d="M23.9761 17.9435C23.3161 17.9435 22.6561 17.9243 21.9975 17.9494C21.5213 17.967 21.3228 17.7697 21.3522 17.3029C21.364 17.1204 21.361 16.9348 21.3463 16.7522C21.3067 16.2811 21.5272 16.0837 21.9858 16.0852C23.341 16.0911 24.6963 16.0896 26.0531 16.0852C26.4647 16.0837 26.6587 16.2752 26.6469 16.686C26.641 16.8686 26.6337 17.0541 26.6469 17.2367C26.6851 17.7417 26.4867 17.9773 25.9546 17.9538C25.2961 17.9258 24.6361 17.9464 23.9761 17.9464V17.9435Z" fill="#3F415A"/>
<path d="M64.7551 23.6361C64.0622 23.6371 63.7134 23.3352 63.7085 22.7306C63.7011 21.7263 63.7702 21.663 64.8682 21.6689C65.7987 21.6733 65.8457 21.7234 65.8531 22.6761C65.858 23.3141 65.492 23.6342 64.7551 23.6361Z" fill="#3F415A"/>
<path d="M72.0825 37.0355C72.0825 37.6746 71.7552 37.9961 71.1006 38C70.0481 38.0059 70.0467 38.0044 70.0496 37.0031C70.0525 36.0446 70.0628 36.0343 71.0256 36.0328C72.0678 36.0313 72.084 36.046 72.084 37.037L72.0825 37.0355Z" fill="#3F415A"/>
</g>
<defs>
<clipPath id="clip0_2855_3816">
<rect width="60" height="16" fill="white"/>
<clipPath id="clip0_1007_3463">
<rect width="76" height="38" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 157.92 68.92">
<defs>
<style>
.cls-1 {
fill: #40415a;
stroke-width: 0px;
}
</style>
</defs>
<g>
<g>
<path class="cls-1" d="M42.01,27.03h-13.48v19.64h4.26v-4.96h9.22c2.63,0,4.76-2.14,4.76-4.76v-5.16c0-2.62-2.14-4.76-4.76-4.76ZM42.51,36.95c0,.28-.23.51-.51.51h-9.22v-6.17h9.22c.28,0,.51.23.51.51v5.16Z"/>
<path class="cls-1" d="M21.74,27.03h-8.72c-2.62,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM22.25,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.51.23.51.51v5.67Z"/>
<path class="cls-1" d="M62.28,27.03h-8.72c-2.62,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM62.78,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.5.23.5.51v5.67Z"/>
<path class="cls-1" d="M102.81,27.03h-8.72c-2.63,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM103.32,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.5.23.5.51v5.67Z"/>
<path class="cls-1" d="M143.35,27.03h-8.72c-2.63,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM143.86,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.51.23.51.51v5.67Z"/>
<path class="cls-1" d="M82.03,27.03h-12.46v19.64h12.46c2.63,0,4.76-2.14,4.76-4.76v-10.11c0-2.63-2.14-4.76-4.76-4.76ZM82.53,41.91c0,.28-.22.5-.5.5h-8.19v-11.12h8.19c.28,0,.5.22.5.5v10.11Z"/>
<polygon class="cls-1" points="123.14 39.14 114.33 27.03 114.31 27.05 114.31 27.03 110.04 27.03 110.04 46.67 114.31 46.67 114.31 34.53 123.14 46.67 127.41 46.67 127.41 27.03 123.14 27.03 123.14 39.14"/>
</g>
<g>
<path class="cls-1" d="M74.98,7.74h-5.59c-1.68,0-3.05,1.37-3.05,3.05v9.55h2.73v-3.18h6.24v3.18h2.73v-9.55c0-1.68-1.37-3.05-3.05-3.05ZM75.3,14.43h-6.24v-3.64c0-.18.15-.32.32-.32h5.59c.18,0,.32.15.32.32v3.64Z"/>
<polygon class="cls-1" points="88.55 15.51 82.9 7.74 82.88 7.75 82.88 7.74 80.14 7.74 80.14 20.34 82.88 20.34 82.88 12.55 88.55 20.34 91.29 20.34 91.29 7.74 88.55 7.74 88.55 15.51"/>
<path class="cls-1" d="M16.91,7.74h-8.65v12.6h2.73v-3.18h5.92c1.68,0,3.05-1.37,3.05-3.05v-3.31c0-1.68-1.37-3.05-3.05-3.05ZM17.24,14.1c0,.18-.15.32-.32.32h-5.92v-3.96h5.92c.18,0,.32.15.32.32v3.31Z"/>
<path class="cls-1" d="M43.58,7.74h-8.65v12.6h2.73v-3.65h5.91c.18,0,.32.15.32.32v3.33h2.73v-3.33c0-.62-.18-1.19-.52-1.68.32-.48.52-1.08.52-1.7v-2.84c0-1.69-1.37-3.05-3.05-3.05ZM43.9,13.64c0,.18-.14.32-.32.32h-5.91v-3.49h5.91c.18,0,.32.14.32.32v2.84Z"/>
<polygon class="cls-1" points="140.08 10.47 140.08 12.68 146.75 12.68 146.75 15.41 140.08 15.41 140.08 20.34 137.34 20.34 137.34 7.75 148.11 7.75 148.11 10.47 140.08 10.47"/>
<polygon class="cls-1" points="21.74 7.74 21.74 20.34 32.5 20.34 32.5 17.61 24.48 17.61 24.48 15.41 31.14 15.41 31.14 12.67 24.48 12.67 24.48 10.47 32.5 10.47 32.5 7.74 24.48 7.74 21.74 7.74"/>
<polygon class="cls-1" points="127.54 7.74 124.79 7.74 124.79 20.34 135.56 20.34 135.56 17.61 127.54 17.61 127.54 7.74"/>
<rect class="cls-1" x="61.62" y="7.65" width="2.75" height="12.69"/>
<path class="cls-1" d="M56.4,20.34h-7.45v-2.7h7.45c.08,0,.14-.06.14-.14v-1.99s-.13-.14-.13-.14h-5.16c-1.56,0-2.84-1.27-2.84-2.84v-1.99c0-.76.29-1.47.83-2.01.54-.54,1.25-.84,2.01-.84h7.42v2.7h-7.42l-.14.14v1.99c0,.08.07.14.14.14h5.16c.76,0,1.47.3,2.01.84.54.54.83,1.25.83,2.01v1.99c0,1.56-1.28,2.84-2.85,2.84Z"/>
<path class="cls-1" d="M106.1,20.34h-5c-1.76,0-3.19-1.43-3.19-3.19v-6.26c0-1.76,1.43-3.19,3.19-3.19h7.16v2.7h-7.16c-.27,0-.49.22-.49.49v6.26c0,.27.22.49.49.49h5c.12,0,.22-.1.22-.22v-1.83h-2.56v-2.7h5.27v4.54c0,1.61-1.31,2.92-2.92,2.92Z"/>
<path class="cls-1" d="M119.03,20.34h-4.97c-1.76,0-3.19-1.43-3.19-3.19V7.72h2.7v9.43c0,.27.22.49.49.49h4.97c.27,0,.49-.22.49-.49V7.72h2.7v9.43c0,1.76-1.43,3.19-3.19,3.19Z"/>
</g>
</g>
<g>
<path class="cls-1" d="M14.9,53.53h-6.22v9.18h1.92v-2.33h4.3c1.2,0,2.18-.99,2.18-2.21v-2.42c0-1.22-.98-2.21-2.18-2.21ZM15.16,58.16c0,.14-.12.26-.26.26h-4.3v-2.95h4.3c.14,0,.26.12.26.26v2.42Z"/>
<path class="cls-1" d="M43.32,53.53h-6.22v9.18h1.93v-2.67h4.29c.14,0,.26.12.26.26v2.41h1.93v-2.41c0-.44-.12-.85-.37-1.21h0s0-.03,0-.03c.24-.36.37-.8.37-1.23v-2.08c0-1.22-.98-2.21-2.18-2.21ZM43.58,57.82c0,.15-.11.26-.26.26h-4.29v-2.6h4.29c.15,0,.26.11.26.26v2.08Z"/>
<path class="cls-1" d="M111.47,53.53h-4.04c-1.2,0-2.18.99-2.18,2.21v6.97h1.92v-2.33h4.55v2.33h1.93v-6.97c0-1.22-.98-2.21-2.18-2.21ZM111.73,58.43h-4.55v-2.69c0-.14.12-.26.26-.26h4.04c.14,0,.26.12.26.26v2.69Z"/>
<path class="cls-1" d="M52.18,53.53h-3.56c-1.2,0-2.18,1-2.18,2.22v4.75c0,1.22.98,2.21,2.18,2.21h3.56c1.2,0,2.18-.99,2.18-2.21v-4.75c0-1.22-.98-2.22-2.18-2.22ZM52.44,60.5c0,.15-.11.26-.26.26h-3.56c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h3.56c.15,0,.26.11.26.26v4.75Z"/>
<path class="cls-1" d="M55.57,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
<path class="cls-1" d="M95.94,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.12-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
<polygon class="cls-1" points="18.38 62.71 26.1 62.71 26.1 60.76 20.31 60.76 20.31 59.09 25.12 59.09 25.12 57.14 20.31 57.14 20.31 55.48 26.1 55.48 26.1 53.53 18.38 53.53 18.38 62.71"/>
<polygon class="cls-1" points="73.97 62.71 81.69 62.71 81.69 60.76 75.9 60.76 75.9 59.09 80.71 59.09 80.71 57.14 75.9 57.14 75.9 55.48 81.69 55.48 81.69 53.53 73.97 53.53 73.97 62.71"/>
<polygon class="cls-1" points="117.26 53.53 115.33 53.53 115.33 62.71 123.06 62.71 123.06 60.76 117.26 60.76 117.26 53.53"/>
<polygon class="cls-1" points="27.31 55.49 30.54 55.49 30.54 62.71 32.48 62.71 32.48 55.49 35.71 55.49 35.71 53.53 27.31 53.53 27.31 55.49"/>
<polygon class="cls-1" points="70.72 57.1 66.65 57.1 66.65 53.46 64.72 53.46 64.72 62.71 66.65 62.71 66.65 59.07 70.72 59.07 70.72 62.71 72.65 62.71 72.65 53.46 70.72 53.46 70.72 57.1"/>
<rect class="cls-1" x="92.42" y="53.46" width="1.94" height="9.24"/>
<polygon class="cls-1" points="89.01 53.86 87.01 57.37 84.98 53.46 83.04 53.46 83.04 62.71 84.97 62.71 84.97 57.63 86.5 60.59 87.47 60.59 89.05 57.7 89.03 62.71 90.97 62.71 90.97 53.46 89.03 53.46 89.01 53.86"/>
<path class="cls-1" d="M142.67,53.53h-3.56c-1.2,0-2.18,1-2.18,2.22v4.75c0,1.22.98,2.21,2.18,2.21h3.56c1.2,0,2.18-.99,2.18-2.21v-4.75c0-1.22-.98-2.22-2.18-2.22ZM142.92,60.5c0,.15-.11.26-.26.26h-3.56c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h3.56c.15,0,.26.11.26.26v4.75Z"/>
<path class="cls-1" d="M127.59,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.12-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
<rect class="cls-1" x="146.18" y="60.75" width="1.93" height="1.96"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,426 +0,0 @@
<!doctype html>
<html lang="fa">
<head>
<meta charset="utf-8">
<title>IRANYekanX Family Type face: خانواده فونت ایران‌سنس</title>
<meta name="fontiran.com:license" content="کد ۵ رقمی لایسنس">
<link href="style.css" rel="stylesheet">
</head>
<body dir="rtl">
<div class="wrapper">
<div class="mainbox">
<div class="titelbox">
<h1>بِسْمِ اللهِ الرَّحْمَنِ الرَّحِيمِ</h1>
</div>
<div class="alphabet" style="line-height:200px" > د </div>
<div class="rightbox">
<br>
<span class="text-xlarge">ن وَالْقَلَمِ وَ مَا يَسْطُرُون</span>
<br>
<span class="text-xlarge"> نون؛ سوگند به قلم و آنچه می نويسند. </span>
<br>
<span class="text-large"> Noon. I swear by the pen and what the angels write </span>
<br>
<br>
</div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1>نکاتی درباره تایپوگرافی وب</h1>
</div>
<div class="alphabet" style="line-height:250px" > ج </div>
<div class="rightbox">
<p>از زمان پیدایش نخستین وب سایت، متن ها یکی از اجزای مهم صفحات وب بودند. هر چند به مرور زمان با ورود تصاویر، صوت و فیلم کمی از بار مسئولیت متون کم شد اما هنوز جایگاه خود را از دست نداده اند و بخش مهمی از کار را به عهده دارند.</p>
<p>بسیاری از طراحان وب سایت به صورت تجربی بهترین ترکیب و ظاهر را برای نمایش متن ها انتخاب می کنند. اما اصولی وجود دارد که با رعایت آن ها، تاثیرپذیری و زیبایی سایت چند برابر خواهد شد.</p>
<p>در ادامه مطلب قصد داریم تعدادی از اصول مقدماتی تایپوگرافی را به اختصار مرور کنیم. هرچند بسیاری از دوستان با این نکات آشنا هستند؛ اما شاید مرور آن ها خالی از لطف نباشد.</p>
</div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1>تاثیر اندازه فونت و سلسله مراتب تگ‌ها</h1>
</div>
<div class="alphabet" style="line-height:170px"> ن </div>
<div class="rightbox">
<p>فونت های داخل سایت باید به گونه ای قرار گیرند که کاربر به راحتی بتواند آن ها را بخواند. نوع فونت، وزن و کوچک (یا بزرگ) بودن اندازه آن ممکن است تمایل کاربر برای بازگشت به وب سایت را کاهش دهد. قواعد و قوانین زیادی برای انتخاب بهترین فونت وجود دارد.</p>
<p>در زیر می توانید ۱۲ وزن مختلف <strong>خانواده فونت ایران‌سنس</strong> را در شرایط یکسان مشاهده کنید. لازم به ذکر است برای این صفحه از وزن معمولی (Normal) استفاده شده است.</p>
</div>
</div>
<div class="mainbox">
<div class="mainbox2">
<div class="text-thin" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Thin)</div>
<div class="text-UltraLight" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (UltraLight)</div>
<div class="text-light" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Light)</div>
<div class="text-regular" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Regular)</div>
<div class="text-medium" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Medium)</div>
<div class="text-demibold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (demiBold)</div>
<div class="text-bold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Bold)</div>
<div class="text-extrabold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (ExtraBold)</div>
<div class="text-black" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Black)</div>
<div class="text-extrablack" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (ExtraBlack)</div>
<div class="text-heavy" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Heavy)</div>
</div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2">
<div class="text-thin" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Thin)</div>
<div class="text-UltraLight" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (UltraLight)</div>
<div class="text-light" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Light)</div>
<div class="text-regular" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Regular)</div>
<div class="text-medium" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Medium)</div>
<div class="text-demibold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (demiBold)</div>
<div class="text-bold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Bold)</div>
<div class="text-extrabold" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (ExtraBold)</div>
<div class="text-black" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Black)</div>
<div class="text-extrablack" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (ExtraBlack)</div>
<div class="text-heavy" style="font-size:2.2em">من نه آنم که زبونی کشم از چرخ فلک (Heavy)</div>
</div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1>تگ‌های هدینگ</h1>
</div>
<div class="alphabet" style="line-height:180px"> و </div>
<div class="rightbox">
<p>در این بین، استفاده از تگ های هدینگ مناسب و رعایت سلسله مراتب آن ها، هم مفهوم نوشته را بهتر منتقل می کند و هم تاثیر قابل توجهی در نتایج موتورهای جستجو خواهد داشت. این نکته یکی از فاکتورهای مهم در بهینه سازی وب سایت برای موتورهای جستجو (Search Engine Optimization) است.</p>
<p>نمونه ای از خروجی تگ های هدینگ با سایز استاندارد مربوطه به فونت های فونت ایران‌سنس را در زیر مشاهده می کنید.</p>
</div>
</div>
<div class="mainbox">
<div class="mainbox2">
<h1>(H1) نابرده رنج گنج میسر نمی شود. No gain without pain </h1>
<h2>(H2) نابرده رنج گنج میسر نمی شود. No gain without pain</h2>
<h3>(H3) نابرده رنج گنج میسر نمی شود. No gain without pain</h3>
<h4>(H4) نابرده رنج گنج میسر نمی شود. No gain without pain </h4>
<h5>(H5) نابرده رنج گنج میسر نمی شود. No gain without pain</h5>
<h6>(H6) نابرده رنج گنج میسر نمی شود. No gain without pain</h6>
</div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2">
<h1>(H1) نابرده رنج گنج میسر نمی شود. No gain without pain </h1>
<h2>(H2) نابرده رنج گنج میسر نمی شود. No gain without pain</h2>
<h3>(H3) نابرده رنج گنج میسر نمی شود. No gain without pain</h3>
<h4>(H4) نابرده رنج گنج میسر نمی شود. No gain without pain </h4>
<h5>(H5) نابرده رنج گنج میسر نمی شود. No gain without pain</h5>
<h6>(H6) نابرده رنج گنج میسر نمی شود. No gain without pain</h6>
</div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1>سطر و ستون بندی</h1>
</div>
<div class="alphabet" style="line-height:200px"> م </div>
<div class="rightbox">
<p>معمولاً هرگاه مقدار نوشته از یک سطر بیشتر شود ناگزیر به ستون بندی هستیم. و این کار ما را با چند متغییر مواجه خواهد کرد:</p>
<p>۱- <strong>عرض ستون‌های متنی</strong> که حاوی حداقل۷ کلمه باشد بهترین انتخاب است. اگر ستون کوتاه‌تر باشد چشم بیننده در اثر حرکت‌های زود به زود از پایان یک سطر به ابتدای سطر بعدی خسته خواهد شد. علاوه بر این لبه ستون‌هایی با عرض کم نیز همیشه دندانه ای و بی نظم خواهد بود. ستون‌هایی با عرض طولانی هم برای خواننده آزار دهنده است چرا که چشم در حرکت بازگشت از انتهای یک سطر به ابتدای سطر بعدی ممکن است دچار اشتباه شود.
<p>۲- <strong>لدینگ یا همان فاصله سطر </strong> هم در ستون بندی اهمیت دارد. ستون‌های فشرده اگرچه به لحاظ گرافیکی منسجم و زیبا هستند اما عمل خواندن را مختل می کنند و در مقابل، فاصله سطر زیاد نیز باعث نازیبایی و خستگی چشم خواننده می‌شود. فاصله سطر همیشه می تواند با توجه به نوع فونت و عرض ستون‌ها تغییر کند. بدین ترتیب که فونت‌هایی با دندانه‌های بلند‌تر و ستون‌هایی با عرض بیشتر به لدینگ بیشتری نیاز دارند.
<p>۳- <strong>همترازی </strong> هم یکی از متغییرهای پر بحث در ستون بندی است. اما به طور کوتاه و خلاصه باید گفت که بهتر است در متن فارسی از همترازی یا همان Justification استفاده نکنید. این عمل اگرچه لبه پاراگراف شما را مرتب خواهد کرد اما تنظیمات فاصله حروف را تغییر خواهد داد و در نتیجه باعث کاهش خوانایی خواهد شد. </div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Thin)</div>
<div class="farsiparagraph"><span class="text-thin">ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-thin">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX UltraLight)</div>
<div class="farsiparagraph"><span class="text-UltraLight">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-UltraLight">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Light)</div>
<div class="farsiparagraph"><span class="text-light">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-light">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Regular)</div>
<div class="farsiparagraph"><span class="text-regular">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-regular">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Mediume)</div>
<div class="farsiparagraph"><span class="text-medium">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-medium">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX DemiBold)</div>
<div class="farsiparagraph"><span class="text-demibold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-demibold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Bold)</div>
<div class="farsiparagraph"><span class="text-bold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-bold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX ExtraBold)</div>
<div class="farsiparagraph"><span class="text-extrabold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-extrabold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Black)</div>
<div class="farsiparagraph"><span class="text-black">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-black">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX ExtraBlack)</div>
<div class="farsiparagraph"><span class="text-extrablack">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-extrablack">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="mainbox2negativ">(IRANYekanX Heavy)</div>
<div class="farsiparagraph"><span class="text-heavy">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-heavy">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Thin)</div>
<div class="farsiparagraph"><span class="text-thin">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-thin">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX UltraLight)</div>
<div class="farsiparagraph"><span class="text-UltraLight">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-UltraLight">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Light)</div>
<div class="farsiparagraph"><span class="text-light">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-light">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Regular)</div>
<div class="farsiparagraph"><span class="text-regular">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-regular">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Mediume)</div>
<div class="farsiparagraph"><span class="text-medium">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-medium">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX DemiBold)</div>
<div class="farsiparagraph"><span class="text-demibold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-demibold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Bold)</div>
<div class="farsiparagraph"><span class="text-bold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-bold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX ExtraBold)</div>
<div class="farsiparagraph"><span class="text-extrabold">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-extrabold">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Black)</div>
<div class="farsiparagraph"><span class="text-black">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-black">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX ExtraBlack)</div>
<div class="farsiparagraph"><span class="text-extrablack">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-extrablack">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainboxnegativ">
<div class="mainbox2negativ">(IRANYekanX Heavy)</div>
<div class="farsiparagraph"><span class="text-heavy">
ایران‌یکان IRANYekanX از ترکیب دو فونت پر طرفدار یکان و ایران‌سنس پدید آمده است. بخشی از منحنی‌های ایران‌سنس به ساختار عمودی و افقی یکان اضافه شده است تا ایران‌یکان IRANYekanX فونتی باشد که با وجود هندسی بودن خشک و مکانیکی نباشد. این فونت با سبک‌های طراحی کمینه‌گرا Minimal سازگاری خوبی دارد و همنشین مناسبی برای فونت‌های سن‌سریف لاتین است. </span></div>
<div class="englishparagraph"><span class="text-heavy">
Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves. While some grotesks distort their letterforms to force a rigid rhythm, Roboto doesnt compromise, allowing letters to be settled into their natural width. This makes for a more natural reading rhythm more commonly found in humanist and serif types.</span></div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1>اعداد و علائم در فونت فارسی</h1>
</div>
<div class="alphabet" style="line-height:470px"> ظ </div>
<div class="rightbox">
<p><span class="text-xlarge">اعداد فارسی: <strong>۱۲۳۴۵۶۷۸۹۰</strong></span><br>
<span class="text-xlarge">اعداد عربی: <strong>۱۲۳٤٥٦۷۸۹۰</strong></span><br>
<span class="text-xlarge">اعداد انگلیسی: <strong>1234567890</strong></span></p>
<p>برای تایپ اعداد فارسی در محیط وب از <a href="http://fontiran.com/%d9%86%d8%b5%d8%a8-%da%a9%db%8c%d8%a8%d9%88%d8%b1%d8%af-%d9%81%d8%a7%d8%b1%d8%b3%db%8c-%d8%a7%d8%b3%d8%aa%d8%a7%d9%86%d8%af%d8%a7%d8%b1%d8%af-%d8%af%d8%b1-%d9%88%db%8c%d9%86%d8%af%d9%88%d8%b28-%d9%88/">کیبورد استاندارد فارسی</a> استفاده کنید. در ویندوز ۸ و یا بالاتر این کیبورد، با نام Persian(Standard)Keyboard در لیست کیبوردهای ویندوز وجود دارد. همچنین می توانید از <a href="http://persian-computing.ir/download/Iranian_Standard_Persian_Keyboard_(ISIRI_9147)_(Version_2.0).zip">این آدرس</a> آن را دانلود و نصب کنید.</p>
<span>
<p>با استفاده از کیبورد استاندارد می‌توانید ممیز فارسی را تایپ کنید.<br>
<span>میانبر این علامت کلیدهای <span class="text-medium">Shift+3</span> است. به این شکل: <strong class="text-xlarge">۳٫۱۴</strong></span><br>
<span >ممیز فارسی با علامت اسلش تفاوت دارد :<strong class="text-xlarge">۳/۱۴</strong></span>
</p>
<p>با استفاده از کیبورد استاندارد می‌توانید جداکننده هزارگان فارسی را تایپ کنید.<br>
<span>میانبر این علامت کلیدهای <strong>Shift+2</strong> است. به این شکل: <strong class="text-xlarge">۹٬۲۱۰٬۰۰۰</strong></span><br>
<span >این علامت با جدا کننده هزارگان انگلیسی تفاوت دارد : <strong class="text-xlarge">۹,۲۱۰,۰۰۰</strong></span>
</p>
</div>
</div>
<div class="mainbox">
<div class="titelbox" style="letter-spacing: 0px">
<h1> نسبت‌های طلایی</h1>
</div>
<div class="alphabet2">
<div style="padding-left:20px">
۳٫۷۷۷ &divide; ۱٫۶۱۸ = <span class="text-regular text-underline">۲٫۳۳۵</span>
<br>
۶٫۱۱۲ &divide; ۱٫۶۱۸ = <span class="text-regular text-underline">۳٫۷۷۷</span>
<br>
۹٫۸۸۹ &divide; ۱٫۶۱۸ = <span class="text-regular text-underline">۶٫۱۱۲</span>
<br>
۱۶ &divide; ۱٫۶۱۸ = <span class="text-regular text-underline">۹٫۸۸۹</span>
<br>
.......................................
<br>
۱۶ &times; ۱٫۶۱۸ = <span class="text-regular text-underline">۲۵٫۸۸۸</span>
<br>
۲۵٫۸۸۸ &times; ۱٫۶۱۸ = <span class="text-regular text-underline">۴۱٫۸۸۷</span>
<br>
۴۱٫۸۸۷ &times; ۱٫۶۱۸ = <span class="text-regular text-underline">۶۷٫۷۷۳</span>
<br>
۶۷٫۷۷۳ &times; ۱٫۶۱۸ = <span class="text-regular text-underline">۱۰۹٫۶۵۶</span>
</div>
</div>
<div class="rightbox">
<p>اهمیت اندازه فونت در خوانایی و زیبا شدن صفحه وب سایت بر کسی پوشیده نیست. در کنار این بحث، موارد دیگری مانند ارتفاع خطوط، فاصله ها، ابعاد قسمت های مختلف و ... نیز در بحث تایپوگرافی اهمیت زیادی دارند.</p>
<p>برای محاسبه این اعداد می توانیم از سری اعداد متناسب (Modular Scale) استفاده کنیم. در حقیقت از تعدادی عدد پشت سر هم که بر اساس مضرب خاصی تشکیل شده اند برای تنظیمات ارتفاع خط، فاصله ها، ابعاد و ... استفاده می کنیم. نسبت (عدد) طلایی همان مضرب اعداد است.</p>
<p>به عنوان مثال می خواهیم از سایز 16px <strong>فونت ایران‌سنس</strong> به عنوان فونت و سایز اصلی متن صفحات استفاده کنیم. عدد فی (phi) یونانی که معادل ۱٫۶۱۸۰۳۳۹۸۸۷ (به اختصار ۱٫۶۱۸) است را به عنوان نسبت طلایی در نظر می گیریم. بنابراین سری اعدادی به شکل روبرو خواهیم داشت:</p>
<p>
به کمک این اعداد و استفاده از آن ها در صفحات وب سایت خود می توانیم خوانایی و زیبایی آن را افزایش دهیم. علاوه بر آن، اگر از واحدهای نسبی مانند em استفاده شود، امکانات بیشتری در اختیار طراح و بازدیدکننده خواهد بود. البته <strong>سری اعداد بر مبنای نسبت طلایی</strong> فقط در تایپوگرافی وب سایت کاربرد ندارد.
این اعداد می تواند ادامه داشته باشد (<span class="text-regular text-underline">۱٬۲۱۵٫۹۸۱</span> ، <span class="text-regular text-underline">۱٬۹۶۷٫۴۵۷</span> ، <span class="text-regular text-underline">۳٬۱۸۳٫۳۴۵</span> و ...).
</p>
</div>
</div>
<div class="mainbox">
<div class="rightbox">
<br>
در این فایل سعی کردیم همراه با یک مطلب آموزشی کوتاه، نحوه استفاده از خانواده فونت ایران‌سنس و پیش نمایشی از قسمت های مختلف آن را مرور کنیم.
<br>
برای مشاهده راهنمای نحوه قراردادن فونت ها در وب سایت خود، به <a href="http://fontiran.com/%d9%86%d8%b5%d8%a8-%d9%81%d9%88%d9%86%d8%aa-%d8%a7%db%8c%d8%b1%d8%a7%d9%86-%d8%b3%d9%86%d8%b3-iransans-%d8%b1%d9%88%db%8c-%d9%88%d8%a8%d8%b3%d8%a7%db%8c%d8%aa/" target="_blank">این آدرس</a> مراجعه کنید.
<br>
<br>
<br>
<br>
<br>
</div>
<div class="alphabet" style="line-height:180px">ء</div>
</div>
<br>
<div class="footer">
Copyright (c) 2021 by <a href="http://fontiran.com">www.fontiran.com</a> (Moslem Ebrahimi). All rights reserved.
<br>
To use this font, it is necessary to obtain the license from www.fontiran.com
</div>
</div>
</body>
</html>

View File

@ -1,116 +0,0 @@
/**
*
* Name: IRANYekanX Fonts
* Version: 2.4
* Author: Moslem Ebrahimi (moslemebrahimi.com)
* Created on: Aug 02, 2022
* Updated on: Aug 02, 2022
* Website: http://fontiran.com
* Copyright: Commercial/Proprietary Software
--------------------------------------------------------------------------------------
فونت ایران یکان X یک نرم افزار مالکیتی محسوب می شود. جهت آگاهی از قوانین استفاده از این فونت ها لطفا به وب سایت (فونت ایران دات کام) مراجعه نمایید
--------------------------------------------------------------------------------------
IRANYekanX fonts are considered a proprietary software. To gain information about the laws regarding the use of these fonts, please visit www.fontiran.com
--------------------------------------------------------------------------------------
This set of fonts are used in this project under the license: (.....)
------------------------------------------------------------------------------------- fonts/-
*
**/
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 100;
src:
url("/font/woff/IRANYekanX-Thin.woff") format("woff"),
url("/font/woff2/IRANYekanX-Thin.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 200;
src:
url("/font/woff/IRANYekanX-UltraLight.woff") format("woff"),
url("/font/woff2/IRANYekanX-UltraLight.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 300;
src:
url("/font/woff/IRANYekanX-Light.woff") format("woff"),
url("/font/woff2/IRANYekanX-Light.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 500;
src:
url("/font/woff/IRANYekanX-Medium.woff") format("woff"),
url("/font/woff2/IRANYekanX-Medium.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 600;
src:
url("/font/woff/IRANYekanX-DemiBold.woff") format("woff"),
url("/font/woff2/IRANYekanX-DemiBold.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 800;
src:
url("/font/woff/IRANYekanX-ExtraBold.woff") format("woff"),
url("/font/woff2/IRANYekanX-ExtraBold.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 900;
src:
url("/font/woff/IRANYekanX-Black.woff") format("woff"),
url("/font/woff2/IRANYekanX-Black.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 950;
src:
url("/font/woff/IRANYekanX-ExtraBlack.woff") format("woff"),
url("/font/woff2/IRANYekanX-ExtraBlack.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: 1000;
src:
url("/font/woff/IRANYekanX-Heavy.woff") format("woff"),
url("/font/woff2/IRANYekanX-Heavy.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: bold;
src:
url("/font/woff/IRANYekanX-Bold.woff") format("woff"),
url("/font/woff2/IRANYekanX-Bold.woff2") format("woff2");
}
@font-face {
font-family: IRANYekanX;
font-style: normal;
font-weight: normal;
src:
url("/font/woff/IRANYekanX-Regular.woff") format("woff"),
url("/font/woff2/IRANYekanX-Regular.woff2") format("woff2");
}

View File

@ -1,463 +0,0 @@
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=9" />
<title>IRANYekanX</title>
<style type="text/css" media="screen">
@font-face { font-family: 'WOFF IRANYekanX-Thin'; src: url('woff/IRANYekanX-Thin.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-UltraLight'; src: url('woff/IRANYekanX-UltraLight.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Light'; src: url('woff/IRANYekanX-Light.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Regular'; src: url('woff/IRANYekanX-Regular.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Medium'; src: url('woff/IRANYekanX-Medium.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-DemiBold'; src: url('woff/IRANYekanX-DemiBold.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Bold'; src: url('woff/IRANYekanX-Bold.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-ExtraBold'; src: url('woff/IRANYekanX-ExtraBold.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Black'; src: url('woff/IRANYekanX-Black.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-ExtraBlack'; src: url('woff/IRANYekanX-ExtraBlack.woff'); }
@font-face { font-family: 'WOFF IRANYekanX-Heavy'; src: url('woff/IRANYekanX-Heavy.woff'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Thin'; src: url('woff2/IRANYekanX-Thin.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-UltraLight'; src: url('woff2/IRANYekanX-UltraLight.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Light'; src: url('woff2/IRANYekanX-Light.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Regular'; src: url('woff2/IRANYekanX-Regular.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Medium'; src: url('woff2/IRANYekanX-Medium.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-DemiBold'; src: url('woff2/IRANYekanX-DemiBold.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Bold'; src: url('woff2/IRANYekanX-Bold.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-ExtraBold'; src: url('woff2/IRANYekanX-ExtraBold.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Black'; src: url('woff2/IRANYekanX-Black.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-ExtraBlack'; src: url('woff2/IRANYekanX-ExtraBlack.woff2'); }
@font-face { font-family: 'WOFF2 IRANYekanX-Heavy'; src: url('woff2/IRANYekanX-Heavy.woff2'); }
body {
background: white;
color: black;
}
.features, .label, a, #controls {
font: normal normal normal small sans-serif;
}
.features .emojiButton {
vertical-align: -5%;
font-size: small;
}
.emojiButton {
cursor: pointer;
}
#flexbox {
display: flex;
flex-flow: column;
height: 100%;
}
#controls {
flex: 0 1 auto;
margin: 0;
padding: 0;
width: 100%;
border: 0px solid transparent;
height: auto;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
#metricsLine {
background-color: #EEE;
border-top: 1px solid #AAA;
border-bottom: 1px solid #AAA;
width: 100%;
margin: 0.2em 0;
padding: 0 0;
font-size: 2em;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
text-overflow: none;
display: none;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
#metricsLine::-webkit-scrollbar { /* WebKit */
width: 0;
height: 0;
}
#waterfall {
flex: 1 1 auto;
border: 0 solid transparent;
margin: 0;
padding: 0;
width: 100%;
color: black;
overflow-x: hidden;
overflow-y: scroll;
font-family: "WOFF IRANYekanX-Thin";
font-feature-settings: "kern" on, "liga" on, "calt" on;
-moz-font-feature-settings: "kern" on, "liga" on, "calt" on;
-webkit-font-feature-settings: "kern" on, "liga" on, "calt" on;
-ms-font-feature-settings: "kern" on, "liga" on, "calt" on;
-o-font-feature-settings: "kern" on, "liga" on, "calt" on;
}
div, p {
padding: 0;
margin: 0;
}
#waterfall p {
margin-bottom: 0.8em;
overflow-wrap: break-word;
}
.○ .sampletext {
-webkit-text-stroke: 1px black;
-webkit-text-fill-color: #FFF0;
}
.features, .label, a {
color: #888;
}
.label {
background-color: #ddd;
padding: 2px 3px;
}
span#p08 { font-size: 08pt; padding: 08pt 0; }
span#p09 { font-size: 09pt; padding: 09pt 0; }
span#p10 { font-size: 10pt; padding: 10pt 0; }
span#p11 { font-size: 11pt; padding: 11pt 0; }
span#p12 { font-size: 12pt; padding: 12pt 0; }
span#p13 { font-size: 13pt; padding: 13pt 0; }
span#p14 { font-size: 14pt; padding: 14pt 0; }
span#p15 { font-size: 15pt; padding: 15pt 0; }
span#p16 { font-size: 16pt; padding: 16pt 0; }
span#largeParagraph { font-size: 32pt; padding: 32pt 0; }
span#veryLargeParagraph { font-size: 100pt; padding: 100pt 0; }
.otFeatureLabel {
color: #666;
background-color: #ddd;
padding: 0.2em 0.5em 0.3em 0.5em;
margin: 0 .04em;
line-height: 2em;
border-radius: 0.3em;
border: 0;
text-align:center;
}
.otFeatureLabel, .otFeature {
position: relative;
opacity: 1;
pointer-events: auto;
white-space: nowrap;
}
.otFeatureLabel {
padding: 0.2em 0.5em 0.3em 0.5em;
margin: 0 .04em;
line-height: 2em;
color: #666;
background-color: #ddd;
border-radius: 0.3em;
border: 0;
text-align: center;
z-index: 6;
}
.wrapper {
width: auto;
overflow: hidden;
border: 0 solid transparent;
}
select {
float: left;
margin: 0 0.5em 0 0;
padding: 0;
}
input[type=text] {
border: 1px solid #999;
margin: 0;
width: 100%;
}
.features {
clear: left;
}
input[type=checkbox]:checked + label {
visibility: visible;
color: #fff;
background-color: #888;
}
.otFeature {
visibility: collapse;
margin: 0 -1em 0 0;
}
.otFeatureLabel .tooltip {
visibility: hidden;
background-color: #333;
color: white;
text-align: center;
padding: 0px 5px;
top: -2em;
left: 0;
position: absolute;
z-index: 8;
}
.otFeatureLabel:hover .tooltip {
visibility: visible;
}
#featureLine {
display: none;
border-bottom: 1px solid #999;
padding: 0.5em 0;
margin-bottom: 0.5em;
}
/* Footer paragraph: */
#helptext {
color: black;
background-color: #ddd;
position: fixed;
bottom: 0;
padding: 2px
width: 100%;
font: x-small sans-serif;
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
body {
background: #333;
}
.features, .label, a, body, p, #metricsLine {
color: white;
}
.label {
background-color: black;
padding: 2px 3px;
}
.otFeatureLabel, input[type=text] {
color: white;
background-color: black;
}
input[type=checkbox]:checked + label {
color: black;
background-color: #aaa;
}
#helptext {
background-color: #777;
}
.○ .sampletext {
-webkit-text-stroke: 1px white;
-webkit-text-fill-color: #0000;
}
#metricsLine {
background-color: #222;
border-color: #777;
}
}
</style>
</head>
<body onload="document.getElementById('textInput').focus();setCharset();">
<div id="flexbox">
<div id="controls">
<div>
<select size="1" id="fontFamilySelector" name="fontFamilySelector" onchange="changeFont()">
<option value="IRANYekanX-Thin.woff">WOFF IRANYekanX-Thin</option>
<option value="IRANYekanX-UltraLight.woff">WOFF IRANYekanX-UltraLight</option>
<option value="IRANYekanX-Light.woff">WOFF IRANYekanX-Light</option>
<option value="IRANYekanX-Regular.woff">WOFF IRANYekanX-Regular</option>
<option value="IRANYekanX-Medium.woff">WOFF IRANYekanX-Medium</option>
<option value="IRANYekanX-DemiBold.woff">WOFF IRANYekanX-DemiBold</option>
<option value="IRANYekanX-Bold.woff">WOFF IRANYekanX-Bold</option>
<option value="IRANYekanX-ExtraBold.woff">WOFF IRANYekanX-ExtraBold</option>
<option value="IRANYekanX-Black.woff">WOFF IRANYekanX-Black</option>
<option value="IRANYekanX-ExtraBlack.woff">WOFF IRANYekanX-ExtraBlack</option>
<option value="IRANYekanX-Heavy.woff">WOFF IRANYekanX-Heavy</option>
<option value="IRANYekanX-Thin.woff2">WOFF2 IRANYekanX-Thin</option>
<option value="IRANYekanX-UltraLight.woff2">WOFF2 IRANYekanX-UltraLight</option>
<option value="IRANYekanX-Light.woff2">WOFF2 IRANYekanX-Light</option>
<option value="IRANYekanX-Regular.woff2">WOFF2 IRANYekanX-Regular</option>
<option value="IRANYekanX-Medium.woff2">WOFF2 IRANYekanX-Medium</option>
<option value="IRANYekanX-DemiBold.woff2">WOFF2 IRANYekanX-DemiBold</option>
<option value="IRANYekanX-Bold.woff2">WOFF2 IRANYekanX-Bold</option>
<option value="IRANYekanX-ExtraBold.woff2">WOFF2 IRANYekanX-ExtraBold</option>
<option value="IRANYekanX-Black.woff2">WOFF2 IRANYekanX-Black</option>
<option value="IRANYekanX-ExtraBlack.woff2">WOFF2 IRANYekanX-ExtraBlack</option>
<option value="IRANYekanX-Heavy.woff2">WOFF2 IRANYekanX-Heavy</option>
</select>
<div class="wrapper" spellcheck="false">
<input type="text" value="Type Text Here." id="textInput" onclick="this.select();" onkeyup="updateParagraph()" />
</div>
</div>
<p class="features">
<a href="javascript:setCharset();">Charset</a>
<a href="javascript:setLat1();">Lat1</a>
&ensp;
<a href="https://caniuse.com/#feat=woff">woff</a>
<a href="https://caniuse.com/#feat=woff2">woff2</a>
&ensp;
<a onclick="toggleInverse();" id="invert" class="emojiButton">🔲</a>
<label><input type="checkbox" id="kern" value="kern" class="otFeature" onchange="updateFeatures()" checked><label for="kern" class="otFeatureLabel">kern</label>
<label><input type="checkbox" id="liga" value="liga" class="otFeature" onchange="updateFeatures()" checked><label for="liga" class="otFeatureLabel">liga/clig</label>
<label><input type="checkbox" id="calt" value="calt" class="otFeature" onchange="updateFeatures()" checked><label for="calt" class="otFeatureLabel">calt</label>
<input type="checkbox" id="numr" value="numr" class="otFeature" onchange="updateFeatures()"><label for="numr" class="otFeatureLabel">numr</label>
<input type="checkbox" id="dnom" value="dnom" class="otFeature" onchange="updateFeatures()"><label for="dnom" class="otFeatureLabel">dnom</label>
<input type="checkbox" id="frac" value="frac" class="otFeature" onchange="updateFeatures()"><label for="frac" class="otFeatureLabel">frac</label>
<input type="checkbox" id="init" value="init" class="otFeature" onchange="updateFeatures()"><label for="init" class="otFeatureLabel">init</label>
<input type="checkbox" id="medi" value="medi" class="otFeature" onchange="updateFeatures()"><label for="medi" class="otFeatureLabel">medi</label>
<input type="checkbox" id="fina" value="fina" class="otFeature" onchange="updateFeatures()"><label for="fina" class="otFeatureLabel">fina</label>
<input type="checkbox" id="rlig" value="rlig" class="otFeature" onchange="updateFeatures()"><label for="rlig" class="otFeatureLabel">rlig</label>
<input type="checkbox" id="dlig" value="dlig" class="otFeature" onchange="updateFeatures()"><label for="dlig" class="otFeatureLabel">dlig</label>
<input type="checkbox" id="salt" value="salt" class="otFeature" onchange="updateFeatures()"><label for="salt" class="otFeatureLabel">salt</label>
<input type="checkbox" id="ss01" value="ss01" class="otFeature" onchange="updateFeatures()"><label for="ss01" class="otFeatureLabel">ss01</label>
<input type="checkbox" id="ss02" value="ss02" class="otFeature" onchange="updateFeatures()"><label for="ss02" class="otFeatureLabel">ss02</label>
<input type="checkbox" id="ss03" value="ss03" class="otFeature" onchange="updateFeatures()"><label for="ss03" class="otFeatureLabel">ss03</label>
<input type="checkbox" id="ss04" value="ss04" class="otFeature" onchange="updateFeatures()"><label for="ss04" class="otFeatureLabel">ss04</label>
<label><input type="checkbox" value="show" onchange="updateFeatures();document.getElementById('featureLine').style.display=this.checked?'block':'none'">CSS</label>
<label><input type="checkbox" value="show" onchange="updateFeatures();document.getElementById('metricsLine').style.display=this.checked?'block':'none'">Metrics</label>
</p>
<p class="features" id="featureLine">font-feature-settings: "kern" on, "liga" on, "calt" on;</p>
</div>
<div id="waterfall" class="●">
<div id="metricsLine"></div>
<p><span class="label">08</span>&nbsp;<span class="sampletext" id="p08"></span></p>
<p><span class="label">09</span>&nbsp;<span class="sampletext" id="p09"></span></p>
<p><span class="label">10</span>&nbsp;<span class="sampletext" id="p10"></span></p>
<p><span class="label">11</span>&nbsp;<span class="sampletext" id="p11"></span></p>
<p><span class="label">12</span>&nbsp;<span class="sampletext" id="p12"></span></p>
<p><span class="label">13</span>&nbsp;<span class="sampletext" id="p13"></span></p>
<p><span class="label">14</span>&nbsp;<span class="sampletext" id="p14"></span></p>
<p><span class="label">15</span>&nbsp;<span class="sampletext" id="p15"></span></p>
<p><span class="label">16</span>&nbsp;<span class="sampletext" id="p16"></span></p>
<p><span class="sampletext" id="largeParagraph"></span></p>
<p><span class="sampletext" id="veryLargeParagraph"></span></p>
</div>
</div>
<!-- Disclaimer -->
<p id="helptext" onmouseleave="vanish(this);">
Ctrl-R: Reset Charset. Ctrl-L: Latin1. Ctrl-J: LTR/RTL. Ctrl-comma/period: step through fonts. Pull mouse across this note to make it disappear.
</p>
<script type="text/javascript">
const selector = document.getElementById("fontFamilySelector");
const selectorOptions = selector.options;
const selectorLength = selectorOptions.length;
document.addEventListener('keyup', keyAnalysis);
function keyAnalysis(event) {
if (event.ctrlKey) {
if (event.code == 'KeyR') {
setCharset();
} else if (event.code == 'KeyL') {
setLat1();
} else if (event.code == 'KeyJ') {
toggleLeftRight();
} else if (event.code == 'Period') {
selector.selectedIndex = (selector.selectedIndex + 1) % selectorLength;
changeFont();
} else if (event.code == 'Comma') {
var newIndex = selector.selectedIndex - 1;
if (newIndex<0) {
newIndex = selectorLength - 1;
}
selector.selectedIndex = newIndex;
changeFont();
}
}
}
function updateParagraph() {
// update paragraph text based on user input:
const txt = document.getElementById('textInput');
const paragraphs = document.getElementsByClassName('sampletext');
for (i = 0; i < paragraphs.length; i++) {
paragraph = paragraphs[i];
paragraph.textContent = txt.value;
}
// update other elements:
document.getElementById('metricsLine').textContent = txt.value;
}
function updateFeatures() {
// update features based on user input:
// first, get feature on/off line:
var cssCode = "";
var codeLine = "";
var checkboxes = document.getElementsByClassName("otFeature")
for (i = 0; i < checkboxes.length; i++) {
var checkbox = checkboxes[i];
codeLine += '"'+checkbox.id+'" ';
codeLine += checkbox.checked ? 'on, ' : 'off, ';
if (checkbox.name=="kern") {
cssCode += "font-kerning: "
cssCode += checkbox.checked ? 'normal; ' : 'none; ';
} else if (checkbox.name=="liga") {
codeLine += '"clig" '
codeLine += checkbox.checked ? 'on, ' : 'off, ';
cssCode += "font-variant-ligatures: "
cssCode += checkbox.checked ? 'common-ligatures contextual; ' : 'no-common-ligatures no-contextual; ';
} else if (checkbox.name=="dlig") {
cssCode += "font-variant-ligatures: "
cssCode += checkbox.checked ? 'discretionary-ligatures; ' : 'no-discretionary-ligatures; ';
} else if (checkbox.name=="hlig") {
cssCode += "font-variant-ligatures: "
cssCode += checkbox.checked ? 'historical-ligatures; ' : 'no-historical-ligatures; ';
}
}
codeLine = codeLine.slice(0, -2)
// then, apply line for every browser:
const prefixes = ["","-moz-","-webkit-","-ms-","-o-",];
const suffix = "font-feature-settings: "
for (i = 0; i < prefixes.length; i++) {
var prefix = prefixes[i];
cssCode += prefix
cssCode += suffix
cssCode += codeLine
cssCode += "; "
}
document.getElementById('waterfall').style.cssText = cssCode;
document.getElementById('featureLine').innerHTML = cssCode.replace(/;/g,";<br/>");
changeFont();
}
function changeFont() {
var selected_index = selector.selectedIndex;
var selected_option_text = selector.options[selected_index].text;
document.getElementById('waterfall').style.fontFamily = selected_option_text;
}
function setDefaultText(defaultText) {
document.getElementById('textInput').value = decodeEntities(defaultText);
updateParagraph();
}
function setLat1() {
const lat1 = "من نه آنم که زبونی کشم از چرخ فلک";
return setDefaultText(lat1);
}
function setCharset() {
const completeCharSet =
'من نه آنم که زبونی کشم از چرخ فلک'
setDefaultText(completeCharSet);
}
function decodeEntities(string){
var elem = document.createElement('div');
elem.innerHTML = string;
return elem.textContent;
}
function vanish(item) {
item.style.setProperty("display", "none");
}
function toggleLeftRight() {
const waterfall = document.getElementById("waterfall");
if (waterfall.dir != "rtl") {
waterfall.dir = "rtl";
waterfall.align = "right";
} else {
waterfall.dir = "";
waterfall.align = "";
}
}
function toggleInverse() {
const testText = document.getElementById("waterfall");
if (testText) {
const link = document.getElementById("invert");
if (testText.className == "●") {
testText.className = "○";
link.textContent = "🔳";
} else {
testText.className = "●";
link.textContent = "🔲";
}
}
}
</script>
</body>

Binary file not shown.

View File

@ -1,243 +0,0 @@
@import url(fontiran.css); /* لینک فایلی که وظیفه بارگذاری فونت ها را برعهده دارد */
body {
font-family: IRANYekanX !important;
direction: rtl;
background-color: #cdcdcd;
margin: 0;
}
h1, h2, h3, h4, h5, h6,input, textarea {
font-family: IRANYekanX !important;
}
h1 {
font-weight: bold;
}
.wrapper {
max-width: 900px;
margin: 0 auto;
}
.ltr {
direction: ltr;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-small {
font-size: 0.8em;
}
.text-xsmall {
font-size: 0.6em;
}
.text-large {
font-size: 1.2em;
}
.text-xlarge {
font-size: 1.4em;
}
.text-underline {
text-decoration:underline;
}
.text-thin {
font-weight: 100;
}
.text-UltraLight {
font-weight: 200;
}
.text-light {
font-weight: 300;
}
.text-regular {
font-weight: normal;
}
.text-medium {
font-weight: 500;
}
.text-demibold {
font-weight: 600;
}
.text-bold {
font-weight: bold;
}
.text-extrabold {
font-weight: 800;
}
.text-black {
font-weight: 900;
}
.text-extrablack {
font-weight: 950;
}
.text-heavy {
font-weight: 1000;
}
blockquote {
font-weight: 700;
padding: 10px;
border: 1px dashed #666666;
}
.mainbox {
width: 100%;
background-color: #EFEFEF;
display: table;
margin-bottom: 30px;
border-right: 8px solid #00adb5;
}
.mainboxnegativ {
width: 100%;
background-color: #303841;
display: table;
margin-bottom: 30px;
border-right: 8px solid #00adb5;
color: #F9F9F9;
}
.mainbox2 {
font-size: 1em;
width: 90%;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.mainboxitalic {
font-size: 1em;
font-style: italic;
width: 90%;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.mainbox3 {
width: 100%;
background-color: #DFDFDF;
display: table;
margin-bottom: 30px;
border-right: 8px solid #FF5EAA;
}
.mainbox2negativ {
font-size: 1em;
color: #F9F9F9;
background-color: #000000;
padding-right: 20px;
}
.farsiparagraph {
font-size: 1em;
width: 47%;
float:right;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.farsiparagraph_negativ {
font-size: 1em;
color: #F9F9F9;
background-color: #000000;
width: 47%;
float:right;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.englishparagraph {
font-size: 1em;
width: 47%;
float: left;
direction:ltr;
padding-left: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.englishparagraph_negativ {
font-size: 1em;
color: #F9F9F9;
background-color: #000000;
width: 47%;
float: left;
direction:ltr;
padding-left: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.rightbox {
width: 60%;
padding-right: 20px;
padding-left: 5px;
float: right;
margin-left: 10px;
margin-bottom: 0px;
min-width: 0px;
background-color: #F7F7F7;
}
.titelbox {
width: 60%;
padding-right: 25px;
padding-left: 0px;
float: right;
margin-left: 10px;
margin-bottom: 0px;
min-width: 0px;
background-color: #d5d5d5;
color: #4B4B4B;
}
.lefttbox {
padding-right: 20px;
padding-left: 4px;
float: right;
margin-bottom: 10px;
min-width: 0px;
}
.alphabet {
width: 35%;
float: left;
font-size: 20em;
text-align: center;
font-weight: 700;
color: #999999;
}
.alphabet2 {
width: 35%;
float: left;
direction: ltr;
font-size: 1.6em;
text-align: left;
font-weight: 600;
color: #333333;
margin-top: 100px;
}
.footer {
font-weight: 400;
font-size: 0.7em;
text-align: center;
direction: ltr;
margin-bottom: 0px;
padding-bottom: 0px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,5 +1,7 @@
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;