diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 94441cd..763d26f 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -6903,8 +6903,14 @@ { "resourceName": "Platform", "key": "Public.about.stats.clients", - "tr": "Mutlu Müşteri", - "en": "Happy Clients" + "tr": "Mutlu Şubeler", + "en": "Happy Branches" + }, + { + "resourceName": "Platform", + "key": "Public.about.stats.users", + "tr": "Mutlu Kullanıcılar", + "en": "Happy Users" }, { "resourceName": "Platform", diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index d21142b..8b55a01 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -369,6 +369,15 @@ "Abouts": [ { "stats": [ + { + "Icon": "FaUser", + "Value": "740\u002B", + "LabelKey": "Public.about.stats.users", + "UseCounter": true, + "CounterEnd": "740\u002B", + "CounterSuffix": "\u002B", + "CounterDuration": 3000 + }, { "icon": "FaUsers", "value": "310+", @@ -380,7 +389,7 @@ }, { "icon": "FaAward", - "value": "20", + "value": "25", "labelKey": "Public.about.stats.experience", "useCounter": true, "counterEnd": "20", diff --git a/ui/src/views/public/About.tsx b/ui/src/views/public/About.tsx index dfaacce..ee91d98 100644 --- a/ui/src/views/public/About.tsx +++ b/ui/src/views/public/About.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' +import type { FC } from 'react' import { Helmet } from 'react-helmet' import navigationIcon from '@/proxy/menus/navigation-icon.config' import { AboutDto } from '@/proxy/about/models' @@ -102,6 +103,88 @@ function resolveLocalizedValue( return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue } +function getCounterParts(value?: string, suffix?: string) { + const sourceValue = value || '0' + const numericMatch = sourceValue.replace(',', '.').match(/\d+(?:\.\d+)?/) + + if (!numericMatch) { + return null + } + + const endValue = Number(numericMatch[0]) + + if (!Number.isFinite(endValue)) { + return null + } + + const prefix = sourceValue.slice(0, numericMatch.index) + const detectedSuffix = sourceValue.slice((numericMatch.index || 0) + numericMatch[0].length) + + return { + endValue, + prefix, + suffix: suffix ?? detectedSuffix, + hasDecimals: numericMatch[0].includes('.'), + } +} + +const AnimatedStatValue: FC<{ stat: AboutStatContent }> = ({ stat }) => { + const counterParts = useMemo( + () => getCounterParts(stat.counterEnd || stat.value, stat.counterSuffix), + [stat.counterEnd, stat.counterSuffix, stat.value], + ) + const [currentValue, setCurrentValue] = useState(0) + + useEffect(() => { + if (!stat.useCounter || !counterParts) { + return + } + + let animationFrameId = 0 + let startTime: number | null = null + const duration = Math.max(stat.counterDuration || 2000, 0) + + const animate = (timestamp: number) => { + if (startTime === null) { + startTime = timestamp + } + + const elapsed = timestamp - startTime + const progress = duration === 0 ? 1 : Math.min(elapsed / duration, 1) + const easedProgress = 1 - Math.pow(1 - progress, 3) + + setCurrentValue(counterParts.endValue * easedProgress) + + if (progress < 1) { + animationFrameId = requestAnimationFrame(animate) + } + } + + setCurrentValue(0) + animationFrameId = requestAnimationFrame(animate) + + return () => { + cancelAnimationFrame(animationFrameId) + } + }, [counterParts, stat.counterDuration, stat.useCounter]) + + if (!stat.useCounter || !counterParts) { + return <>{stat.value} + } + + const formattedValue = counterParts.hasDecimals + ? currentValue.toFixed(1) + : Math.floor(currentValue).toString() + + return ( + <> + {counterParts.prefix} + {formattedValue} + {counterParts.suffix} + + ) +} + function buildAboutContent( about: AboutDto | undefined, translate: (key: string) => string, @@ -213,7 +296,7 @@ function buildAboutContent( } } -const About: React.FC = () => { +const About: FC = () => { const { translate } = useLocalization() const { setLang } = useStoreActions((actions) => actions.locale) const { getConfig } = useStoreActions((actions) => actions.abpConfig) @@ -372,7 +455,7 @@ const About: React.FC = () => { }) } - const selectedSelection: DesignerSelection | null = React.useMemo(() => { + const selectedSelection: DesignerSelection | null = useMemo(() => { if (!content || !selectedBlockId) { return null } @@ -736,7 +819,7 @@ const About: React.FC = () => { {/* Stats Section */}
-
+
{content?.stats.map((stat, index) => { const IconComponent = navigationIcon[stat.icon || ''] @@ -754,7 +837,9 @@ const About: React.FC = () => { className={`w-12 h-12 mx-auto mb-4 ${getIconColor(index)}`} /> )} -
{stat.value}
+
+ +
{stat.label}