Initial commit
This commit is contained in:
111
src/components/layout/Footer.tsx
Normal file
111
src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
target?: '_blank' | '_self';
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
links: FooterLink[];
|
||||
};
|
||||
|
||||
const footerColumns: FooterColumn[] = [
|
||||
{
|
||||
title: 'Affiliate Projects',
|
||||
links: [
|
||||
{ label: 'Project Mycelium', href: 'https://project.mycelium.tf', target: '_blank' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ label: 'Manual', href: 'https://manual.grid.tf/', target: '_blank' },
|
||||
{ label: 'Support', href: 'mailto:support@threefold.tech', target: '_blank' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'GeoMind',
|
||||
links: [
|
||||
{ label: 'Technology', href: '/technology' },
|
||||
{ label: 'Use Cases', href: '/usecases' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer className="border-t border-slate-200 bg-white">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
className="mx-auto flex max-w-6xl flex-col gap-8 px-6 py-12 lg:flex-row lg:items-start lg:justify-between lg:px-8"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="/images/geomind_logo.png"
|
||||
alt="Geomind logo"
|
||||
className="h-12 w-12 rounded-full object-contain shadow-subtle"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-semibold tracking-[0.35em] text-slate-500">
|
||||
GEOMIND
|
||||
</p>
|
||||
<p className="mt-2 max-w-xs text-sm text-slate-500">
|
||||
The datacenter standard for the next era of cloud and AI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid flex-1 grid-cols-1 gap-8 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
||||
{footerColumns.map((column) => (
|
||||
<div key={column.title}>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-slate-400">
|
||||
{column.title}
|
||||
</p>
|
||||
<ul className="mt-3 space-y-3 text-sm font-medium text-slate-600">
|
||||
{column.links.map((link) => (
|
||||
<li key={link.label}>
|
||||
<a
|
||||
href={link.href}
|
||||
target={link.target}
|
||||
rel={link.target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||
className="transition-colors duration-300 hover:text-brand-600"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="bg-mist py-4">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-6 text-xs text-slate-500 sm:flex-row lg:px-8">
|
||||
<span>Copyright {new Date().getFullYear()} GeoMind. All rights reserved.</span>
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href="https://www.linkedin.com/company/tf9/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-brand-600"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/threefoldtech"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-brand-600"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
136
src/components/layout/Header.tsx
Normal file
136
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', to: '/' },
|
||||
{ label: 'About', to: '/about' },
|
||||
{ label: 'Technology', to: '/technology' },
|
||||
{ label: 'Use Cases', to: '/usecases' },
|
||||
];
|
||||
|
||||
export const Header = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setIsScrolled(window.scrollY > 12);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMenuOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ opacity: 0, y: -12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-0 z-50 transition-all duration-500',
|
||||
isScrolled ? 'bg-white/95 shadow-lg backdrop-blur-sm' : 'bg-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4 lg:px-8">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<img
|
||||
src="/images/geomind_logo.png"
|
||||
alt="Geomind logo"
|
||||
className="h-10 w-10 rounded-full object-contain shadow-subtle"
|
||||
/>
|
||||
<span className="text-base font-semibold tracking-wider text-ink">
|
||||
GEOMIND
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-8 md:flex">
|
||||
{navItems.map(({ label, to }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'text-sm font-medium uppercase tracking-wide text-slate-500 transition-colors duration-300',
|
||||
isActive && 'text-brand-600',
|
||||
)
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
<a
|
||||
href="mailto:support@threefold.tech"
|
||||
className="rounded-full border border-brand-200 bg-white px-4 py-2 text-sm font-semibold text-brand-700 shadow-subtle transition-all duration-300 hover:-translate-y-0.5 hover:bg-brand-600 hover:text-white"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle navigation"
|
||||
className="md:hidden"
|
||||
onClick={() => setIsMenuOpen((prev) => !prev)}
|
||||
>
|
||||
<span className="relative block h-5 w-6">
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-0 top-1 h-0.5 w-full bg-ink transition-all duration-300',
|
||||
isMenuOpen && 'translate-y-2 rotate-45',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-0 top-3 h-0.5 w-full bg-ink transition-all duration-300',
|
||||
isMenuOpen && 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-0 top-5 h-0.5 w-4 bg-ink transition-all duration-300',
|
||||
isMenuOpen && 'left-0 top-3 w-full -rotate-45',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.nav
|
||||
initial={{ opacity: 0, y: -12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -16 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
className="border-t border-slate-100 bg-white/95 px-6 py-4 shadow-lg md:hidden"
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-4">
|
||||
{navItems.map(({ label, to }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'text-base font-medium uppercase tracking-wide text-slate-600',
|
||||
isActive && 'text-brand-700',
|
||||
)
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
<a
|
||||
href="mailto:support@threefold.tech"
|
||||
className="rounded-full border border-brand-200 bg-brand-600 px-4 py-2 text-center text-sm font-semibold text-white shadow-subtle"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</div>
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
);
|
||||
};
|
17
src/components/layout/Layout.tsx
Normal file
17
src/components/layout/Layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-white via-mist to-white">
|
||||
<Header />
|
||||
<main className="mx-auto max-w-6xl px-6 pb-24 pt-28 lg:px-8 lg:pt-32">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
55
src/components/ui/PrimaryButton.tsx
Normal file
55
src/components/ui/PrimaryButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
type PrimaryButtonProps = {
|
||||
to?: string;
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
variant?: 'solid' | 'outline' | 'ghost';
|
||||
className?: string;
|
||||
target?: string;
|
||||
};
|
||||
|
||||
const styles: Record<
|
||||
NonNullable<PrimaryButtonProps['variant']>,
|
||||
string
|
||||
> = {
|
||||
solid:
|
||||
'bg-brand-600 text-white shadow-subtle hover:bg-brand-500 hover:-translate-y-0.5',
|
||||
outline:
|
||||
'border border-brand-200 bg-white text-brand-700 hover:border-brand-400 hover:-translate-y-0.5',
|
||||
ghost:
|
||||
'bg-transparent text-brand-600 hover:text-brand-500',
|
||||
};
|
||||
|
||||
export const PrimaryButton = ({
|
||||
to,
|
||||
href,
|
||||
children,
|
||||
variant = 'solid',
|
||||
className,
|
||||
target,
|
||||
}: PrimaryButtonProps) => {
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center rounded-full px-5 py-2 text-sm font-semibold transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-300 focus-visible:ring-offset-2';
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<Link to={to} className={cn(baseClasses, styles[variant], className)}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||
className={cn(baseClasses, styles[variant], className)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user