Skip to content

Commit

Permalink
feat: Changelog component (#42)
Browse files Browse the repository at this point in the history
* package lock version update

* add shadcn dialog

* Feat: add changelog component

* increase dialog z-index

* fix: indent wrapped title text and list items

* fix: list overflow scroll and prevent fullscreen at small screen sizes

* add recent changelogs

* update usehooks-ts

* fix: changelog overflow, only show on newUser/changes available

* add spelling exception to workspace

* fix: replace deprecated useElementSize hook

* fix: changelog spelling

* add changelog nav button

* fix: more changelog text updates
  • Loading branch information
david-abell authored May 27, 2024
1 parent 86c6a84 commit d788938
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 17 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"luxon",
"moveend",
"ontime",
"Openchange",
"papaparse",
"popupclose",
"popupopen",
Expand Down
38 changes: 26 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "irish-buses",
"version": "0.11.2",
"version": "0.12.0",
"type": "module",
"scripts": {
"dev": "next dev --experimental-https",
Expand Down Expand Up @@ -61,7 +61,7 @@
"tailwindcss-animate": "^1.0.7",
"ts-retry": "^4.2.5",
"typescript": "^5.3.3",
"usehooks-ts": "^2.10.0",
"usehooks-ts": "^2.16.0",
"vaul": "^0.9.1",
"zod": "^3.21.4"
},
Expand Down
69 changes: 69 additions & 0 deletions src/components/changelog/Changelog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Dialog,
DialogContent,
DialogClose,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { changelogs } from "./changelogs";
import Version from "./Version";
import { useLocalStorage } from "usehooks-ts";
import packageJson from "package.json";
import { useEffect } from "react";
const packageAppVersion = packageJson.version;

const VERSION_KEY = "app-version";
const NEW_USER_KEY = "new-user";

type Props = {
show: boolean;
setShow: (s: boolean) => void;
};

function Changelog({ show, setShow }: Props) {
const [appVersion, setAppVersion] = useLocalStorage(VERSION_KEY, "default");
const [newUser, setNewUser] = useLocalStorage(NEW_USER_KEY, "default");

// Radix onOpenchange only triggers on dialog close
const handleOpenChange = () => {
setAppVersion(packageAppVersion);
setShow(false);
setNewUser("false");
};

useEffect(() => {
if (appVersion === "default") {
setAppVersion(packageAppVersion);
} else if (appVersion === packageAppVersion && newUser === "default") {
setNewUser("true");
setShow(true);
} else if (appVersion !== packageAppVersion) {
setShow(true);
}
}, [appVersion, newUser, setAppVersion, setNewUser, setShow]);

if (appVersion === "default" || show === false) return null;

return (
<Dialog open={show} onOpenChange={handleOpenChange}>
<DialogTrigger className="sr-only">Open</DialogTrigger>
<DialogContent className="max-w-[90dvw] md:max-w-lg rounded-lg">
<DialogHeader>
<DialogTitle className="font-bold">Changelog</DialogTitle>
</DialogHeader>
<div className="overflow-y-auto max-h-[70dvh]">
{changelogs.map((record) => (
<Version key={"version: " + record.version} {...record} />
))}
</div>
<DialogFooter>
<DialogClose>Dismiss</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

export default Changelog;
21 changes: 21 additions & 0 deletions src/components/changelog/Version.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Changelog } from "./changelogs";

function Version({ version, changes, title }: Changelog) {
return (
<section>
<header>
{!!title && <p className="italic py-2 pl-2 -indent-2">{title}</p>}
<h3 className="text-lg">Version {version}:</h3>
</header>
<ul className="list-outside list-disc pl-5">
{changes.map((change) => (
<li className="last:pb-2" key={version + change}>
{change}
</li>
))}
</ul>
</section>
);
}

export default Version;
48 changes: 48 additions & 0 deletions src/components/changelog/changelogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export type Changelog = {
version: string;
changes: string[];
title?: string;
};
export const changelogs: Changelog[] = [
{
version: "0.12.0",
title: "Feature: Changelog dialog.",
changes: [
"Add Shadcn dialog component.",
"Add changelog component and recent app updates.",
],
},
{
version: "0.11.2",
changes: [
"Fix dynamic map tiles url causing duplicate image caching.",
"Fix security policy causing cached map tiles to use way too much storage space.",
"Fix chrome service worker bug causing significant slowdown when ignoring url search params with many images cached.",
],
},
{
version: "0.11.1",
changes: [
"Fix z-index inconsistencies in footer, global alerts, saved stops, and drawer/sheet ui components.",
],
},
{
version: "0.11.0",
title:
"Feature: change footer from accordion to mobile friendly drawer component.",
changes: [
"Add shadcn drawer component.",
"Adjust global muted color style.",
],
},
{
version: "0.10.0",
title: "Feature: support installation as progressive web app (PWA).",
changes: [
"High resolution icons for android and iOS.",
"Cache map tiles for for reduced network bandwidth.",
"Skip animations when changing map location.",
"Adjust zoom level that displays vehicle markers.",
],
},
];
120 changes: 120 additions & 0 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";

import { cn } from "@/lib/utils";

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-[2000] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-[3000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
Loading

0 comments on commit d788938

Please sign in to comment.