Skip to content

Commit

Permalink
feat(i18n): Shipped the new i18n page (#1449)
Browse files Browse the repository at this point in the history
* 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
3 people authored Jan 23, 2025
1 parent 8e33b86 commit c6bd941
Show file tree
Hide file tree
Showing 38 changed files with 2,518 additions and 169 deletions.
41 changes: 41 additions & 0 deletions apps/frontend/app/(website)/i18n/page.tsx
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} />;
}
2 changes: 1 addition & 1 deletion apps/frontend/components/global/Navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function Navigation({ data }: NavigationProps) {
ref={headerRef}
id={"desktop-navigation"}
className={cx(
"left-0 top-0 z-40 w-full border-b-[1px] transition-all duration-150 ease-out z-10000",
"left-0 top-0 z-[80] w-full border-b-[1px] transition-all duration-150 ease-out z-10000",
isStaticHeader ? "absolute" : "fixed",
{
"-translate-y-10 opacity-0": !hasMounted,
Expand Down
130 changes: 100 additions & 30 deletions apps/frontend/components/shared/pt.blocks/Quote.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,110 @@
"use client";
import TextRotate from "@/components/templates/i18nPage/Cobe/TextRotate";
import type { QuoteProps } from "@/types/object.types";
import { LayoutGroup, motion, useInView } from "motion/react";
import { useRef } from "react";
import { SanityImage } from "../SanityImage";

export const Quote = (props: QuoteProps) => {
const blockquoteRef = useRef<HTMLQuoteElement>(null);
const inView = useInView(blockquoteRef, {
once: true,
margin: "0% 0%",
});

return (
<blockquote className="my-10">
{props.image && (
<SanityImage
maxWidth={450}
image={props.image}
elProps={{ className: "mb-6 block h-10 w-auto" }}
alt={props.authorImage?.alt}
/>
)}
<q className="body-l-medium lg:m-heading mb-8 block font-medium">
{props.quote}
</q>
<div className="flex items-center gap-3">
{props.authorImage && (
<SanityImage
elProps={{ className: "rounded-full w-8 h-8" }}
maxWidth={100}
image={props.authorImage}
alt={props.authorImage?.alt}
/>
<blockquote
ref={blockquoteRef}
className="my-6 py-4 border-y border-black/10 dark:border-white/10"
>
<LayoutGroup>
{props.quote && (
<motion.p
className="body-l-medium lg:m-heading mb-5 !font-bold !pb-0 min-h-32"
layout
>
<TextRotate
texts={["", `“${props.quote}”`]}
auto={inView}
staggerFrom={"first"}
staggerDuration={0.03}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ type: "spring", damping: 30, stiffness: 400 }}
elementLevelClassName="inline whitespace-nowrap"
loop={false}
rotationInterval={50}
splitBy="words"
/>
</motion.p>
)}
<div className="flex items-center flex-row flex-wrap gap-1">
<cite className="body-s-medium font-medium not-italic">
{props.authorName}
</cite>
{props.authorPosition && (
<span className="body-s-medium font-medium text-secondary-light dark:text-secondary-dark">
{props.authorPosition}
</span>
)}
<div className="flex items-center gap-3">
<motion.div
className="relative w-8 h-8 rounded-full dark:bg-white/20 bg-black/20"
initial={{ opacity: 0 }}
animate={inView && { opacity: 1 }}
transition={{ delay: 1.5 }}
>
{props.authorImage && (
<SanityImage
elProps={{ className: "rounded-full w-8 h-8 object-cover" }}
maxWidth={100}
image={props.authorImage}
alt={props.authorImage?.alt}
/>
)}
{props.image && (
<SanityImage
maxWidth={64}
image={props.image}
elProps={{
className:
"h-4 w-4 absolute bottom-0 right-0 z-10 rounded-full dark:bg-white/20 bg-black/20 object-cover",
}}
alt={props.authorImage?.alt}
/>
)}
</motion.div>
<div className="flex items-center flex-row flex-wrap gap-1">
{props.authorName && (
<cite className="body-s-medium font-medium not-italic">
<TextRotate
texts={["", props.authorName]}
auto={inView}
staggerFrom={"first"}
staggerDuration={0.04}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ type: "spring", damping: 30, stiffness: 400 }}
loop={false}
rotationInterval={1000}
splitBy="words"
/>
</cite>
)}

{props.authorPosition && (
<span className="body-s-medium font-medium text-secondary-light dark:text-secondary-dark">
<TextRotate
texts={["", props.authorPosition]}
auto={inView}
staggerFrom={"first"}
staggerDuration={0.04}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ type: "spring", damping: 30, stiffness: 400 }}
loop={false}
rotationInterval={1500}
splitBy="words"
/>
</span>
)}
</div>
</div>
</div>
</LayoutGroup>
</blockquote>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function BlogArticlePageContent(props: BlogArticlePayload) {
)}
</div>

<h1 className="l-heading lg:xl-heading text-center">
<h1 className="l-heading lg:xl-heading text-center w-[768px] max-w-full">
{props?.title}
</h1>
<ArticleAuthors authors={props.authors} />
Expand Down Expand Up @@ -112,7 +112,7 @@ export default function BlogArticlePageContent(props: BlogArticlePayload) {

<div className="relative flex w-full mt-m z-10">
{/* Body */}
<div className="body-l relative max-w-full flex-1 lg:max-w-xl xl:max-w-2xl mx-auto [&_p]:pb-8">
<div className="body-l relative max-w-full flex-1 lg:max-w-xl xl:max-w-2xl mx-auto [&_p]:pb-8 [&_p]:dark:opacity-85">
<div className="flex flex-wrap flex-row justify-between mb-8 lg:mb-16">
{typeof props.readTime === "number" && (
<span className="inline-flex gap-2 items-center body-s-medium font-medium text-secondary-light dark:text-secondary-dark">
Expand Down
120 changes: 120 additions & 0 deletions apps/frontend/components/templates/i18nPage/Cobe/Globe/index.tsx
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>
);
}
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}
/>
);
}
Loading

0 comments on commit c6bd941

Please sign in to comment.