@shipsite.dev/components 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/blog/BlogArticle.tsx +14 -0
- package/src/blog/BlogCTA.tsx +19 -0
- package/src/blog/BlogCTABanner.tsx +25 -0
- package/src/blog/BlogFAQ.tsx +32 -0
- package/src/blog/BlogIndex.tsx +24 -0
- package/src/blog/BlogIntro.tsx +9 -0
- package/src/blog/BlogTable.tsx +27 -0
- package/src/blog/BlogTip.tsx +15 -0
- package/src/blog/StartFreeNowCTA.tsx +29 -0
- package/src/context/ShipSiteProvider.tsx +78 -0
- package/src/context/ThemeProvider.tsx +26 -0
- package/src/index.ts +63 -0
- package/src/layout/Footer.tsx +68 -0
- package/src/layout/Header.tsx +95 -0
- package/src/legal/LegalPage.tsx +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/marketing/AlternatingFeatures.tsx +74 -0
- package/src/marketing/BannerCTA.tsx +43 -0
- package/src/marketing/BentoGrid.tsx +51 -0
- package/src/marketing/CalloutCard.tsx +25 -0
- package/src/marketing/CardGrid.tsx +29 -0
- package/src/marketing/Carousel.tsx +81 -0
- package/src/marketing/Companies.tsx +71 -0
- package/src/marketing/FAQ.tsx +50 -0
- package/src/marketing/Features.tsx +47 -0
- package/src/marketing/Gallery.tsx +55 -0
- package/src/marketing/Hero.tsx +60 -0
- package/src/marketing/PageHero.tsx +27 -0
- package/src/marketing/PricingSection.tsx +146 -0
- package/src/marketing/SocialProof.tsx +38 -0
- package/src/marketing/Stats.tsx +57 -0
- package/src/marketing/Steps.tsx +53 -0
- package/src/marketing/TabsSection.tsx +84 -0
- package/src/marketing/Testimonial.tsx +29 -0
- package/src/marketing/Testimonials.tsx +60 -0
- package/src/styles/utils.css +84 -0
- package/src/ui/accordion.tsx +66 -0
- package/src/ui/badge.tsx +55 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/card.tsx +75 -0
- package/src/ui/footer.tsx +51 -0
- package/src/ui/glow.tsx +48 -0
- package/src/ui/item.tsx +51 -0
- package/src/ui/mockup.tsx +64 -0
- package/src/ui/navbar.tsx +45 -0
- package/src/ui/section.tsx +15 -0
- package/src/ui/sheet.tsx +145 -0
- package/src/ui/theme-toggle.tsx +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipsite.dev/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist/",
|
|
23
|
+
"src/",
|
|
23
24
|
"components.json"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface BlogArticleProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
contentFolder?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function BlogArticle({ children }: BlogArticleProps) {
|
|
9
|
+
return (
|
|
10
|
+
<article className="py-12 md:py-20">
|
|
11
|
+
<div className="container-main max-w-3xl">{children}</div>
|
|
12
|
+
</article>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from '../ui/button';
|
|
3
|
+
|
|
4
|
+
interface BlogCTAProps {
|
|
5
|
+
title: string;
|
|
6
|
+
buttonText: string;
|
|
7
|
+
buttonHref: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function BlogCTA({ title, buttonText, buttonHref }: BlogCTAProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="my-10 p-8 rounded-2xl glass-1 text-center">
|
|
13
|
+
<h3 className="text-xl font-bold text-foreground mb-4">{title}</h3>
|
|
14
|
+
<Button asChild>
|
|
15
|
+
<a href={buttonHref}>{buttonText}</a>
|
|
16
|
+
</Button>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from '../ui/button';
|
|
3
|
+
import Glow from '../ui/glow';
|
|
4
|
+
|
|
5
|
+
interface BlogCTABannerProps {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
buttonText: string;
|
|
9
|
+
buttonLink: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function BlogCTABanner({ title, description, buttonText, buttonLink }: BlogCTABannerProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="my-12 rounded-2xl glass-4 p-10 text-center relative overflow-hidden">
|
|
15
|
+
<Glow variant="center" />
|
|
16
|
+
<div className="relative z-10">
|
|
17
|
+
<h3 className="text-2xl font-bold text-foreground mb-3">{title}</h3>
|
|
18
|
+
<p className="text-muted-foreground mb-6 max-w-lg mx-auto">{description}</p>
|
|
19
|
+
<Button asChild>
|
|
20
|
+
<a href={buttonLink}>{buttonText}</a>
|
|
21
|
+
</Button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Accordion,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionTrigger,
|
|
8
|
+
AccordionContent,
|
|
9
|
+
} from '../ui/accordion';
|
|
10
|
+
|
|
11
|
+
interface BlogFAQProps {
|
|
12
|
+
title: string;
|
|
13
|
+
items: Array<{ question: string; answer: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function BlogFAQ({ title, items }: BlogFAQProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="my-12">
|
|
19
|
+
<h2 className="text-2xl font-bold text-foreground mb-6">{title}</h2>
|
|
20
|
+
<Accordion type="single" collapsible>
|
|
21
|
+
{items.map((item, i) => (
|
|
22
|
+
<AccordionItem key={i} value={`faq-${i}`}>
|
|
23
|
+
<AccordionTrigger>{item.question}</AccordionTrigger>
|
|
24
|
+
<AccordionContent>
|
|
25
|
+
<div className="text-muted-foreground leading-relaxed">{item.answer}</div>
|
|
26
|
+
</AccordionContent>
|
|
27
|
+
</AccordionItem>
|
|
28
|
+
))}
|
|
29
|
+
</Accordion>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Section } from '../ui/section';
|
|
3
|
+
|
|
4
|
+
interface BlogIndexProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function BlogIndex({ title, description, children }: BlogIndexProps) {
|
|
11
|
+
return (
|
|
12
|
+
<Section>
|
|
13
|
+
<div className="container-main">
|
|
14
|
+
{(title || description) && (
|
|
15
|
+
<div className="text-center mb-12">
|
|
16
|
+
{title && <h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">{title}</h2>}
|
|
17
|
+
{description && <p className="text-lg text-muted-foreground">{description}</p>}
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
</Section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface BlogTableProps {
|
|
4
|
+
headers: string[];
|
|
5
|
+
rows: string[][];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function BlogTable({ headers, rows }: BlogTableProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="my-8 overflow-x-auto rounded-lg border border-border">
|
|
11
|
+
<table className="w-full text-sm">
|
|
12
|
+
<thead>
|
|
13
|
+
<tr className="bg-muted">
|
|
14
|
+
{headers.map((h, i) => <th key={i} className="text-left py-3 px-4 font-semibold text-foreground">{h}</th>)}
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
<tbody>
|
|
18
|
+
{rows.map((row, i) => (
|
|
19
|
+
<tr key={i} className="border-t border-border">
|
|
20
|
+
{row.map((cell, j) => <td key={j} className="py-3 px-4 text-muted-foreground">{cell}</td>)}
|
|
21
|
+
</tr>
|
|
22
|
+
))}
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface BlogTipProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function BlogTip({ title = 'Tip', children }: BlogTipProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="my-6 p-5 rounded-lg bg-emerald-50 border border-emerald-200">
|
|
11
|
+
<p className="font-semibold text-emerald-700 text-sm mb-1">{title}</p>
|
|
12
|
+
<div className="text-sm text-muted-foreground">{children}</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Check } from 'lucide-react';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
|
|
5
|
+
interface StartFreeNowCTAProps {
|
|
6
|
+
title: string;
|
|
7
|
+
bullets: string[];
|
|
8
|
+
buttonText: string;
|
|
9
|
+
buttonHref: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StartFreeNowCTA({ title, bullets, buttonText, buttonHref }: StartFreeNowCTAProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="my-12 p-8 rounded-2xl glass-3">
|
|
15
|
+
<h3 className="text-xl font-bold text-foreground mb-4">{title}</h3>
|
|
16
|
+
<ul className="space-y-2 mb-6">
|
|
17
|
+
{bullets.map((bullet, i) => (
|
|
18
|
+
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
19
|
+
<Check className="w-4 h-4 text-primary shrink-0" />
|
|
20
|
+
{bullet}
|
|
21
|
+
</li>
|
|
22
|
+
))}
|
|
23
|
+
</ul>
|
|
24
|
+
<Button asChild>
|
|
25
|
+
<a href={buttonHref}>{buttonText}</a>
|
|
26
|
+
</Button>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ShipSiteContextValue {
|
|
6
|
+
siteName: string;
|
|
7
|
+
siteUrl: string;
|
|
8
|
+
logo?: string | { light: string; dark: string };
|
|
9
|
+
ogImage?: string;
|
|
10
|
+
colors: {
|
|
11
|
+
primary: string;
|
|
12
|
+
accent: string;
|
|
13
|
+
background: string;
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
navigation: {
|
|
17
|
+
items: Array<{ label: string; href: string }>;
|
|
18
|
+
cta?: { label: string; href: string };
|
|
19
|
+
};
|
|
20
|
+
footer: {
|
|
21
|
+
columns?: Array<{
|
|
22
|
+
title: string;
|
|
23
|
+
links: Array<{ label: string; href: string }>;
|
|
24
|
+
}>;
|
|
25
|
+
social?: Array<{ platform: string; href: string }>;
|
|
26
|
+
copyright?: string;
|
|
27
|
+
};
|
|
28
|
+
navLinks: Record<string, string>;
|
|
29
|
+
alternatePathMap: Record<string, Record<string, string>>;
|
|
30
|
+
locale: string;
|
|
31
|
+
locales: string[];
|
|
32
|
+
defaultLocale: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ShipSiteContext = createContext<ShipSiteContextValue | null>(null);
|
|
36
|
+
|
|
37
|
+
export function ShipSiteProvider({
|
|
38
|
+
children,
|
|
39
|
+
value,
|
|
40
|
+
}: {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
value: ShipSiteContextValue;
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<ShipSiteContext.Provider value={value}>{children}</ShipSiteContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useShipSite() {
|
|
50
|
+
const context = useContext(ShipSiteContext);
|
|
51
|
+
if (!context) {
|
|
52
|
+
throw new Error('useShipSite must be used within a ShipSiteProvider');
|
|
53
|
+
}
|
|
54
|
+
return context;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useNavLinks() {
|
|
58
|
+
return useShipSite().navLinks;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useResolveHref() {
|
|
62
|
+
const { navLinks } = useShipSite();
|
|
63
|
+
return (href: string): string => {
|
|
64
|
+
if (
|
|
65
|
+
href.startsWith('http') ||
|
|
66
|
+
href.startsWith('#') ||
|
|
67
|
+
href.startsWith('mailto:')
|
|
68
|
+
) {
|
|
69
|
+
return href;
|
|
70
|
+
}
|
|
71
|
+
const slug = href.startsWith('/') ? href.slice(1) : href;
|
|
72
|
+
return navLinks[slug] ?? href;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useAlternateLinks() {
|
|
77
|
+
return useShipSite().alternatePathMap;
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { ThemeProvider as NextThemeProvider } from 'next-themes';
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
7
|
+
const [mounted, setMounted] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setMounted(true);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
if (!mounted) {
|
|
14
|
+
return <>{children}</>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<NextThemeProvider
|
|
19
|
+
attribute="class"
|
|
20
|
+
defaultTheme="dark"
|
|
21
|
+
enableSystem
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</NextThemeProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @shipsite.dev/components — MDX Component Registry
|
|
2
|
+
|
|
3
|
+
// Context
|
|
4
|
+
export {
|
|
5
|
+
ShipSiteProvider,
|
|
6
|
+
useShipSite,
|
|
7
|
+
useNavLinks,
|
|
8
|
+
useResolveHref,
|
|
9
|
+
useAlternateLinks,
|
|
10
|
+
} from './context/ShipSiteProvider';
|
|
11
|
+
export type { ShipSiteContextValue } from './context/ShipSiteProvider';
|
|
12
|
+
|
|
13
|
+
// Theme
|
|
14
|
+
export { ThemeProvider } from './context/ThemeProvider';
|
|
15
|
+
export { ThemeToggle } from './ui/theme-toggle';
|
|
16
|
+
|
|
17
|
+
// Layout
|
|
18
|
+
export { Header } from './layout/Header';
|
|
19
|
+
export { Footer } from './layout/Footer';
|
|
20
|
+
|
|
21
|
+
// Marketing
|
|
22
|
+
export { Hero } from './marketing/Hero';
|
|
23
|
+
export { PageHero } from './marketing/PageHero';
|
|
24
|
+
export { Features, Feature } from './marketing/Features';
|
|
25
|
+
export {
|
|
26
|
+
AlternatingFeatures,
|
|
27
|
+
AlternatingFeatureRow,
|
|
28
|
+
AlternatingFeatureItem,
|
|
29
|
+
} from './marketing/AlternatingFeatures';
|
|
30
|
+
export {
|
|
31
|
+
PricingSection,
|
|
32
|
+
PricingPlan,
|
|
33
|
+
ComparisonRow,
|
|
34
|
+
ComparisonCategory,
|
|
35
|
+
} from './marketing/PricingSection';
|
|
36
|
+
export { Companies } from './marketing/Companies';
|
|
37
|
+
export { Testimonial } from './marketing/Testimonial';
|
|
38
|
+
export { BannerCTA, BannerFeature } from './marketing/BannerCTA';
|
|
39
|
+
export { FAQ, FAQItem } from './marketing/FAQ';
|
|
40
|
+
export { Steps, Step } from './marketing/Steps';
|
|
41
|
+
export { CardGrid, CardGridItem } from './marketing/CardGrid';
|
|
42
|
+
export { CalloutCard } from './marketing/CalloutCard';
|
|
43
|
+
export { Stats, Stat } from './marketing/Stats';
|
|
44
|
+
export { Testimonials, TestimonialCard } from './marketing/Testimonials';
|
|
45
|
+
export { BentoGrid, BentoItem } from './marketing/BentoGrid';
|
|
46
|
+
export { Gallery, GalleryItem } from './marketing/Gallery';
|
|
47
|
+
export { SocialProof } from './marketing/SocialProof';
|
|
48
|
+
export { Carousel, CarouselItem } from './marketing/Carousel';
|
|
49
|
+
export { TabsSection, TabItem } from './marketing/TabsSection';
|
|
50
|
+
|
|
51
|
+
// Blog
|
|
52
|
+
export { BlogArticle } from './blog/BlogArticle';
|
|
53
|
+
export { BlogIndex } from './blog/BlogIndex';
|
|
54
|
+
export { BlogCTA } from './blog/BlogCTA';
|
|
55
|
+
export { BlogCTABanner } from './blog/BlogCTABanner';
|
|
56
|
+
export { BlogFAQ } from './blog/BlogFAQ';
|
|
57
|
+
export { BlogIntro } from './blog/BlogIntro';
|
|
58
|
+
export { BlogTable } from './blog/BlogTable';
|
|
59
|
+
export { BlogTip } from './blog/BlogTip';
|
|
60
|
+
export { StartFreeNowCTA } from './blog/StartFreeNowCTA';
|
|
61
|
+
|
|
62
|
+
// Legal
|
|
63
|
+
export { LegalPage, LegalSection } from './legal/LegalPage';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useShipSite, useResolveHref } from '../context/ShipSiteProvider';
|
|
5
|
+
import {
|
|
6
|
+
FooterRoot,
|
|
7
|
+
FooterContent,
|
|
8
|
+
FooterColumn,
|
|
9
|
+
FooterBottom,
|
|
10
|
+
} from '../ui/footer';
|
|
11
|
+
|
|
12
|
+
export function Footer() {
|
|
13
|
+
const { siteName, footer } = useShipSite();
|
|
14
|
+
const resolveHref = useResolveHref();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<footer className="border-t border-border">
|
|
18
|
+
<FooterRoot>
|
|
19
|
+
<div className="container-main">
|
|
20
|
+
<FooterContent>
|
|
21
|
+
{footer.columns?.map((column) => (
|
|
22
|
+
<FooterColumn key={column.title}>
|
|
23
|
+
<h3 className="text-sm font-semibold text-foreground">
|
|
24
|
+
{column.title}
|
|
25
|
+
</h3>
|
|
26
|
+
<ul className="flex flex-col gap-2">
|
|
27
|
+
{column.links.map((link) => (
|
|
28
|
+
<li key={link.href}>
|
|
29
|
+
<a
|
|
30
|
+
href={resolveHref(link.href)}
|
|
31
|
+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
32
|
+
>
|
|
33
|
+
{link.label}
|
|
34
|
+
</a>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
</FooterColumn>
|
|
39
|
+
))}
|
|
40
|
+
</FooterContent>
|
|
41
|
+
|
|
42
|
+
<FooterBottom>
|
|
43
|
+
<p>
|
|
44
|
+
{footer.copyright ||
|
|
45
|
+
`\u00A9 ${new Date().getFullYear()} ${siteName}`}
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
{footer.social && footer.social.length > 0 && (
|
|
49
|
+
<div className="flex items-center gap-4">
|
|
50
|
+
{footer.social.map((social) => (
|
|
51
|
+
<a
|
|
52
|
+
key={social.href}
|
|
53
|
+
href={social.href}
|
|
54
|
+
target="_blank"
|
|
55
|
+
rel="noopener noreferrer"
|
|
56
|
+
className="hover:text-foreground transition-colors capitalize"
|
|
57
|
+
>
|
|
58
|
+
{social.platform}
|
|
59
|
+
</a>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</FooterBottom>
|
|
64
|
+
</div>
|
|
65
|
+
</FooterRoot>
|
|
66
|
+
</footer>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Menu } from 'lucide-react';
|
|
5
|
+
import { useShipSite, useResolveHref } from '../context/ShipSiteProvider';
|
|
6
|
+
import { cn } from '../lib/utils';
|
|
7
|
+
import { Button } from '../ui/button';
|
|
8
|
+
import { Navbar, NavbarLeft, NavbarRight } from '../ui/navbar';
|
|
9
|
+
import {
|
|
10
|
+
Sheet,
|
|
11
|
+
SheetTrigger,
|
|
12
|
+
SheetContent,
|
|
13
|
+
SheetTitle,
|
|
14
|
+
} from '../ui/sheet';
|
|
15
|
+
import { ThemeToggle } from '../ui/theme-toggle';
|
|
16
|
+
|
|
17
|
+
export function Header() {
|
|
18
|
+
const { siteName, logo, navigation, locale, defaultLocale } = useShipSite();
|
|
19
|
+
const resolveHref = useResolveHref();
|
|
20
|
+
|
|
21
|
+
const logoSrc = typeof logo === 'string' ? logo : logo?.light;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<header className="sticky top-0 z-50 bg-background/80 backdrop-blur-lg border-b border-border">
|
|
25
|
+
<div className="container-main">
|
|
26
|
+
<Navbar>
|
|
27
|
+
<NavbarLeft>
|
|
28
|
+
<a
|
|
29
|
+
href={locale === defaultLocale ? '/' : `/${locale}`}
|
|
30
|
+
className="flex items-center gap-2"
|
|
31
|
+
>
|
|
32
|
+
{logoSrc && (
|
|
33
|
+
<img src={logoSrc} alt={siteName} className="h-8 w-auto" />
|
|
34
|
+
)}
|
|
35
|
+
<span className="font-semibold text-lg text-foreground">
|
|
36
|
+
{siteName}
|
|
37
|
+
</span>
|
|
38
|
+
</a>
|
|
39
|
+
</NavbarLeft>
|
|
40
|
+
|
|
41
|
+
<NavbarRight className="hidden md:flex">
|
|
42
|
+
{navigation.items.map((item) => (
|
|
43
|
+
<a
|
|
44
|
+
key={item.href}
|
|
45
|
+
href={resolveHref(item.href)}
|
|
46
|
+
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
47
|
+
>
|
|
48
|
+
{item.label}
|
|
49
|
+
</a>
|
|
50
|
+
))}
|
|
51
|
+
<ThemeToggle />
|
|
52
|
+
{navigation.cta && (
|
|
53
|
+
<Button asChild size="sm">
|
|
54
|
+
<a href={navigation.cta.href}>
|
|
55
|
+
{navigation.cta.label}
|
|
56
|
+
</a>
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
59
|
+
</NavbarRight>
|
|
60
|
+
|
|
61
|
+
<div className="md:hidden">
|
|
62
|
+
<Sheet>
|
|
63
|
+
<SheetTrigger asChild>
|
|
64
|
+
<Button variant="ghost" size="icon" aria-label="Toggle menu">
|
|
65
|
+
<Menu className="size-5" />
|
|
66
|
+
</Button>
|
|
67
|
+
</SheetTrigger>
|
|
68
|
+
<SheetContent side="right">
|
|
69
|
+
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
70
|
+
<nav className="flex flex-col gap-4 mt-8">
|
|
71
|
+
{navigation.items.map((item) => (
|
|
72
|
+
<a
|
|
73
|
+
key={item.href}
|
|
74
|
+
href={resolveHref(item.href)}
|
|
75
|
+
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
76
|
+
>
|
|
77
|
+
{item.label}
|
|
78
|
+
</a>
|
|
79
|
+
))}
|
|
80
|
+
{navigation.cta && (
|
|
81
|
+
<Button asChild className="mt-2">
|
|
82
|
+
<a href={navigation.cta.href}>
|
|
83
|
+
{navigation.cta.label}
|
|
84
|
+
</a>
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
</nav>
|
|
88
|
+
</SheetContent>
|
|
89
|
+
</Sheet>
|
|
90
|
+
</div>
|
|
91
|
+
</Navbar>
|
|
92
|
+
</div>
|
|
93
|
+
</header>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface LegalSectionProps {
|
|
4
|
+
title: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function LegalSection({ title, children }: LegalSectionProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="mb-8">
|
|
11
|
+
<h2 className="text-xl font-bold text-foreground mb-4">{title}</h2>
|
|
12
|
+
<div className="text-muted-foreground text-sm leading-relaxed [&>p]:mb-3 [&>ul]:list-disc [&>ul]:pl-5 [&>ul]:mb-3">
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LegalPageProps {
|
|
20
|
+
title: string;
|
|
21
|
+
lastUpdated?: string;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LegalPage({ title, lastUpdated, children }: LegalPageProps) {
|
|
26
|
+
return (
|
|
27
|
+
<section className="py-12 md:py-20">
|
|
28
|
+
<div className="container-main max-w-3xl">
|
|
29
|
+
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-2">{title}</h1>
|
|
30
|
+
{lastUpdated && <p className="text-sm text-muted-foreground mb-8">Last updated: {lastUpdated}</p>}
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Section } from '../ui/section';
|
|
3
|
+
import { Mockup } from '../ui/mockup';
|
|
4
|
+
|
|
5
|
+
interface AlternatingFeatureItemProps {
|
|
6
|
+
icon?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AlternatingFeatureItem({ icon, title, description }: AlternatingFeatureItemProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex gap-3 items-start">
|
|
14
|
+
{icon && <span className="text-primary text-xl mt-0.5">{icon}</span>}
|
|
15
|
+
<div>
|
|
16
|
+
<h4 className="font-semibold text-foreground mb-1">{title}</h4>
|
|
17
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AlternatingFeatureRowProps {
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
image: string;
|
|
27
|
+
imageDark?: string;
|
|
28
|
+
imageAlt?: string;
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AlternatingFeatureRow({ title, description, image, imageDark, imageAlt, children }: AlternatingFeatureRowProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center py-12 [&:nth-child(even)>div:first-child]:md:order-2">
|
|
35
|
+
<div>
|
|
36
|
+
<h3 className="text-2xl md:text-3xl font-bold text-foreground mb-4">{title}</h3>
|
|
37
|
+
{description && <p className="text-muted-foreground mb-6">{description}</p>}
|
|
38
|
+
{children && <div className="space-y-4">{children}</div>}
|
|
39
|
+
</div>
|
|
40
|
+
<Mockup type="responsive">
|
|
41
|
+
{imageDark ? (
|
|
42
|
+
<>
|
|
43
|
+
<img src={image} alt={imageAlt || title} className="w-full dark:hidden" />
|
|
44
|
+
<img src={imageDark} alt={imageAlt || title} className="w-full hidden dark:block" />
|
|
45
|
+
</>
|
|
46
|
+
) : (
|
|
47
|
+
<img src={image} alt={imageAlt || title} className="w-full" />
|
|
48
|
+
)}
|
|
49
|
+
</Mockup>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface AlternatingFeaturesProps {
|
|
55
|
+
title?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function AlternatingFeatures({ title, description, children }: AlternatingFeaturesProps) {
|
|
61
|
+
return (
|
|
62
|
+
<Section>
|
|
63
|
+
<div className="container-main">
|
|
64
|
+
{(title || description) && (
|
|
65
|
+
<div className="text-center mb-16">
|
|
66
|
+
{title && <h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">{title}</h2>}
|
|
67
|
+
{description && <p className="text-lg text-muted-foreground max-w-2xl mx-auto">{description}</p>}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<div className="divide-y divide-border">{children}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</Section>
|
|
73
|
+
);
|
|
74
|
+
}
|