Skip to content

Commit

Permalink
feat: Redesigned Apps Page with Categories (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJustChew authored Aug 22, 2024
2 parents 6c336d7 + 1a8f620 commit c45e0f7
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 130 deletions.
119 changes: 119 additions & 0 deletions src/app/[lang]/(mods-pages)/apps/AppItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { apps } from "@/const/apps";
import { ExternalLink } from "lucide-react";
import React from "react";
import { useCallback } from "react";
import useDictionary from "@/dictionaries/useDictionary";
import { useHeadlessAIS } from "@/hooks/contexts/useHeadlessAIS";
import { useSettings } from "@/hooks/contexts/settings";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { toast } from "@/components/ui/use-toast";

const AppItem = ({
app,
mini = false,
}: {
app: (typeof apps)[number];
mini?: boolean;
}) => {
const { language } = useSettings();
const { ais, getACIXSTORE } = useHeadlessAIS();
const router = useRouter();
const dict = useDictionary();
const [aisLoading, setAisLoading] = React.useState(false);

const onItemClicked = useCallback(async () => {
if (app.ais) {
if (!ais.enabled) {
toast({ title: dict.ccxp.not_logged_in_error });
return;
}

setAisLoading(true);
const token = await getACIXSTORE();
if (!token) {
setAisLoading(false);
return;
}

// if starts with http, open in new tab
if (app.href.startsWith("https://www.ccxp.nthu.edu.tw")) {
// Redirect user
const redirect_url = app.href + `?ACIXSTORE=${token}`;
console.log(redirect_url);
const link = document.createElement("a");
link.href = redirect_url;
link.target = "_blank";
link.click();
} else {
router.push(app.href);
}
setAisLoading(false);
} else {
router.push(app.href);
}
}, [router, app, ais, getACIXSTORE, dict]);

return (
<div
className={cn(
!mini
? "flex flex-row items-center space-x-2 flex-1"
: "flex flex-col items-center space-y-1",
(app.ais && ais.enabled) || !app.ais
? "cursor-pointer"
: "cursor-not-allowed opacity-30",
)}
onClick={onItemClicked}
>
<div className="p-2 rounded-lg bg-nthu-100 text-nthu-800 grid place-items-center">
<app.Icon size={24} />
</div>
<div className="flex flex-col">
<h2
className={cn(!mini ? "font-medium" : "text-xs max-w-20 text-center")}
>
{language == "zh" ? app.title_zh : app.title_en}
</h2>
{!mini && app.href.startsWith("https://www.ccxp.nthu.edu.tw") && (
<h3 className="text-xs text-muted-foreground">
<ExternalLink size={12} className="inline" /> {dict.applist.to_ccxp}
</h3>
)}
</div>
<Dialog open={aisLoading} modal={true}>
<DialogContent>
<div className="flex flex-col items-center space-y-4">
<div className="flex flex-col space-y-4 items-center">
{/* <div className='animate-spin rounded-full h-16 w-16 border-2 border-gray-900'></div> */}
<svg
className="animate-spin h-14 w-14 text-gray-900 dark:text-gray-100"
viewBox="0 0 24 24"
>
<circle
className="opacity-0"
cx="12"
cy="12"
r="12"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
</svg>
<p className="text-gray-700 dark:text-gray-500">
{dict.ccxp.logging_in_please_wait}
</p>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};

export default AppItem;
163 changes: 91 additions & 72 deletions src/app/[lang]/(mods-pages)/apps/page.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,104 @@
"use client";
import { apps } from "@/const/apps";
import { getDictionary } from "@/dictionaries/dictionaries";
import { LangProps } from "@/types/pages";
import Link from "next/link";
import { Info, ArrowRight } from "lucide-react";
import FavouriteApp from "./Favorite";
import React from "react";
import { apps, categories } from "@/const/apps";
import { Settings, Star } from "lucide-react";
import useDictionary from "@/dictionaries/useDictionary";
import { useHeadlessAIS } from "@/hooks/contexts/useHeadlessAIS";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useSettings } from "@/hooks/contexts/settings";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import AppItem from "./AppItem";

const AppList = ({ params: { lang } }: LangProps) => {
const AppList = () => {
const dict = useDictionary();
const { ais } = useHeadlessAIS();
const { language, pinnedApps, toggleApp } = useSettings();

return (
<div className="h-full w-full">
<div className="flex flex-col" suppressHydrationWarning>
<h1 className="text-xl font-bold px-4 py-2">{dict.applist.title}</h1>
{apps
.filter((m) => (m.ais ? !!ais.enabled : true))
.map((app) => (
<div
key={app.id}
className="flex flex-row items-center space-x-2 py-2 px-4 hover:bg-gray-100 dark:hover:bg-neutral-800"
>
<Link
href={app.href}
className="flex flex-row flex-1 items-center space-x-2"
target={app.target}
>
<div className="p-3 rounded-full bg-nthu-200 text-nthu-800 grid place-items-center">
<app.Icon size={16} />
<div className="h-full w-full px-2">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="flex flex-col p-4 rounded-md border border-border gap-4">
<div className="flex flex-row items-center">
<h1 className="font-bold text-muted-foreground flex-1">
{dict.applist.pinned_apps_title}
</h1>
<Dialog>
<DialogTrigger asChild>
<Settings size={20} className="cursor-pointer" />
</DialogTrigger>
<DialogContent>
<div className="flex flex-col gap-4">
<h1 className="font-bold text-muted-foreground">
{dict.applist.edit_pinned_apps_title}
</h1>
<ScrollArea className="max-h-[80dvh]">
<div className="flex flex-col gap-2">
{apps.map((app) => (
<div
key={app.id}
className="flex flex-row items-center space-x-2"
>
<div className="p-2 rounded-lg bg-nthu-100 text-nthu-800 grid place-items-center">
<app.Icon size={24} />
</div>
<div className="flex flex-col flex-1">
<h2 className=" font-medium">
{language == "zh" ? app.title_zh : app.title_en}
</h2>
</div>
<div className="flex flex-row items-center space-x-2 pr-4">
<Button
size="icon"
variant="ghost"
onClick={() => toggleApp(app.id)}
>
<Star
size={20}
className={cn(
!pinnedApps.includes(app.id)
? ""
: "fill-yellow-500 stroke-yellow-500",
)}
/>
</Button>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
<div className="flex flex-col gap-1 flex-1">
<h2 className="text-base font-medium text-muted-foreground">
{lang == "zh" ? app.title_zh : app.title_en}
</h2>
</div>
</Link>
<div className="items-center px-3">
<FavouriteApp appId={app.id} />
</div>
</div>
))}
{/* <CCXPDownAlert/> */}
<div className="px-4 py-2 space-y-2">
{!ais.enabled && (
<Alert color="success">
<Info className="mr-2" />
<div className="flex flex-col">
<h4 className="font-bold mb-1">還有更多功能!</h4>
<p>
到設定同步校務資訊系統后,可以直接在這裏使用校務資訊系統的功能!
</p>
</div>
<React.Fragment>
<Link href={`/${lang}/settings#headless_ais`}>
<Button variant="default">
前往開通
<ArrowRight className="w-4 h-4" />
</Button>
</Link>
</React.Fragment>
</Alert>
</DialogContent>
</Dialog>
</div>
<div className="grid md:grid-cols-2 gap-2">
{apps
.filter((app) => pinnedApps.includes(app.id))
.map((app) => (
<AppItem key={app.id} app={app} />
))}
</div>
{pinnedApps.length == 0 && (
<p className="text-muted-foreground text-center">
{dict.applist.empty_pinned_apps_reminder}
</p>
)}
<Alert>
<AlertTitle>沒有你要的功能?</AlertTitle>
<AlertDescription>
快到
<Link
href="https://github.com/nthumodifications/courseweb/issues/new"
className="underline text-indigo-600"
>
Github
</Link>
提出你的想法吧
</AlertDescription>
</Alert>
</div>
{Object.keys(categories).map((category) => (
<div
className="flex flex-col p-4 rounded-md border border-border gap-4"
key={category}
>
<h1 className="font-bold text-muted-foreground">
{categories[category][`title_${language}`]}
</h1>
<div className="grid md:grid-cols-2 gap-2">
{apps
.filter((m) => m.category === category)
.map((app) => (
<AppItem key={app.id} app={app} />
))}
</div>
</div>
))}
</div>
</div>
);
Expand Down
19 changes: 3 additions & 16 deletions src/components/Today/TodaySchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import useTime from "@/hooks/useTime";
import { NoClassPickedReminder } from "./NoClassPickedReminder";
import { TimetableItemDrawer } from "@/components/Timetable/TimetableItemDrawer";
import AppItem from "@/app/[lang]/(mods-pages)/apps/AppItem";

const getRangeOfDays = (start: Date, end: Date) => {
const days = [];
Expand Down Expand Up @@ -132,23 +133,9 @@ const TodaySchedule: FC<{
if (applist.length == 0) return <></>;
return (
<div className="flex flex-col gap-1">
<h1 className="text-xs font-bold text-gray-500">
{dict.applist.title}
</h1>
<div className="flex flex-row flex-wrap gap-2 pb-2">
<div className="flex flex-row flex-wrap gap-2 pb-2 justify-evenly">
{applist.map((app, index) => (
<Link href={app.href} key={index}>
<div className="flex flex-col items-center justify-center p-2 gap-2 w-16">
<div className="p-3 rounded-full bg-nthu-200 text-nthu-800 grid place-items-center">
<app.Icon size={20} />
</div>
<div className="flex flex-col gap-1 flex-1">
<h2 className="text-xs font-medium text-gray-600 text-center line-clamp-2 break-all">
{language == "zh" ? app.title_zh : app.title_en}
</h2>
</div>
</div>
</Link>
<AppItem key={index} app={app} mini />
))}
</div>
</div>
Expand Down
Loading

0 comments on commit c45e0f7

Please sign in to comment.