Skip to content

Commit

Permalink
Shorten links for sharing and convert dialogs to @shadcn/ui (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJustChew authored May 30, 2024
2 parents ec057ac + 9bb0974 commit e4ba70e
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 136 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ NEXT_PUBLIC_SUPABASE_URL=https://cmzdlrqfpuktcczvsobs.supabase.co
SUPABASE_URL=https://cmzdlrqfpuktcczvsobs.supabase.co
NTHU_OAUTH_CLIENT_ID=nthumods
GITHUB_INSTALLATION_ID=50806112
GITHUB_CLIENT_ID=Iv23lisqArfcAbBMGOXe
GITHUB_CLIENT_ID=Iv23lisqArfcAbBMGOXe
CLOUDFLARE_KV_SHORTLINKS_NAMESPACE=4f141ed7ad0b4113b8e61a53710b653f
CLOUDFLARE_WORKER_ACCOUNT_ID=50396718bfac13dffb7727aad0e82150
10 changes: 10 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CWA_API_KEY=YOUR_CWA_API_KEY
NEXTAUTH_SECRET=YOUR_NEXTAUTH_SECRET
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY
NTHU_OAUTH_SECRET_KEY=<from NTHU OAuth>
CRON_SECRET=<vercel cron secret>
CALENDAR_API_KEY=<google calendar api key>
NTHU_HEADLESS_AIS_SIGNING_KEY=<generate a 32-byte key>
GITHUB_APP_PRIVATE_KEY=YOUR_GITHUB_APP_PRIVATE_KEY
NTHU_HEADLESS_AIS_ENCRYPTION_KEY=<generate a 32-byte key>
CLOUDFLARE_KV_API_TOKEN=YOUR_CLOUDFLARE_KV_API_TOKEN
4 changes: 2 additions & 2 deletions src/app/[lang]/(mods-pages)/(side-pages)/issues/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Divider, Switch} from '@mui/joy';
import { Codepen, Database, Globe } from 'lucide-react';
import Link from 'next/link';
import EmptyIssueForm from './EmptyIssueForm';
import { Separator } from '@/components/ui/separator';

const IssueButton = ({title, description, icon, href}: {title: string, description: string, icon: any, href: string}) => {
return (
Expand All @@ -26,7 +26,7 @@ const IssuesPage = () => {
<IssueButton title="Bug/Feature" description="Report a bug or request a feature" icon={<Codepen className='w-8 h-8'/>} href="https://github.com/nthumodifications/courseweb/issues/new/choose"/>
<IssueButton title="Other" description="Report an issue that doesn't fit in the above categories" icon={<Globe className='w-8 h-8'/>} href="mailto:nthumods@googlegroups.com"/>
</div>
<Divider/>
<Separator/>
<div id="dataissue" className="flex flex-col gap-4 py-4">
{/* Explainer of the data sources */}
<h2 className="font-semibold text-xl text-gray-600 dark:text-gray-400 pb-2">{"Data Sources"}</h2>
Expand Down
15 changes: 15 additions & 0 deletions src/app/l/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getShortLink } from "@/lib/cloudflarekv";
import { NextRequest, NextResponse } from "next/server";

export const GET = async (req: NextRequest, props: { params: { slug: string }}) => {
const slug = props.params.slug;
if (!slug) {
return NextResponse.json({ error: { message: 'Invalid Slug'} }, { status: 404 });
}
const url = await getShortLink(slug);
if (url === null) {
return NextResponse.json({ error: { message: 'Link does not exist'} }, { status: 404 });
}
return NextResponse.redirect(url);
}

37 changes: 22 additions & 15 deletions src/components/Timetable/DownloadTimetableDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import useDictionary from '@/dictionaries/useDictionary';
import {Button, DialogContent, DialogTitle, ModalClose, ModalDialog} from '@mui/joy';
import {Download, Image} from 'lucide-react';
import {Download, Image, Loader2} from 'lucide-react';
import Timetable from './Timetable';
import useUserTimetable from '@/hooks/contexts/useUserTimetable';
import { toPng } from 'html-to-image';
import { useCallback, useRef, useState } from 'react';
import { createTimetableFromCourses } from '@/helpers/timetable';
import { MinimalCourse } from '@/types/courses';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
import { DialogTrigger } from '@radix-ui/react-dialog';
import { Button } from '../ui/button';

const DownloadTimetableComponent = () => {
const dict = useDictionary();
Expand Down Expand Up @@ -42,10 +44,12 @@ const DownloadTimetableComponent = () => {
return <>
<Button
onClick={handleConvert}
variant="outlined"
startDecorator={<Image className="w-4 h-4" />}
loading={loading}
>{dict.dialogs.DownloadTimetableDialog.buttons.image}</Button>
variant="outline"
disabled={loading}
>{loading?
<Loader2 className="w-4 h-4 animate-spin"/>
:<><Image className="w-4 h-4 mr-2" /> {dict.dialogs.DownloadTimetableDialog.buttons.image}</>}
</Button>
<div className='relative overflow-hidden'>
<div className='absolute h-[915px] w-[539px] px-2 pt-4 pb-8 grid place-items-center bg-white dark:bg-background' ref={ref}>
<div className='h-[915px] w-[414px]'>
Expand All @@ -56,7 +60,7 @@ const DownloadTimetableComponent = () => {
</>
}

const DownloadTimetableDialog = ({ onClose, icsfileLink }: { onClose: () => void, icsfileLink: string }) => {
const DownloadTimetableDialog = ({ icsfileLink }: { icsfileLink: string }) => {
const dict = useDictionary();

const handleDownloadCalendar = async () => {
Expand All @@ -68,21 +72,24 @@ const DownloadTimetableDialog = ({ onClose, icsfileLink }: { onClose: () => void
link.click();
}

return <ModalDialog>
<ModalClose />
<DialogTitle>{dict.dialogs.DownloadTimetableDialog.title}</DialogTitle>
return <Dialog>
<DialogTrigger asChild>
<Button variant="outline"><Download className="w-4 h-4 mr-1" /> {dict.timetable.actions.download}</Button>
</DialogTrigger>
<DialogContent>
<p>{dict.dialogs.DownloadTimetableDialog.description}</p>
<DialogHeader>
<DialogTitle>{dict.dialogs.DownloadTimetableDialog.title}</DialogTitle>
<DialogDescription>{dict.dialogs.DownloadTimetableDialog.description}</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 pt-4">
<Button
onClick={handleDownloadCalendar}
variant="outlined"
startDecorator={<Download className="w-4 h-4" />}
>{dict.dialogs.DownloadTimetableDialog.buttons.ICS}</Button>
variant="outline"
><Download className="w-4 h-4 mr-2" />{dict.dialogs.DownloadTimetableDialog.buttons.ICS}</Button>
<DownloadTimetableComponent/>
</div>
</DialogContent>
</ModalDialog>
</Dialog>
}

export default DownloadTimetableDialog;
35 changes: 0 additions & 35 deletions src/components/Timetable/ShareRecievedDialog.tsx

This file was deleted.

149 changes: 100 additions & 49 deletions src/components/Timetable/ShareSyncTimetableDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,113 @@
import {
Button,
DialogContent,
DialogTitle,
IconButton,
Input,
ModalClose,
ModalDialog,
} from '@mui/joy';
import { Calendar, Mail, Share } from 'lucide-react';
import { Calendar, Copy, Mail, Share } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import useDictionary from '@/dictionaries/useDictionary';
import { useEffect, useState } from 'react';
import { addShortLink } from '@/lib/cloudflarekv';
import { toast } from '../ui/use-toast';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import Link from 'next/link';
import { Skeleton } from '../ui/skeleton';

const ShareSyncTimetableDialog = ({ onClose, shareLink, webcalLink, onCopy }: { onClose: () => void, shareLink: string, webcalLink: string, onCopy: () => void }) => {

const dict = useDictionary();

return <ModalDialog>
<ModalClose />
<DialogTitle>{dict.dialogs.ShareSyncTimetableDialog.title}</DialogTitle>
<DialogContent>
<p>
{dict.dialogs.ShareSyncTimetableDialog.description}
</p>
<Input
size="lg"
value={shareLink}
endDecorator={
<IconButton variant="solid" onClick={onCopy}>
<Share className="w-5 h-5" />
</IconButton>
} />
const ComponentSkeleton = () => {
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-row gap-4'>
<Skeleton className="flex-1 p-2 bg-gray-100 dark:bg-neutral-800 rounded-md w-[300px] h-[40px]" />
<Skeleton className="w-[40px] h-[40px] rounded-full" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<h3 className="text-lg font-semibold">{dict.dialogs.ShareSyncTimetableDialog['category:qr']}</h3>
<div className='p-2 bg-white rounded-md w-min'>
<QRCodeSVG value={shareLink} />
</div>
<Skeleton className="text-lg font-semibold w-[150px] h-[24px]" />
<Skeleton className='p-2 bg-white rounded-md w-[100px] h-[100px]' />
</div>
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-semibold">{dict.dialogs.ShareSyncTimetableDialog['category:links']}</h3>
<Button
component="a"
// Subject: Here is My Timetable, Body: My Timetable can be found on NTHUMODS at {shareLink}
href={`mailto:?subject=Here is My Timetable&body=My Timetable can be found on NTHUMODS at ${shareLink}`}
target='_blank'
variant="outlined"
startDecorator={<Mail className="w-4 h-4" />}
>{dict.dialogs.ShareSyncTimetableDialog.links.email}</Button>
<Button
disabled={true}
target='_blank'
variant="outlined"
startDecorator={<Calendar className="w-4 h-4" />}
>Sync To Calendar</Button>
<Skeleton className="text-lg font-semibold w-[150px] h-[24px]" />
<Button variant="outline" disabled>
<Skeleton className="w-[200px] h-[40px]" />
</Button>
<Button variant="outline" disabled>
<Skeleton className="w-[200px] h-[40px]" />
</Button>
</div>
</div>
</div>
);
};

const ShareSyncTimetableDialog = ({ shareLink, webcalLink }: { shareLink: string, webcalLink: string }) => {
const [open, setOpen] = useState(false);
const dict = useDictionary();
const [link, setLink] = useState<string | null>(null);

useEffect(() => {
if(open) {
addShortLink(shareLink).then((shortLink) => {
if(typeof shortLink == 'object' && 'error' in shortLink) {
toast({
title: 'Short Link Error',
description: 'Failed to generate short link. Please try again later.',
})
}
setLink(shortLink as string);
});
}
}, [open]);

const handleCopy = () => {
if(link) navigator.clipboard.writeText(link).then(() => {
toast({
title: 'Copied',
description: 'Link copied to clipboard',
})
});
}
return <Dialog open={open} onOpenChange={v => setOpen(v)}>
<DialogTrigger asChild>
<Button variant="outline"><Share className="w-4 h-4 mr-1" /> {dict.timetable.actions.share}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{dict.dialogs.ShareSyncTimetableDialog.title}</DialogTitle>
<DialogDescription>{dict.dialogs.ShareSyncTimetableDialog.description}</DialogDescription>
</DialogHeader>
{link ? <div className='flex flex-col gap-4'>
<div className='flex flex-row gap-4'>
<input type="text" value={link} readOnly className='flex-1 p-2 bg-gray-100 dark:bg-neutral-800 rounded-md' />
<Button onClick={handleCopy}><Copy className='w-4 h-4'/></Button>
</div><div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<h3 className="text-lg font-semibold">{dict.dialogs.ShareSyncTimetableDialog['category:qr']}</h3>
<div className='p-2 bg-white rounded-md w-min'>
<QRCodeSVG value={link} />
</div>
</div>
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-semibold">{dict.dialogs.ShareSyncTimetableDialog['category:links']}</h3>
<Button variant="outline" asChild>
<Link
// Subject: Here is My Timetable, Body: My Timetable can be found on NTHUMODS at {shareLink}
href={`mailto:?subject=Here is My Timetable&body=My Timetable can be found on NTHUMODS at ${link}`}
target='_blank'
>
<Mail className="w-4 h-4 mr-2" />{dict.dialogs.ShareSyncTimetableDialog.links.email}
</Link>
</Button>
<Button variant="outline" asChild disabled>
<Link
href={webcalLink}
target='_blank'
>
<Calendar className="w-4 h-4 mr-2" /> Sync To Calendar
</Link>
</Button>
</div>
</div>
</div>:
<ComponentSkeleton />}
</DialogContent>
</ModalDialog>
</Dialog>
}

export default ShareSyncTimetableDialog;
Loading

0 comments on commit e4ba70e

Please sign in to comment.