@olwiba/ui 0.0.36 → 0.0.38
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/dist/index.d.ts +49 -3
- package/dist/index.js +125 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/blog/ChangelogCard.tsx +73 -0
- package/src/blog/ChangelogList.tsx +35 -0
- package/src/index.ts +4 -2
- package/src/marketing/ContactSection.tsx +57 -21
- package/src/marketing/NewsletterSection.tsx +53 -14
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Badge, cn } from '@olwiba/cn';
|
|
5
|
+
import type { AppShellRenderLink } from '../app/AppShell';
|
|
6
|
+
|
|
7
|
+
export interface ChangelogCardProps {
|
|
8
|
+
title: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
date: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
hrefPrefix?: string;
|
|
14
|
+
renderLink?: AppShellRenderLink;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDate(dateStr: string) {
|
|
18
|
+
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
19
|
+
year: 'numeric',
|
|
20
|
+
month: 'long',
|
|
21
|
+
day: 'numeric',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ChangelogCard({
|
|
26
|
+
title,
|
|
27
|
+
summary,
|
|
28
|
+
date,
|
|
29
|
+
slug,
|
|
30
|
+
version,
|
|
31
|
+
hrefPrefix = '/changelog',
|
|
32
|
+
renderLink,
|
|
33
|
+
}: ChangelogCardProps) {
|
|
34
|
+
const href = `${hrefPrefix}/${slug}`;
|
|
35
|
+
|
|
36
|
+
const LinkWrapper = ({
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
}: {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
className?: string;
|
|
42
|
+
}) =>
|
|
43
|
+
renderLink ? (
|
|
44
|
+
renderLink({ href, children, className })
|
|
45
|
+
) : (
|
|
46
|
+
<a href={href} className={className}>
|
|
47
|
+
{children}
|
|
48
|
+
</a>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<article className="group flex flex-col gap-3 border-b border-border/60 pb-8 last:border-0 last:pb-0">
|
|
53
|
+
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
|
54
|
+
<time dateTime={date}>{formatDate(date)}</time>
|
|
55
|
+
{version && (
|
|
56
|
+
<>
|
|
57
|
+
<span>·</span>
|
|
58
|
+
<Badge variant="secondary" className="font-mono text-xs">
|
|
59
|
+
{version}
|
|
60
|
+
</Badge>
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
<h2 className="text-lg font-semibold leading-snug tracking-tight">
|
|
65
|
+
<LinkWrapper className="transition-colors hover:text-primary">{title}</LinkWrapper>
|
|
66
|
+
</h2>
|
|
67
|
+
<p className={cn('line-clamp-3 text-sm text-muted-foreground')}>{summary}</p>
|
|
68
|
+
<LinkWrapper className="text-sm font-medium text-primary hover:underline">
|
|
69
|
+
Read release notes →
|
|
70
|
+
</LinkWrapper>
|
|
71
|
+
</article>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { AppShellRenderLink } from '../app/AppShell';
|
|
4
|
+
import { ChangelogCard, type ChangelogCardProps } from './ChangelogCard';
|
|
5
|
+
|
|
6
|
+
export interface ChangelogListProps {
|
|
7
|
+
entries: ChangelogCardProps[];
|
|
8
|
+
renderLink?: AppShellRenderLink;
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
hrefPrefix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChangelogList({
|
|
14
|
+
entries,
|
|
15
|
+
renderLink,
|
|
16
|
+
emptyMessage = 'No changelog entries yet.',
|
|
17
|
+
hrefPrefix,
|
|
18
|
+
}: ChangelogListProps) {
|
|
19
|
+
if (entries.length === 0) {
|
|
20
|
+
return <p className="text-sm italic text-muted-foreground">{emptyMessage}</p>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-8">
|
|
25
|
+
{entries.map((entry) => (
|
|
26
|
+
<ChangelogCard
|
|
27
|
+
key={entry.slug}
|
|
28
|
+
{...entry}
|
|
29
|
+
hrefPrefix={hrefPrefix}
|
|
30
|
+
renderLink={renderLink}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
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';
|
|
@@ -107,6 +107,8 @@ export { RootErrorFallback } from './components/RootErrorFallback';
|
|
|
107
107
|
// ─── Blog ────────────────────────────────────────────────────────────────────
|
|
108
108
|
export { PostCard, type PostCardProps } from './blog/PostCard';
|
|
109
109
|
export { PostList, type PostListProps } from './blog/PostList';
|
|
110
|
+
export { ChangelogList, type ChangelogListProps } from './blog/ChangelogList';
|
|
111
|
+
export { ChangelogCard, type ChangelogCardProps } from './blog/ChangelogCard';
|
|
110
112
|
export { MdxContent, type MdxContentProps } from './blog/MdxContent';
|
|
111
113
|
|
|
112
114
|
// ─── Hooks ────────────────────────────────────────────────────────────────────
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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">
|
|
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">
|
|
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)}>
|
|
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,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
|
|
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">
|
|
27
|
-
|
|
28
|
-
|
|
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">
|
|
37
|
-
<span className="text-muted-foreground">
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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>
|