@olwiba/ui 0.0.35 → 0.0.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/ui",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -43,11 +43,18 @@
43
43
  "zod": "^4.3.6"
44
44
  },
45
45
  "peerDependencies": {
46
+ "@content-collections/mdx": ">=0.2.0",
46
47
  "@olwiba/cn": ">=0.1.2",
47
48
  "react": ">=18.0.0",
48
49
  "react-dom": ">=18.0.0"
49
50
  },
51
+ "peerDependenciesMeta": {
52
+ "@content-collections/mdx": {
53
+ "optional": true
54
+ }
55
+ },
50
56
  "devDependencies": {
57
+ "@content-collections/mdx": "^0.2.2",
51
58
  "@olwiba/cn": "^0.1.15",
52
59
  "@tailwindcss/vite": "^4.1.18",
53
60
  "@tanstack/react-router": "1.154.8",
@@ -0,0 +1,71 @@
1
+ import * as React from 'react';
2
+ import { useMDXComponent } from '@content-collections/mdx/react';
3
+ import { cn } from '../lib/utils';
4
+
5
+ const components = {
6
+ h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
7
+ <h1 className={cn('mt-8 scroll-m-20 text-3xl font-bold tracking-tight text-foreground first:mt-0', className)} {...props} />
8
+ ),
9
+ h2: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
10
+ <h2 className={cn('mt-10 scroll-m-20 text-2xl font-semibold tracking-tight text-foreground first:mt-0', className)} {...props} />
11
+ ),
12
+ h3: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
13
+ <h3 className={cn('mt-8 scroll-m-20 text-xl font-semibold tracking-tight text-foreground', className)} {...props} />
14
+ ),
15
+ h4: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
16
+ <h4 className={cn('mt-6 scroll-m-20 text-lg font-semibold tracking-tight text-foreground', className)} {...props} />
17
+ ),
18
+ p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
19
+ <p className={cn('leading-7 text-muted-foreground [&:not(:first-child)]:mt-6', className)} {...props} />
20
+ ),
21
+ ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
22
+ <ul className={cn('my-6 ml-6 list-disc text-muted-foreground', className)} {...props} />
23
+ ),
24
+ ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
25
+ <ol className={cn('my-6 ml-6 list-decimal text-muted-foreground', className)} {...props} />
26
+ ),
27
+ li: ({ className, ...props }: React.HTMLAttributes<HTMLLIElement>) => (
28
+ <li className={cn('mt-2', className)} {...props} />
29
+ ),
30
+ blockquote: ({ className, ...props }: React.HTMLAttributes<HTMLQuoteElement>) => (
31
+ <blockquote className={cn('mt-6 rounded-r-lg border-l-4 border-primary bg-muted py-4 pl-6 pr-4 italic text-muted-foreground', className)} {...props} />
32
+ ),
33
+ hr: (props: React.HTMLAttributes<HTMLHRElement>) => (
34
+ <hr className="my-8 border-border" {...props} />
35
+ ),
36
+ img: ({ className, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
37
+ <img className={cn('rounded-lg border shadow-sm', className)} alt={alt} {...props} />
38
+ ),
39
+ pre: ({ className, ...props }: React.HTMLAttributes<HTMLPreElement>) => (
40
+ <pre className={cn('mt-6 mb-4 overflow-x-auto rounded-lg bg-muted p-4 text-sm', className)} {...props} />
41
+ ),
42
+ code: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
43
+ <code className={cn('relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm text-foreground', className)} {...props} />
44
+ ),
45
+ table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
46
+ <div className="my-6 w-full overflow-y-auto rounded-lg border border-border">
47
+ <table className={cn('w-full', className)} {...props} />
48
+ </div>
49
+ ),
50
+ tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
51
+ <tr className={cn('m-0 border-t border-border p-0 even:bg-muted', className)} {...props} />
52
+ ),
53
+ th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
54
+ <th className={cn('border border-border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right', className)} {...props} />
55
+ ),
56
+ td: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
57
+ <td className={cn('border border-border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right', className)} {...props} />
58
+ ),
59
+ a: ({ className, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
60
+ <a className={cn('font-medium text-primary underline underline-offset-4 hover:text-primary/80', className)} {...props} />
61
+ ),
62
+ };
63
+
64
+ export interface MdxContentProps {
65
+ code: string;
66
+ }
67
+
68
+ export function MdxContent({ code }: MdxContentProps) {
69
+ const MDXComponent = useMDXComponent(code);
70
+ return <MDXComponent components={components} />;
71
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../lib/utils';
3
+
4
+ function AppScreenRoot({ children, className }: { children?: React.ReactNode; className?: string }) {
5
+ return (
6
+ <div className={cn('flex h-full flex-col', className)}>
7
+ {children}
8
+ </div>
9
+ );
10
+ }
11
+
12
+ function AppScreenHeader({ children, className }: { children?: React.ReactNode; className?: string }) {
13
+ return (
14
+ <div className={cn('flex items-center gap-2 border-b border-border px-4 py-3 shrink-0', className)}>
15
+ {children}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ function AppScreenBody({ children, className }: { children?: React.ReactNode; className?: string }) {
21
+ return (
22
+ <div className={cn('flex-1 overflow-auto px-4 py-3', className)}>
23
+ {children}
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export const AppScreen = Object.assign(AppScreenRoot, {
29
+ Header: AppScreenHeader,
30
+ Body: AppScreenBody,
31
+ });
32
+
33
+ export type { };
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../lib/utils';
3
+
4
+ const sizes = {
5
+ sm: { outer: 'w-44', height: 'h-80', notch: 'w-16 h-4', border: 'rounded-[2rem]', screen: 'rounded-[1.75rem]' },
6
+ md: { outer: 'w-56', height: 'h-[26rem]', notch: 'w-20 h-5', border: 'rounded-[2.5rem]', screen: 'rounded-[2.25rem]' },
7
+ lg: { outer: 'w-72', height: 'h-[34rem]', notch: 'w-24 h-6', border: 'rounded-[3rem]', screen: 'rounded-[2.75rem]' },
8
+ } as const;
9
+
10
+ export interface PhoneFrameProps {
11
+ children?: React.ReactNode;
12
+ size?: keyof typeof sizes;
13
+ className?: string;
14
+ float?: boolean;
15
+ }
16
+
17
+ export function PhoneFrame({ children, size = 'md', className, float = true }: PhoneFrameProps) {
18
+ const s = sizes[size];
19
+ return (
20
+ <>
21
+ {float && (
22
+ <style>{`
23
+ @keyframes olwiba-phone-float {
24
+ 0%, 100% { transform: translateY(0px); }
25
+ 50% { transform: translateY(-10px); }
26
+ }
27
+ .olwiba-phone-float { animation: olwiba-phone-float 5s ease-in-out infinite; }
28
+ `}</style>
29
+ )}
30
+ <div
31
+ className={cn(
32
+ 'relative mx-auto',
33
+ s.outer,
34
+ float && 'olwiba-phone-float',
35
+ className,
36
+ )}
37
+ >
38
+ {/* Device body */}
39
+ <div
40
+ className={cn(
41
+ 'relative flex w-full flex-col overflow-hidden border-[3px] border-foreground/15 bg-muted shadow-2xl',
42
+ s.border,
43
+ s.height,
44
+ )}
45
+ >
46
+ {/* Status bar / notch area */}
47
+ <div className="relative flex justify-center pt-3 pb-1 shrink-0">
48
+ <div className={cn('rounded-full bg-foreground/10', s.notch)} />
49
+ </div>
50
+
51
+ {/* Screen */}
52
+ <div className={cn('mx-1.5 mb-1.5 flex-1 overflow-hidden bg-background', s.screen)}>
53
+ {children}
54
+ </div>
55
+ </div>
56
+
57
+ {/* Side buttons */}
58
+ <div className="absolute right-[-5px] top-24 h-12 w-[3px] rounded-l-sm bg-foreground/15" />
59
+ <div className="absolute left-[-5px] top-20 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
60
+ <div className="absolute left-[-5px] top-32 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
61
+ <div className="absolute left-[-5px] top-44 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
62
+ </div>
63
+ </>
64
+ );
65
+ }
package/src/index.ts CHANGED
@@ -57,8 +57,8 @@ export { TestimonialsSection, type TestimonialsSectionProps } from './marketing/
57
57
  export { TeamSection } from './marketing/TeamSection';
58
58
  export { FaqSection, type FaqSectionProps } from './marketing/FaqSection';
59
59
  export { StatsSection, type StatsSectionProps } from './marketing/StatsSection';
60
- export { NewsletterSection } from './marketing/NewsletterSection';
61
- export { ContactSection } from './marketing/ContactSection';
60
+ export { NewsletterSection, type NewsletterSectionProps } from './marketing/NewsletterSection';
61
+ export { ContactSection, type ContactSectionProps, type ContactInfoItem } from './marketing/ContactSection';
62
62
 
63
63
  // ─── Marketing — elements ─────────────────────────────────────────────────────
64
64
  export { Navbar, type NavbarProps } from './marketing/Navbar';
@@ -81,6 +81,10 @@ export { Dock, type DockProps, type DockItem } from './components/Dock';
81
81
  export { ContextMenu, type ContextMenuProps, type ContextMenuDef } from './components/ContextMenu';
82
82
  export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';
83
83
 
84
+ // ─── Components — device mockups ────────────────────────────────────────────
85
+ export { PhoneFrame, type PhoneFrameProps } from './components/PhoneFrame';
86
+ export { AppScreen } from './components/AppScreen';
87
+
84
88
  // ─── Components — cards ───────────────────────────────────────────────────────
85
89
  export { GlassCard, type GlassCardProps } from './components/GlassCard';
86
90
  export { FeatureCard, type FeatureCardProps } from './components/FeatureCard';
@@ -103,6 +107,7 @@ export { RootErrorFallback } from './components/RootErrorFallback';
103
107
  // ─── Blog ────────────────────────────────────────────────────────────────────
104
108
  export { PostCard, type PostCardProps } from './blog/PostCard';
105
109
  export { PostList, type PostListProps } from './blog/PostList';
110
+ export { MdxContent, type MdxContentProps } from './blog/MdxContent';
106
111
 
107
112
  // ─── Hooks ────────────────────────────────────────────────────────────────────
108
113
  export { useMounted } from './hooks/use-mounted';
@@ -1,44 +1,81 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import { Mail, MapPin, MessageSquare, Phone, Send } from 'lucide-react';
4
+ import { Mail, MapPin, MessageSquare, Phone, Send, type LucideIcon } from 'lucide-react';
5
5
  import { Badge, Button, Input, Label, Separator, Textarea } from '@olwiba/cn';
6
6
 
7
- const contactInfo = [
8
- { Icon: Mail, label: 'Email', value: 'hello@olwiba.com' },
9
- { Icon: Phone, label: 'Phone', value: '+1 (555) 000-0000' },
10
- { Icon: MapPin, label: 'Office', value: 'San Francisco, CA' },
7
+ export type ContactInfoItem = {
8
+ label: string;
9
+ value: string;
10
+ icon?: LucideIcon;
11
+ };
12
+
13
+ export interface ContactSectionProps {
14
+ badge?: string;
15
+ title?: string;
16
+ description?: string;
17
+ contactInfo?: ContactInfoItem[];
18
+ successTitle?: string;
19
+ successDescription?: string;
20
+ sendAnotherLabel?: string;
21
+ onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
22
+ }
23
+
24
+ const defaultContactInfoResolved: ContactInfoItem[] = [
25
+ { icon: Mail, label: 'Email', value: 'hello@olwiba.com' },
26
+ { icon: Phone, label: 'Phone', value: '+1 (555) 000-0000' },
27
+ { icon: MapPin, label: 'Office', value: 'San Francisco, CA' },
11
28
  ];
12
29
 
13
- export function ContactSection() {
30
+ const defaults = {
31
+ badge: 'Contact us',
32
+ title: "Let's talk",
33
+ description:
34
+ "Have a question or want to work together? Fill in the form and we'll get back to you within one business day.",
35
+ successTitle: 'Message sent',
36
+ successDescription: "Thanks for reaching out. We'll get back to you within one business day.",
37
+ sendAnotherLabel: 'Send another',
38
+ };
39
+
40
+ export function ContactSection({
41
+ badge = defaults.badge,
42
+ title = defaults.title,
43
+ description = defaults.description,
44
+ contactInfo = defaultContactInfoResolved,
45
+ successTitle = defaults.successTitle,
46
+ successDescription = defaults.successDescription,
47
+ sendAnotherLabel = defaults.sendAnotherLabel,
48
+ onSubmit,
49
+ }: ContactSectionProps = {}) {
14
50
  const [submitted, setSubmitted] = React.useState(false);
15
51
 
16
- function handleSubmit(e: React.FormEvent) {
52
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
17
53
  e.preventDefault();
54
+ await onSubmit?.(e);
18
55
  setSubmitted(true);
19
56
  }
20
57
 
21
58
  return (
22
59
  <section className="overflow-hidden rounded-2xl border bg-card">
23
60
  <div className="grid lg:grid-cols-[1fr_1.4fr]">
24
- {/* Left panel */}
25
61
  <div className="relative overflow-hidden bg-primary px-8 py-12 text-primary-foreground">
26
62
  <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary-foreground)/0.1),transparent_60%)]" />
27
63
  <div className="relative space-y-6">
28
64
  <div>
29
- <Badge variant="secondary" className="mb-3 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/20">
30
- Contact us
65
+ <Badge
66
+ variant="secondary"
67
+ className="mb-3 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/20"
68
+ >
69
+ {badge}
31
70
  </Badge>
32
- <h2 className="text-2xl font-semibold">Let's talk</h2>
33
- <p className="mt-2 text-sm text-primary-foreground/70">
34
- Have a question or want to work together? Fill in the form and we'll get back to you within one business day.
35
- </p>
71
+ <h2 className="text-2xl font-semibold">{title}</h2>
72
+ <p className="mt-2 text-sm text-primary-foreground/70">{description}</p>
36
73
  </div>
37
74
 
38
75
  <Separator className="bg-primary-foreground/20" />
39
76
 
40
77
  <div className="space-y-4">
41
- {contactInfo.map(({ Icon, label, value }) => (
78
+ {contactInfo.map(({ icon: Icon = Mail, label, value }) => (
42
79
  <div key={label} className="flex items-center gap-3">
43
80
  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary-foreground/10">
44
81
  <Icon className="size-4" />
@@ -53,7 +90,6 @@ export function ContactSection() {
53
90
  </div>
54
91
  </div>
55
92
 
56
- {/* Form */}
57
93
  <div className="p-8">
58
94
  {submitted ? (
59
95
  <div className="flex h-full min-h-[320px] flex-col items-center justify-center gap-4 text-center">
@@ -61,12 +97,12 @@ export function ContactSection() {
61
97
  <MessageSquare className="size-6" />
62
98
  </div>
63
99
  <div>
64
- <h3 className="font-semibold">Message sent</h3>
65
- <p className="mt-1 text-sm text-muted-foreground">
66
- Thanks for reaching out. We'll get back to you within one business day.
67
- </p>
100
+ <h3 className="font-semibold">{successTitle}</h3>
101
+ <p className="mt-1 text-sm text-muted-foreground">{successDescription}</p>
68
102
  </div>
69
- <Button variant="outline" onClick={() => setSubmitted(false)}>Send another</Button>
103
+ <Button variant="outline" onClick={() => setSubmitted(false)}>
104
+ {sendAnotherLabel}
105
+ </Button>
70
106
  </div>
71
107
  ) : (
72
108
  <form onSubmit={handleSubmit} className="space-y-5">
@@ -4,6 +4,7 @@ import * as React from 'react';
4
4
  import { ArrowRight } from 'lucide-react';
5
5
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button } from '@olwiba/cn';
6
6
  import { FadeIn } from '../motion/FadeIn';
7
+ import { PhoneFrame } from '../components/PhoneFrame';
7
8
  import type { AppShellRenderLink } from '../app/AppShell';
8
9
 
9
10
  export interface HeroSectionProps {
@@ -12,7 +13,9 @@ export interface HeroSectionProps {
12
13
  description: string;
13
14
  primaryCta: { label: string; href: string };
14
15
  secondaryCta?: { label: string; href: string };
15
- heroImage: React.ReactNode;
16
+ heroImage?: React.ReactNode;
17
+ media?: 'image' | 'phone' | 'none';
18
+ phoneSize?: 'sm' | 'md' | 'lg';
16
19
  avatarUrls?: string[];
17
20
  socialProofText?: string;
18
21
  renderLink?: AppShellRenderLink;
@@ -29,6 +32,8 @@ export function HeroSection({
29
32
  primaryCta,
30
33
  secondaryCta,
31
34
  heroImage,
35
+ media = 'image',
36
+ phoneSize = 'lg',
32
37
  avatarUrls,
33
38
  socialProofText,
34
39
  renderLink = defaultRenderLink,
@@ -74,12 +79,22 @@ export function HeroSection({
74
79
  </div>
75
80
  </FadeIn>
76
81
 
77
- {/* Right - hero image */}
78
- <FadeIn direction="right" delay={200}>
79
- <div className="overflow-hidden rounded-2xl">
80
- {heroImage}
81
- </div>
82
- </FadeIn>
82
+ {/* Right - media */}
83
+ {media !== 'none' && (
84
+ <FadeIn direction="right" delay={200}>
85
+ {media === 'phone' ? (
86
+ <div className="flex justify-center">
87
+ <PhoneFrame size={phoneSize}>
88
+ {heroImage}
89
+ </PhoneFrame>
90
+ </div>
91
+ ) : (
92
+ <div className="overflow-hidden rounded-2xl">
93
+ {heroImage}
94
+ </div>
95
+ )}
96
+ </FadeIn>
97
+ )}
83
98
  </div>
84
99
 
85
100
  {/* Avatar social proof row */}
@@ -4,13 +4,51 @@ import * as React from 'react';
4
4
  import { ArrowRight, Mail } from 'lucide-react';
5
5
  import { Badge, Button, Input } from '@olwiba/cn';
6
6
 
7
- export function NewsletterSection() {
7
+ export interface NewsletterSectionProps {
8
+ badge?: string;
9
+ title?: string;
10
+ description?: string;
11
+ placeholder?: string;
12
+ submitLabel?: string;
13
+ privacyHref?: string;
14
+ privacyLabel?: string;
15
+ successTitle?: string;
16
+ successDescription?: string;
17
+ onSubscribe?: (email: string) => void | Promise<void>;
18
+ }
19
+
20
+ const defaults = {
21
+ badge: 'Newsletter',
22
+ title: 'Stay in the loop',
23
+ description:
24
+ 'New components, release notes, and product tips — straight to your inbox. No spam, unsubscribe any time.',
25
+ placeholder: 'you@company.com',
26
+ submitLabel: 'Subscribe',
27
+ privacyHref: '#',
28
+ privacyLabel: 'Privacy Policy',
29
+ successTitle: "You're on the list.",
30
+ successDescription: "We'll be in touch soon.",
31
+ };
32
+
33
+ export function NewsletterSection({
34
+ badge = defaults.badge,
35
+ title = defaults.title,
36
+ description = defaults.description,
37
+ placeholder = defaults.placeholder,
38
+ submitLabel = defaults.submitLabel,
39
+ privacyHref = defaults.privacyHref,
40
+ privacyLabel = defaults.privacyLabel,
41
+ successTitle = defaults.successTitle,
42
+ successDescription = defaults.successDescription,
43
+ onSubscribe,
44
+ }: NewsletterSectionProps = {}) {
8
45
  const [email, setEmail] = React.useState('');
9
46
  const [submitted, setSubmitted] = React.useState(false);
10
47
 
11
- function handleSubmit(e: React.FormEvent) {
48
+ async function handleSubmit(e: React.FormEvent) {
12
49
  e.preventDefault();
13
50
  if (!email) return;
51
+ await onSubscribe?.(email);
14
52
  setSubmitted(true);
15
53
  }
16
54
 
@@ -23,31 +61,29 @@ export function NewsletterSection() {
23
61
  <Mail className="size-5" />
24
62
  </div>
25
63
 
26
- <Badge variant="secondary" className="mb-3">Newsletter</Badge>
27
- <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
28
- Stay in the loop
29
- </h2>
30
- <p className="mx-auto mt-4 max-w-sm text-pretty text-muted-foreground">
31
- New components, release notes, and product tips — straight to your inbox. No spam, unsubscribe any time.
32
- </p>
64
+ <Badge variant="secondary" className="mb-3">
65
+ {badge}
66
+ </Badge>
67
+ <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">{title}</h2>
68
+ <p className="mx-auto mt-4 max-w-sm text-pretty text-muted-foreground">{description}</p>
33
69
 
34
70
  {submitted ? (
35
71
  <div className="mt-8 rounded-2xl border bg-muted/50 px-6 py-5 text-sm">
36
- <span className="font-medium">You're on the list.</span>{' '}
37
- <span className="text-muted-foreground">We'll be in touch soon.</span>
72
+ <span className="font-medium">{successTitle}</span>{' '}
73
+ <span className="text-muted-foreground">{successDescription}</span>
38
74
  </div>
39
75
  ) : (
40
76
  <form onSubmit={handleSubmit} className="mt-8 flex flex-col gap-3 sm:flex-row">
41
77
  <Input
42
78
  type="email"
43
- placeholder="you@company.com"
79
+ placeholder={placeholder}
44
80
  value={email}
45
81
  onChange={(e) => setEmail(e.target.value)}
46
82
  required
47
83
  className="flex-1"
48
84
  />
49
85
  <Button type="submit">
50
- Subscribe
86
+ {submitLabel}
51
87
  <ArrowRight className="ml-2 size-4" />
52
88
  </Button>
53
89
  </form>
@@ -55,7 +91,10 @@ export function NewsletterSection() {
55
91
 
56
92
  <p className="mt-4 text-xs text-muted-foreground">
57
93
  By subscribing you agree to our{' '}
58
- <a href="#" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>.
94
+ <a href={privacyHref} className="underline underline-offset-4 hover:text-foreground">
95
+ {privacyLabel}
96
+ </a>
97
+ .
59
98
  </p>
60
99
  </div>
61
100
  </div>