Skip to content

Commit 4d64545

Browse files
feat: add share links
1 parent 159376b commit 4d64545

File tree

26 files changed

+438
-151
lines changed

26 files changed

+438
-151
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {getShareEntry} from "@/lib/db/shares/get-share-entry";
2+
import {notFound} from "next/navigation";
3+
import CopyContainer from "@/components/copy-container";
4+
import {QRCode} from "@/components/qrcode-card";
5+
6+
export default async function ShareIdPage({params}: {params: {shareId: string}}) {
7+
const shareEntry = await getShareEntry(params.shareId);
8+
9+
if (!shareEntry?.beanconquerorUrl) {
10+
return notFound();
11+
}
12+
13+
const shareUrl = `https://beanstats.com/s/${shareEntry.publicId}`;
14+
15+
return (
16+
<div className={"flex flex-col items-center space-y-6"}>
17+
<section className={"text-center max-w-xl space-y-6"}>
18+
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
19+
Share a <span className={"gradient-text"}>Beanconqueror</span> link
20+
</h1>
21+
<p className={"text-center"}>
22+
Your share link has been shortened, copy the link or scan the qrcode with your phone camera.
23+
</p>
24+
</section>
25+
<section className={"w-full text-center"}>
26+
<h3 className={"text-2xl font-semibold"}>{shareEntry.name}</h3>
27+
<div className={"italic"}>
28+
Roasted by {shareEntry.roaster}
29+
</div>
30+
</section>
31+
<CopyContainer value={shareUrl} />
32+
<QRCode value={shareUrl} />
33+
</div>
34+
);
35+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use server";
2+
3+
import {redirect} from "next/navigation";
4+
import {getShortShareEntry} from "@/lib/share/actions";
5+
import {shortLinkFormSchema} from "@/components/beanconqueror/share/shorten/schema";
6+
7+
export type ShortenLinkFormState = {
8+
error: string | null
9+
}
10+
11+
export async function processShortenLink(prevState: ShortenLinkFormState, formData: FormData): Promise<ShortenLinkFormState> {
12+
const link = formData.get("link") as string | null;
13+
const parsed = shortLinkFormSchema.shape.link.safeParse(link);
14+
15+
if (!parsed.success || !link) {
16+
return {error: "invalid or missing link provided"};
17+
}
18+
19+
const share = await getShortShareEntry(link);
20+
21+
if (!share?.publicId) {
22+
return {error: "something went wrong while shortening the link"};
23+
}
24+
25+
redirect(`/beanconqueror/shorten/${share.publicId}`);
26+
}
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
import {type Metadata} from "next";
22

3-
import {ShortenContainer} from "@/components/beanconqueror/share/shorten/shorten-link-container";
3+
import {ShortLinkForm} from "@/components/beanconqueror/share/shorten/form";
44
import PageShell from "@/components/layout/page-shell";
55

66
export const metadata: Metadata = {
7-
title: "Shorten Beanconqueror link",
8-
description: "Shorten a Beanconqueror (share) link using this Beanlink",
9-
openGraph: {
10-
title: "Shorten a (share) link",
11-
description: "Shorten a Beanconqueror (share) link using Beanlink",
12-
images: ["/beanconqueror_logo.png"],
13-
},
7+
title: "Shorten Beanconqueror link",
8+
description: "Shorten a Beanconqueror (share) link",
9+
openGraph: {
10+
title: "Shorten a (share) link",
11+
description: "Shorten a Beanconqueror (share) link",
12+
images: ["/beanconqueror_logo.png"],
13+
},
1414
};
1515

1616
export default function CreateShareLinkPage() {
17-
return (
18-
<PageShell>
19-
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
20-
Shorten a <span className={"gradient-text"}>Beanconqueror</span> share link
21-
</h1>
22-
<p className={"text-center"}>
23-
This form uses Beanlink to shorten a Beanconqueror share link.
24-
</p>
25-
<ShortenContainer link={null} />
26-
</PageShell>
27-
);
17+
return (
18+
<PageShell>
19+
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
20+
Shorten a <span className={"gradient-text"}>Beanconqueror</span> share link
21+
</h1>
22+
<p className={"text-center"}>
23+
This form creates a shorter and easier-to-share Beanconqueror share link.
24+
</p>
25+
<ShortLinkForm />
26+
</PageShell>
27+
);
2828
}

src/app/s/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# s
2+
3+
s path contains the share links and is short for share

src/app/s/[shareId]/page.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {type Metadata} from "next";
2+
import {notFound, redirect} from "next/navigation";
3+
4+
import {getShareEntry} from "@/lib/db/shares/get-share-entry";
5+
6+
export async function generateMetadata({params}: {params: {shareId: string}}): Promise<Metadata> {
7+
const shareEntry = await getShareEntry(params.shareId);
8+
9+
if (!shareEntry) {
10+
return {
11+
title: "Beanconqueror import link",
12+
description: "Provided by Beanstats",
13+
openGraph: {
14+
title: "Beanconqueror import link",
15+
description: "Import this coffee into your Beanconqueror app",
16+
images: ["/beanconqueror_logo.png"],
17+
},
18+
};
19+
}
20+
21+
const roaster = !!shareEntry.roaster ? ` roasted by ${shareEntry.roaster ?? "??"}` : "";
22+
23+
return {
24+
title: `${shareEntry.name}${roaster}`,
25+
description: "Provided by Beanstats",
26+
openGraph: {
27+
title: "Beanconqueror import link",
28+
description: "Import this coffee into your Beanconqueror app",
29+
images: ["/beanconqueror_logo.png"],
30+
},
31+
};
32+
}
33+
34+
/**
35+
* Simple redirect page for a shortened share url
36+
* @param params
37+
* @constructor
38+
*/
39+
export default async function ShareLinkPage({params}: {params: {shareId: string}}) {
40+
const shareEntry = await getShareEntry(params.shareId);
41+
42+
if (!shareEntry?.beanconquerorUrl) {
43+
return notFound();
44+
}
45+
46+
redirect(shareEntry.beanconquerorUrl);
47+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import {useForm} from "react-hook-form";
4+
import {useFormState, useFormStatus} from "react-dom";
5+
import {type z} from "zod";
6+
import {zodResolver} from "@hookform/resolvers/zod";
7+
8+
import {processShortenLink} from "@/app/beanconqueror/(share)/shorten/actions";
9+
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
10+
import {Input} from "@/components/ui/input";
11+
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
12+
import {AlertCircle, Loader} from "lucide-react";
13+
import {useToast} from "@/components/ui/use-toast";
14+
import {Button} from "@/components/ui/button";
15+
import {shortLinkFormSchema} from "@/components/beanconqueror/share/shorten/schema";
16+
17+
function SubmitButton() {
18+
const {pending} = useFormStatus();
19+
return (
20+
<Button type={"submit"} aria-disabled={pending} disabled={pending}>
21+
{pending ? "Shortening" : "Shorten"}
22+
{pending && <Loader className={"animate-spin h-4 w-4 ml-2"} />}
23+
</Button>
24+
)
25+
}
26+
27+
export function ShortLinkForm() {
28+
const form = useForm<z.infer<typeof shortLinkFormSchema>>({
29+
resolver: zodResolver(shortLinkFormSchema),
30+
defaultValues: {
31+
link: "",
32+
}
33+
});
34+
const [state, action] = useFormState(processShortenLink, {error: null});
35+
const {toast} = useToast();
36+
37+
const validateData = (formData: FormData) => {
38+
const parseResult = shortLinkFormSchema.shape.link.safeParse(formData.get("link"));
39+
40+
if (!parseResult.success) {
41+
toast({
42+
title: "Invalid url",
43+
description: "Did not receive a valid Beanconqueror url",
44+
variant: "destructive",
45+
});
46+
return;
47+
}
48+
action(formData);
49+
};
50+
51+
return (
52+
<Form {...form}>
53+
<form action={validateData}>
54+
<fieldset className={"flex items-end gap-2"}>
55+
<FormField<z.infer<typeof shortLinkFormSchema>>
56+
name={"link"}
57+
control={form.control}
58+
render={({field}) => (
59+
<FormItem>
60+
<FormLabel>Link</FormLabel>
61+
<FormControl>
62+
<Input placeholder={"Enter here"} {...field} />
63+
</FormControl>
64+
<FormMessage/>
65+
</FormItem>
66+
)}
67+
/>
68+
<SubmitButton />
69+
</fieldset>
70+
{!!state.error && (
71+
<Alert variant={"destructive"}>
72+
<AlertCircle className={"h-4 w-4"}/>
73+
<AlertTitle>Error</AlertTitle>
74+
<AlertDescription>
75+
{state.error}
76+
</AlertDescription>
77+
</Alert>
78+
)}
79+
</form>
80+
</Form>
81+
);
82+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {z} from "zod";
2+
import {BEANCONQUEROR_RE} from "@/lib/beanconqueror/validations/links";
3+
4+
export const shortLinkFormSchema = z.object(
5+
{
6+
link: z.string().url().regex(BEANCONQUEROR_RE, {message: "Provide a valid Beanconqueror url"})
7+
}
8+
);

src/components/beanconqueror/share/shorten/shorten-link-container.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/components/beanconqueror/share/view/shared-bean.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
// https://beanconqueror.com?shareUserBean0=ChtSd2FuZGEgcm9vdHMgb3JpZ2luIEludGFuZ28SGDIwMjMtMDYtMTNUMDk6MzQ6MDAuMDAwWhoYMjAyMy0wNS0yM1QwOTozNDowMC4wMDBaIgAqIlRleGVsc2UgQnJhbmRpbmcgKFRleGVsIPCfh7Pwn4exKSAyADgAQABIAVIAWgpSZWQgRnJ1aXRzYPoBaABwC4IBAIgBAJIBAJoBAKABAKoBKwoGUndhbmRhEglMYWtlIEtpdnUaB0ludGFuZ28qBDE1MDBCB05hdHVyYWywAQC6ARYIABAAGgAgACgAMAA6AEAASABQAFgAwgEAyAEA0AEA2gEWCAAQABgAIAAoADAAOABAAEgAUABYAOIBAgoA
2-
// https://beanconqueror.com?shareUserBean0=CglGYWtlIG5hbWUSGDIwMjMtMDYtMTVUMTM6NDI6MDAuMDAwWhoYMjAyMy0wNi0xNVQxMzo0MjowMC4wMDBaIgtGYWtlIG5vdGVzICoMRmFrZSByb2FzdGVyMgA4DUADSAJSDWN1c3RvbSBkZWdyZWVaD0xla2tlciwgcHJvZmllbGB7aABwDIIBBTM0LjU2iAEBkgEDVXJsmgEDRWFuoAEAqgF8Cg5GYWtlIGNvdW50cnkgMRINRmFrZSByZWdpb24gMRoLRmFrZSBmYXJtIDEiDUZha2UgZmFybWVyIDEqBDE1MDAyC0hhcnZlc3RlZCAxOglWYXJpZXR5IDFCDFByb2Nlc3NpbmcgMUoHQ2VydGkgMVAMWMDEB2DShdjMBKoBFQoJQ291bnRy&shareUserBean1=eSAyEghSZWdpb24gMrABAboBFggAEAAaACAAKAAwADoAQABIAFAAWADCAQDIAQDQAQDaARYIABAAGAAgACgAMAA4AEAASABQAFgA4gECCgA=
31
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
42
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
53
import {useToast} from "@/components/ui/use-toast";
64
import {decodeMessage} from "@/lib/beanconqueror/proto";
75
import {beanconqueror} from "@/lib/beanconqueror/proto/generated/beanconqueror";
86
import {BEANLINK_RE} from "@/lib/beanconqueror/validations/links";
9-
import {type BeanLinkResponse, followBeanLink} from "@/lib/beanlink";
7+
import {followBeanLink} from "@/lib/beanlink";
108
import {getTextWithFlagSupport} from "@/lib/flags";
119

1210
import Roast = beanconqueror.Roast;
@@ -16,13 +14,12 @@ import BeanProto = beanconqueror.BeanProto;
1614
import IBeanInformation = beanconqueror.IBeanInformation;
1715

1816
import LabelledValue from "@/components/beanconqueror/share/view/labelled-value";
19-
import QRCodeCard from "@/components/qrcode-card";
2017

2118
import {useEffect, useState} from "react";
2219

2320
import {Alert} from "@/components/alert";
2421
import {ShortenLinkForm} from "@/components/forms/shorten-link-form";
25-
import {BeanLinkCard} from "@/components/share-card";
22+
import {type CheckedShareEntryType, type ShareEntryType, ShortShareCard} from "@/components/share-card";
2623

2724
const GeneralTabsContent = ({decoded}: {decoded: BeanProto}) => (
2825
<>
@@ -108,7 +105,7 @@ const VarietyTabsContent = ({decoded}: {decoded: BeanProto}) => (
108105

109106
const SharedBean = ({url}: { url: string}) => {
110107
const [viewUrl, setViewUrl] = useState<string>(url);
111-
const [data, setData] = useState<BeanLinkResponse | null>(null);
108+
const [data, setData] = useState<ShareEntryType>();
112109
const {toast} = useToast();
113110

114111
let err;
@@ -163,14 +160,11 @@ const SharedBean = ({url}: { url: string}) => {
163160
<TabsContent value={"share"} className={"flex flex-col space-y-4"}>
164161
{!data && <ShortenLinkForm
165162
link={viewUrl}
166-
callback={(data: BeanLinkResponse) => setData(data)}
163+
callback={(data: ShareEntryType) => setData(data)}
167164
buttonText={"Create share link"}
168165
/>}
169-
{!!data && (
170-
<>
171-
<BeanLinkCard response={data} />
172-
<QRCodeCard value={data.link} />
173-
</>
166+
{!!data?.publicId && (
167+
<ShortShareCard entry={data as CheckedShareEntryType} />
174168
)}
175169
</TabsContent>
176170
</Tabs>

0 commit comments

Comments
 (0)