tenext redesign
This commit is contained in:
64
components/shared/AnchoredCardGrid.tsx
Normal file
64
components/shared/AnchoredCardGrid.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { SectionContainer } from "@/components/shared/SectionContainer";
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
import { SectionHeading } from "@/components/shared/SectionHeading";
|
||||
import { RevealOnView } from "@/components/shared/RevealOnView";
|
||||
import { CardItem } from "@/data/site-pages";
|
||||
|
||||
interface AnchoredCardGridProps {
|
||||
tag?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
items: CardItem[];
|
||||
/** Number of columns at lg breakpoint */
|
||||
columns?: 2 | 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* AnchoredCardGrid — data-driven grid of bordered cards.
|
||||
*
|
||||
* Each card carries an `id` so navbar hash links (e.g. /solutions#devops)
|
||||
* scroll to it. Shared by the Solutions and Industries pages; composes the
|
||||
* existing SectionContainer / PageContainer / SectionHeading primitives.
|
||||
*/
|
||||
export function AnchoredCardGrid({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
columns = 3,
|
||||
}: AnchoredCardGridProps) {
|
||||
const colClass = columns === 2 ? "md:grid-cols-2" : "md:grid-cols-2 lg:grid-cols-3";
|
||||
|
||||
return (
|
||||
<SectionContainer paddingY="lg" className="bg-white px-[20px]">
|
||||
<PageContainer>
|
||||
<SectionHeading
|
||||
tag={tag}
|
||||
title={title}
|
||||
description={description}
|
||||
titleClassName="text-tenext-dark max-w-[720px]"
|
||||
descriptionClassName="max-w-[620px]"
|
||||
className="mb-[50px]"
|
||||
/>
|
||||
|
||||
<div className={`grid grid-cols-1 gap-[20px] ${colClass}`}>
|
||||
{items.map((item, index) => (
|
||||
<RevealOnView key={item.id} delay={index * 90}>
|
||||
<div
|
||||
id={item.id}
|
||||
className="flex h-full scroll-mt-[120px] flex-col gap-[15px] rounded-[25px] border border-tenext-card-border p-[40px] transition-transform duration-300 hover:-translate-y-2"
|
||||
>
|
||||
<h3 className="font-[var(--font-sora)] text-[22px] font-bold leading-[1.3] text-tenext-dark">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-[16px] leading-[1.7] text-tenext-text-muted">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</RevealOnView>
|
||||
))}
|
||||
</div>
|
||||
</PageContainer>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { siteConfig } from "@/data/site-config";
|
||||
import { ctaContent } from "@/data/site-content";
|
||||
import { SectionHeading } from "@/components/shared/SectionHeading";
|
||||
import { SocialIcons } from "@/components/shared/SocialIcons";
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
@@ -42,10 +43,10 @@ export function ContactFormSection() {
|
||||
{/* ── Left: Info column ── */}
|
||||
<div className="flex flex-col">
|
||||
<SectionHeading
|
||||
tag="get in touch"
|
||||
title="We are always ready to help you and answer your questions"
|
||||
tag={ctaContent.contact.tag}
|
||||
title={ctaContent.contact.title}
|
||||
titleClassName="text-white"
|
||||
description="Pacific hake false trevally queen parrotfish black prickleback mosshead warbonnet sweeper! Greenling sleeper."
|
||||
description={ctaContent.contact.description}
|
||||
/>
|
||||
|
||||
{/* Contact details grid */}
|
||||
@@ -62,12 +63,6 @@ export function ContactFormSection() {
|
||||
>
|
||||
{siteConfig.phone}
|
||||
</a>
|
||||
<a
|
||||
href="tel:+11800234567"
|
||||
className="text-[15px] text-tenext-text-muted transition-colors hover:text-tenext-teal"
|
||||
>
|
||||
+ (123) 1800-234-5678
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { siteConfig } from "@/data/site-config";
|
||||
import { SocialIcons } from "@/components/shared/SocialIcons";
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
import { SiteLogo } from "@/components/shared/SiteLogo";
|
||||
|
||||
/**
|
||||
* Footer — shared site-wide footer.
|
||||
@@ -23,14 +23,8 @@ export function Footer() {
|
||||
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-[1.5fr_1fr_1fr]">
|
||||
{/* Brand column */}
|
||||
<div className="flex flex-col items-start gap-8">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Image
|
||||
src="/images/tenext/logo-dark.png"
|
||||
alt={siteConfig.name}
|
||||
width={146}
|
||||
height={26}
|
||||
className="h-auto w-auto brightness-0 invert"
|
||||
/>
|
||||
<Link href="/" aria-label={`${siteConfig.legalName} — Home`}>
|
||||
<SiteLogo size={28} />
|
||||
</Link>
|
||||
|
||||
<SocialIcons links={siteConfig.socials} />
|
||||
|
||||
79
components/shared/Icon.tsx
Normal file
79
components/shared/Icon.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Cloud,
|
||||
Cpu,
|
||||
Database,
|
||||
Code2,
|
||||
Smartphone,
|
||||
Boxes,
|
||||
GitBranch,
|
||||
ShieldCheck,
|
||||
LineChart,
|
||||
Rocket,
|
||||
Target,
|
||||
Eye,
|
||||
Compass,
|
||||
Building2,
|
||||
Landmark,
|
||||
HeartPulse,
|
||||
Factory,
|
||||
GraduationCap,
|
||||
ShoppingBag,
|
||||
Layers,
|
||||
Workflow,
|
||||
Users,
|
||||
Sparkles,
|
||||
Handshake,
|
||||
BadgeCheck,
|
||||
Search,
|
||||
PenTool,
|
||||
Hammer,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
Headset,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
const map: Record<string, LucideIcon> = {
|
||||
cloud: Cloud,
|
||||
cpu: Cpu,
|
||||
database: Database,
|
||||
code: Code2,
|
||||
mobile: Smartphone,
|
||||
boxes: Boxes,
|
||||
devops: GitBranch,
|
||||
shield: ShieldCheck,
|
||||
chart: LineChart,
|
||||
rocket: Rocket,
|
||||
target: Target,
|
||||
eye: Eye,
|
||||
compass: Compass,
|
||||
building: Building2,
|
||||
landmark: Landmark,
|
||||
health: HeartPulse,
|
||||
factory: Factory,
|
||||
education: GraduationCap,
|
||||
retail: ShoppingBag,
|
||||
layers: Layers,
|
||||
workflow: Workflow,
|
||||
users: Users,
|
||||
sparkles: Sparkles,
|
||||
handshake: Handshake,
|
||||
badge: BadgeCheck,
|
||||
discover: Search,
|
||||
design: PenTool,
|
||||
build: Hammer,
|
||||
modernize: RefreshCw,
|
||||
scale: TrendingUp,
|
||||
support: Headset,
|
||||
};
|
||||
|
||||
interface IconProps {
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Renders a named lucide icon. Keeps content data serializable (string keys). */
|
||||
export function Icon({ name, className = "h-6 w-6" }: IconProps) {
|
||||
const Cmp = map[name] ?? Sparkles;
|
||||
return <Cmp className={className} strokeWidth={1.6} />;
|
||||
}
|
||||
42
components/shared/LegalContent.tsx
Normal file
42
components/shared/LegalContent.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SectionContainer } from "@/components/shared/SectionContainer";
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
import { RevealOnView } from "@/components/shared/RevealOnView";
|
||||
import { LegalSection, legalEffectiveDate } from "@/data/legal";
|
||||
|
||||
interface LegalContentProps {
|
||||
sections: LegalSection[];
|
||||
}
|
||||
|
||||
/**
|
||||
* LegalContent — shared renderer for Privacy and Terms pages.
|
||||
* Uses the site's spacing, container, typography, and reveal animation.
|
||||
*/
|
||||
export function LegalContent({ sections }: LegalContentProps) {
|
||||
return (
|
||||
<SectionContainer paddingY="lg" className="bg-white px-[20px]">
|
||||
<PageContainer className="max-w-[820px]">
|
||||
<p className="mb-[40px] text-[14px] font-semibold uppercase tracking-[0.1em] text-tenext-teal">
|
||||
Effective {legalEffectiveDate}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-[40px]">
|
||||
{sections.map((section, index) => (
|
||||
<RevealOnView key={section.heading} delay={index * 60}>
|
||||
<h2 className="mb-[16px] font-[var(--font-sora)] text-[24px] font-bold leading-[1.25] text-tenext-dark">
|
||||
{section.heading}
|
||||
</h2>
|
||||
{section.body.map((paragraph, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className="mb-[14px] text-[16px] leading-[1.8] text-tenext-text-muted"
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</RevealOnView>
|
||||
))}
|
||||
</div>
|
||||
</PageContainer>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { siteConfig } from "@/data/site-config";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
@@ -23,8 +25,23 @@ interface PageTitleBannerProps {
|
||||
* - Top-left and bottom-right rounded corner decorations
|
||||
*/
|
||||
export function PageTitleBanner({ title, breadcrumbs }: PageTitleBannerProps) {
|
||||
const breadcrumbJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: breadcrumbs.map((crumb, index) => ({
|
||||
"@type": "ListItem",
|
||||
position: index + 1,
|
||||
name: crumb.label,
|
||||
...(crumb.href ? { item: `${siteConfig.url}${crumb.href}` } : {}),
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mx-5 overflow-hidden rounded-[25px] bg-tenext-dark">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
{/* Background image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center opacity-80"
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
import { SectionContainer } from "@/components/shared/SectionContainer";
|
||||
import { industries } from "@/data/site-pages";
|
||||
|
||||
/**
|
||||
* Industries trust strip.
|
||||
*
|
||||
* Replaces the template's fabricated partner-logo carousel. Tenext does
|
||||
* not publish client logos here, so this presents the real industries it
|
||||
* serves. Swap in a genuine client-logo row once approved logos exist.
|
||||
*/
|
||||
export function PartnerLogosSection() {
|
||||
const logos = [
|
||||
"/images/tenext/partners-1.png",
|
||||
"/images/tenext/partners-2.png",
|
||||
"/images/tenext/partners-1.png",
|
||||
"/images/tenext/partners-2.png",
|
||||
"/images/tenext/partners-1.png",
|
||||
"/images/tenext/partners-2.png",
|
||||
];
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
paddingY="lg"
|
||||
@@ -28,25 +25,18 @@ export function PartnerLogosSection() {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Our trusted collaborators in progress and success
|
||||
Trusted to deliver across the industries we serve
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-8 md:gap-16 lg:gap-24 opacity-60">
|
||||
{logos.map((src, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex h-12 items-center justify-center grayscale transition-all hover:grayscale-0"
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 md:gap-x-16">
|
||||
{industries.map((industry) => (
|
||||
<span
|
||||
key={industry.id}
|
||||
className="text-[18px] font-semibold text-white/70 transition-colors hover:text-tenext-teal md:text-[22px]"
|
||||
style={{ fontFamily: "var(--font-sora)" }}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={`Partner ${index + 1}`}
|
||||
className="h-full w-auto object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src =
|
||||
"/images/tenext/logo-dark.png";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{industry.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</PageContainer>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { tenextButton as TenextButton } from "@/components/shared/AieroButton";
|
||||
import { tenextButton as TenextButton } from "@/components/shared/Button";
|
||||
import { PageContainer } from "@/components/shared/PageContainer";
|
||||
import { ctaContent } from "@/data/site-content";
|
||||
|
||||
/**
|
||||
* PreFooterCTA — "Elevate your business with our innovative solutions" banner.
|
||||
* PreFooterCTA — closing conversion banner.
|
||||
*
|
||||
* Shared across: Home, About, Services, Contacts.
|
||||
*
|
||||
@@ -25,7 +26,7 @@ export function PreFooterCTA() {
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
Elevate your business with our innovative solutions
|
||||
{ctaContent.preFooter.heading}
|
||||
</h2>
|
||||
|
||||
{/* CTA */}
|
||||
@@ -34,7 +35,7 @@ export function PreFooterCTA() {
|
||||
variant="simple"
|
||||
className="shrink-0 text-[15px] font-semibold text-tenext-teal"
|
||||
>
|
||||
Get in touch
|
||||
{ctaContent.preFooter.linkLabel}
|
||||
</TenextButton>
|
||||
</PageContainer>
|
||||
</section>
|
||||
|
||||
24
components/shared/SiteLogo.tsx
Normal file
24
components/shared/SiteLogo.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
interface SiteLogoProps {
|
||||
/** Extra classes (e.g. color overrides) */
|
||||
className?: string;
|
||||
/** Wordmark font-size in px — matches the slot it replaces */
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SiteLogo — Tenext Technologies wordmark.
|
||||
*
|
||||
* Temporary text logo in the project's Sora typography, used until an
|
||||
* official logo asset is supplied. Rendered on dark surfaces (navbar,
|
||||
* footer), so it defaults to white with a teal accent dot.
|
||||
*/
|
||||
export function SiteLogo({ className = "", size = 22 }: SiteLogoProps) {
|
||||
return (
|
||||
<span
|
||||
className={`font-bold leading-none tracking-[-0.02em] text-white ${className}`}
|
||||
style={{ fontFamily: "var(--font-sora)", fontSize: `${size}px` }}
|
||||
>
|
||||
Tenext<span className="text-tenext-teal">.</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user