-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(i18n): Shipped the new i18n page (#1449)
* feat(i18n): Shipped the new i18n page * feat(ui): Redesigned Quote design (blog) and Improved WordRotate Component * feat(ui): Changed the copies * feat(ui): Refactored timeline and extended animation duration * feat(ui): Improved UX * feat(ui): Improved UI * fix(ui): Fixed mobile issues * fix(ui): Fixed mobile issues * Revert "Merge branch 'main' into feature/new-i18n-page" This reverts commit 1a9ba60, reversing changes made to 7c448aa. * feat(ui): Redesigned the timeline and fixed animation tweaks * fix(ui) Remove AI from last step * feat(ui): Supported Autoplay when in view * feat(ui) Improved timline and duration of hero section animation * chore: Update pnpm lock file --------- Co-authored-by: Mohab Sameh <mohabsameh@outlook.com> Co-authored-by: Mohamad Mohebifar <mohamad@mohebifar.com>
- Loading branch information
1 parent
8e33b86
commit c6bd941
Showing
38 changed files
with
2,518 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { I18nPage } from "@/components/templates/i18nPage/Page"; | ||
import I18NPagePreview from "@/components/templates/i18nPage/PagePreview"; | ||
import { loadSanityPageByRouteProps } from "@/data/sanity"; | ||
import { resolveSanityRouteMetadata } from "@/data/sanity/resolveSanityRouteMetadata"; | ||
import type { RouteProps } from "@/types"; | ||
import type { ResolvingMetadata } from "next"; | ||
import { draftMode } from "next/headers"; | ||
import { notFound } from "next/navigation"; | ||
|
||
export async function generateMetadata( | ||
props: RouteProps, | ||
parent: ResolvingMetadata, | ||
) { | ||
const initialData = await loadSanityPageByRouteProps({ | ||
params: { | ||
path: ["i18n"], | ||
locale: "", | ||
}, | ||
}); | ||
|
||
if (!initialData?.data) return notFound(); | ||
|
||
return resolveSanityRouteMetadata(initialData.data, parent); | ||
} | ||
|
||
export default async function I18n() { | ||
const initial = await loadSanityPageByRouteProps({ | ||
params: { | ||
path: ["i18n"], | ||
locale: "", | ||
}, | ||
}); | ||
|
||
if (!initial?.data) return notFound(); | ||
|
||
if (draftMode().isEnabled) { | ||
return <I18NPagePreview initial={initial} />; | ||
} | ||
|
||
return <I18nPage data={initial.data} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
apps/frontend/components/templates/i18nPage/Cobe/Globe/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
"use client"; | ||
|
||
import { cn } from "@/utils"; | ||
import createGlobe, { type COBEOptions } from "cobe"; | ||
import { useCallback, useEffect, useRef, useState } from "react"; | ||
import { useThemeDetector } from "../../Preview/hooks"; | ||
|
||
const LIGHT_THEME_CONFIG: Partial<COBEOptions> = { | ||
dark: 0, | ||
baseColor: [255 / 255, 255 / 255, 255 / 255], // #FFF in normalized RGB | ||
markerColor: [137 / 255, 185 / 255, 0 / 255], // #89B900 in normalized RGB | ||
glowColor: [247 / 255, 255 / 255, 223 / 255], // #F7FFDF in normalized RGB | ||
}; | ||
|
||
const DARK_THEME_CONFIG: Partial<COBEOptions> = { | ||
dark: 1, | ||
baseColor: [53 / 255, 69 / 255, 85 / 255], // #354555 in normalized RGB | ||
markerColor: [214 / 255, 255 / 255, 98 / 255], // #D6FF62 in normalized RGB | ||
glowColor: [12 / 255, 22 / 255, 31 / 255], // #0C161F in normalized RGB | ||
}; | ||
|
||
const GLOBE_CONFIG: Partial<COBEOptions> = { | ||
width: 800, | ||
height: 800, | ||
onRender: () => {}, | ||
devicePixelRatio: 2, | ||
phi: 0, | ||
theta: 0.3, | ||
diffuse: 1.75, | ||
mapSamples: 16000, | ||
mapBrightness: 7, | ||
markers: [ | ||
{ location: [14.5995, 120.9842], size: 0.03 }, | ||
{ location: [19.076, 72.8777], size: 0.1 }, | ||
{ location: [23.8103, 90.4125], size: 0.05 }, | ||
{ location: [30.0444, 31.2357], size: 0.07 }, | ||
{ location: [39.9042, 116.4074], size: 0.08 }, | ||
{ location: [-23.5505, -46.6333], size: 0.1 }, | ||
{ location: [19.4326, -99.1332], size: 0.1 }, | ||
{ location: [40.7128, -74.006], size: 0.1 }, | ||
{ location: [34.6937, 135.5022], size: 0.05 }, | ||
{ location: [41.0082, 28.9784], size: 0.06 }, | ||
], | ||
}; | ||
|
||
export function Globe({ | ||
className, | ||
config = GLOBE_CONFIG, | ||
}: { | ||
className?: string; | ||
config?: Partial<COBEOptions>; | ||
}) { | ||
let phi = 0; | ||
let width = 0; | ||
const canvasRef = useRef<HTMLCanvasElement>(null); | ||
const pointerInteracting = useRef<number | null>(null); | ||
const pointerInteractionMovement = useRef(0); | ||
const [r, setR] = useState(0); | ||
const theme = useThemeDetector(); | ||
|
||
// Dynamically apply theme-based config | ||
const _config = { | ||
...(theme === "dark" ? DARK_THEME_CONFIG : LIGHT_THEME_CONFIG), | ||
...config, | ||
} as COBEOptions; | ||
|
||
const updateMovement = (clientX: number) => { | ||
if (pointerInteracting.current !== null) { | ||
const delta = clientX - pointerInteracting.current; | ||
pointerInteractionMovement.current = delta; | ||
setR(delta / 200); | ||
} | ||
}; | ||
|
||
const onRender = useCallback( | ||
(state: Record<string, any>) => { | ||
if (!pointerInteracting.current) phi += 0.005; | ||
state.phi = phi + r; | ||
state.width = width * 2; | ||
state.height = width * 2; | ||
}, | ||
[r], | ||
); | ||
|
||
const onResize = () => { | ||
if (canvasRef.current) { | ||
width = canvasRef.current.offsetWidth; | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
window.addEventListener("resize", onResize); | ||
onResize(); | ||
|
||
const globe = createGlobe(canvasRef.current!, { | ||
..._config, | ||
width: width * 2, | ||
height: width * 2, | ||
onRender, | ||
}); | ||
|
||
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 300); | ||
return () => globe.destroy(); | ||
}, [_config]); | ||
|
||
return ( | ||
<div className={cn("mx-auto aspect-[1/1] w-full max-w-[600px]", className)}> | ||
<canvas | ||
className={cn( | ||
"size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]", | ||
)} | ||
ref={canvasRef} | ||
onMouseMove={(e) => updateMovement(e.clientX)} | ||
onTouchMove={(e) => | ||
e.touches[0] && updateMovement(e.touches[0].clientX) | ||
} | ||
/> | ||
</div> | ||
); | ||
} |
60 changes: 60 additions & 0 deletions
60
apps/frontend/components/templates/i18nPage/Cobe/NumberTicker/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
"use client"; | ||
|
||
import { cn } from "@/utils"; | ||
import { useInView, useMotionValue, useSpring } from "framer-motion"; | ||
import { useEffect, useRef } from "react"; | ||
|
||
export function NumberTicker({ | ||
value, | ||
direction = "up", | ||
delay = 0, | ||
className, | ||
decimalPlaces = 0, | ||
suffix = "", | ||
}: { | ||
value: number; | ||
direction?: "up" | "down"; | ||
className?: string; | ||
delay?: number; // delay in seconds | ||
decimalPlaces?: number; | ||
suffix?: string; // suffix text | ||
}) { | ||
const ref = useRef<HTMLSpanElement>(null); | ||
const motionValue = useMotionValue(direction === "down" ? value : 0); | ||
const springValue = useSpring(motionValue, { | ||
damping: 60, | ||
stiffness: 100, | ||
}); | ||
const isInView = useInView(ref, { once: true, margin: "0px" }); | ||
|
||
useEffect(() => { | ||
if (isInView) { | ||
setTimeout(() => { | ||
motionValue.set(direction === "down" ? 0 : value); | ||
}, delay * 1000); | ||
} | ||
}, [motionValue, isInView, delay, value, direction]); | ||
|
||
useEffect(() => { | ||
const unsubscribe = springValue.on("change", (latest) => { | ||
if (ref.current) { | ||
ref.current.textContent = `${Intl.NumberFormat("en-US", { | ||
minimumFractionDigits: decimalPlaces, | ||
maximumFractionDigits: decimalPlaces, | ||
}).format(Number(latest.toFixed(decimalPlaces)))}${suffix}`; | ||
} | ||
}); | ||
|
||
return () => unsubscribe(); | ||
}, [springValue, decimalPlaces, suffix]); | ||
|
||
return ( | ||
<span | ||
className={cn( | ||
"inline-block tabular-nums tracking-wider text-black dark:text-white", | ||
className, | ||
)} | ||
ref={ref} | ||
/> | ||
); | ||
} |
Oops, something went wrong.