Skip to content

Commit

Permalink
feat: add share links
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelblijleven committed Jan 8, 2024
1 parent 159376b commit 4d64545
Show file tree
Hide file tree
Showing 26 changed files with 438 additions and 151 deletions.
35 changes: 35 additions & 0 deletions src/app/beanconqueror/(share)/shorten/[shareId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {getShareEntry} from "@/lib/db/shares/get-share-entry";
import {notFound} from "next/navigation";
import CopyContainer from "@/components/copy-container";
import {QRCode} from "@/components/qrcode-card";

export default async function ShareIdPage({params}: {params: {shareId: string}}) {
const shareEntry = await getShareEntry(params.shareId);

if (!shareEntry?.beanconquerorUrl) {
return notFound();
}

const shareUrl = `https://beanstats.com/s/${shareEntry.publicId}`;

return (
<div className={"flex flex-col items-center space-y-6"}>
<section className={"text-center max-w-xl space-y-6"}>
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
Share a <span className={"gradient-text"}>Beanconqueror</span> link
</h1>
<p className={"text-center"}>
Your share link has been shortened, copy the link or scan the qrcode with your phone camera.
</p>
</section>
<section className={"w-full text-center"}>
<h3 className={"text-2xl font-semibold"}>{shareEntry.name}</h3>
<div className={"italic"}>
Roasted by {shareEntry.roaster}
</div>
</section>
<CopyContainer value={shareUrl} />
<QRCode value={shareUrl} />
</div>
);
}
26 changes: 26 additions & 0 deletions src/app/beanconqueror/(share)/shorten/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use server";

import {redirect} from "next/navigation";
import {getShortShareEntry} from "@/lib/share/actions";
import {shortLinkFormSchema} from "@/components/beanconqueror/share/shorten/schema";

export type ShortenLinkFormState = {
error: string | null
}

export async function processShortenLink(prevState: ShortenLinkFormState, formData: FormData): Promise<ShortenLinkFormState> {
const link = formData.get("link") as string | null;
const parsed = shortLinkFormSchema.shape.link.safeParse(link);

if (!parsed.success || !link) {
return {error: "invalid or missing link provided"};
}

const share = await getShortShareEntry(link);

if (!share?.publicId) {
return {error: "something went wrong while shortening the link"};
}

redirect(`/beanconqueror/shorten/${share.publicId}`);
}
38 changes: 19 additions & 19 deletions src/app/beanconqueror/(share)/shorten/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import {type Metadata} from "next";

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

export const metadata: Metadata = {
title: "Shorten Beanconqueror link",
description: "Shorten a Beanconqueror (share) link using this Beanlink",
openGraph: {
title: "Shorten a (share) link",
description: "Shorten a Beanconqueror (share) link using Beanlink",
images: ["/beanconqueror_logo.png"],
},
title: "Shorten Beanconqueror link",
description: "Shorten a Beanconqueror (share) link",
openGraph: {
title: "Shorten a (share) link",
description: "Shorten a Beanconqueror (share) link",
images: ["/beanconqueror_logo.png"],
},
};

export default function CreateShareLinkPage() {
return (
<PageShell>
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
Shorten a <span className={"gradient-text"}>Beanconqueror</span> share link
</h1>
<p className={"text-center"}>
This form uses Beanlink to shorten a Beanconqueror share link.
</p>
<ShortenContainer link={null} />
</PageShell>
);
return (
<PageShell>
<h1 className={"text-4xl md:text-6xl font-bold text-center"}>
Shorten a <span className={"gradient-text"}>Beanconqueror</span> share link
</h1>
<p className={"text-center"}>
This form creates a shorter and easier-to-share Beanconqueror share link.
</p>
<ShortLinkForm />
</PageShell>
);
}
3 changes: 3 additions & 0 deletions src/app/s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# s

s path contains the share links and is short for share
47 changes: 47 additions & 0 deletions src/app/s/[shareId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {type Metadata} from "next";
import {notFound, redirect} from "next/navigation";

import {getShareEntry} from "@/lib/db/shares/get-share-entry";

export async function generateMetadata({params}: {params: {shareId: string}}): Promise<Metadata> {
const shareEntry = await getShareEntry(params.shareId);

if (!shareEntry) {
return {
title: "Beanconqueror import link",
description: "Provided by Beanstats",
openGraph: {
title: "Beanconqueror import link",
description: "Import this coffee into your Beanconqueror app",
images: ["/beanconqueror_logo.png"],
},
};
}

const roaster = !!shareEntry.roaster ? ` roasted by ${shareEntry.roaster ?? "??"}` : "";

return {
title: `${shareEntry.name}${roaster}`,
description: "Provided by Beanstats",
openGraph: {
title: "Beanconqueror import link",
description: "Import this coffee into your Beanconqueror app",
images: ["/beanconqueror_logo.png"],
},
};
}

/**
* Simple redirect page for a shortened share url
* @param params
* @constructor
*/
export default async function ShareLinkPage({params}: {params: {shareId: string}}) {
const shareEntry = await getShareEntry(params.shareId);

if (!shareEntry?.beanconquerorUrl) {
return notFound();
}

redirect(shareEntry.beanconquerorUrl);
}
82 changes: 82 additions & 0 deletions src/components/beanconqueror/share/shorten/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import {useForm} from "react-hook-form";
import {useFormState, useFormStatus} from "react-dom";
import {type z} from "zod";
import {zodResolver} from "@hookform/resolvers/zod";

import {processShortenLink} from "@/app/beanconqueror/(share)/shorten/actions";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {AlertCircle, Loader} from "lucide-react";
import {useToast} from "@/components/ui/use-toast";
import {Button} from "@/components/ui/button";
import {shortLinkFormSchema} from "@/components/beanconqueror/share/shorten/schema";

function SubmitButton() {
const {pending} = useFormStatus();
return (
<Button type={"submit"} aria-disabled={pending} disabled={pending}>
{pending ? "Shortening" : "Shorten"}
{pending && <Loader className={"animate-spin h-4 w-4 ml-2"} />}
</Button>
)
}

export function ShortLinkForm() {
const form = useForm<z.infer<typeof shortLinkFormSchema>>({
resolver: zodResolver(shortLinkFormSchema),
defaultValues: {
link: "",
}
});
const [state, action] = useFormState(processShortenLink, {error: null});
const {toast} = useToast();

const validateData = (formData: FormData) => {
const parseResult = shortLinkFormSchema.shape.link.safeParse(formData.get("link"));

if (!parseResult.success) {
toast({
title: "Invalid url",
description: "Did not receive a valid Beanconqueror url",
variant: "destructive",
});
return;
}
action(formData);
};

return (
<Form {...form}>
<form action={validateData}>
<fieldset className={"flex items-end gap-2"}>
<FormField<z.infer<typeof shortLinkFormSchema>>
name={"link"}
control={form.control}
render={({field}) => (
<FormItem>
<FormLabel>Link</FormLabel>
<FormControl>
<Input placeholder={"Enter here"} {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<SubmitButton />
</fieldset>
{!!state.error && (
<Alert variant={"destructive"}>
<AlertCircle className={"h-4 w-4"}/>
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{state.error}
</AlertDescription>
</Alert>
)}
</form>
</Form>
);
}
8 changes: 8 additions & 0 deletions src/components/beanconqueror/share/shorten/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {z} from "zod";
import {BEANCONQUEROR_RE} from "@/lib/beanconqueror/validations/links";

export const shortLinkFormSchema = z.object(
{
link: z.string().url().regex(BEANCONQUEROR_RE, {message: "Provide a valid Beanconqueror url"})
}
);

This file was deleted.

18 changes: 6 additions & 12 deletions src/components/beanconqueror/share/view/shared-bean.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// https://beanconqueror.com?shareUserBean0=ChtSd2FuZGEgcm9vdHMgb3JpZ2luIEludGFuZ28SGDIwMjMtMDYtMTNUMDk6MzQ6MDAuMDAwWhoYMjAyMy0wNS0yM1QwOTozNDowMC4wMDBaIgAqIlRleGVsc2UgQnJhbmRpbmcgKFRleGVsIPCfh7Pwn4exKSAyADgAQABIAVIAWgpSZWQgRnJ1aXRzYPoBaABwC4IBAIgBAJIBAJoBAKABAKoBKwoGUndhbmRhEglMYWtlIEtpdnUaB0ludGFuZ28qBDE1MDBCB05hdHVyYWywAQC6ARYIABAAGgAgACgAMAA6AEAASABQAFgAwgEAyAEA0AEA2gEWCAAQABgAIAAoADAAOABAAEgAUABYAOIBAgoA
// https://beanconqueror.com?shareUserBean0=CglGYWtlIG5hbWUSGDIwMjMtMDYtMTVUMTM6NDI6MDAuMDAwWhoYMjAyMy0wNi0xNVQxMzo0MjowMC4wMDBaIgtGYWtlIG5vdGVzICoMRmFrZSByb2FzdGVyMgA4DUADSAJSDWN1c3RvbSBkZWdyZWVaD0xla2tlciwgcHJvZmllbGB7aABwDIIBBTM0LjU2iAEBkgEDVXJsmgEDRWFuoAEAqgF8Cg5GYWtlIGNvdW50cnkgMRINRmFrZSByZWdpb24gMRoLRmFrZSBmYXJtIDEiDUZha2UgZmFybWVyIDEqBDE1MDAyC0hhcnZlc3RlZCAxOglWYXJpZXR5IDFCDFByb2Nlc3NpbmcgMUoHQ2VydGkgMVAMWMDEB2DShdjMBKoBFQoJQ291bnRy&shareUserBean1=eSAyEghSZWdpb24gMrABAboBFggAEAAaACAAKAAwADoAQABIAFAAWADCAQDIAQDQAQDaARYIABAAGAAgACgAMAA4AEAASABQAFgA4gECCgA=
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {useToast} from "@/components/ui/use-toast";
import {decodeMessage} from "@/lib/beanconqueror/proto";
import {beanconqueror} from "@/lib/beanconqueror/proto/generated/beanconqueror";
import {BEANLINK_RE} from "@/lib/beanconqueror/validations/links";
import {type BeanLinkResponse, followBeanLink} from "@/lib/beanlink";
import {followBeanLink} from "@/lib/beanlink";
import {getTextWithFlagSupport} from "@/lib/flags";

import Roast = beanconqueror.Roast;
Expand All @@ -16,13 +14,12 @@ import BeanProto = beanconqueror.BeanProto;
import IBeanInformation = beanconqueror.IBeanInformation;

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

import {useEffect, useState} from "react";

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

const GeneralTabsContent = ({decoded}: {decoded: BeanProto}) => (
<>
Expand Down Expand Up @@ -108,7 +105,7 @@ const VarietyTabsContent = ({decoded}: {decoded: BeanProto}) => (

const SharedBean = ({url}: { url: string}) => {
const [viewUrl, setViewUrl] = useState<string>(url);
const [data, setData] = useState<BeanLinkResponse | null>(null);
const [data, setData] = useState<ShareEntryType>();
const {toast} = useToast();

let err;
Expand Down Expand Up @@ -163,14 +160,11 @@ const SharedBean = ({url}: { url: string}) => {
<TabsContent value={"share"} className={"flex flex-col space-y-4"}>
{!data && <ShortenLinkForm
link={viewUrl}
callback={(data: BeanLinkResponse) => setData(data)}
callback={(data: ShareEntryType) => setData(data)}
buttonText={"Create share link"}
/>}
{!!data && (
<>
<BeanLinkCard response={data} />
<QRCodeCard value={data.link} />
</>
{!!data?.publicId && (
<ShortShareCard entry={data as CheckedShareEntryType} />
)}
</TabsContent>
</Tabs>
Expand Down
Loading

0 comments on commit 4d64545

Please sign in to comment.