···2525} from "@/components/ui/form";
2626import { Input } from "@/components/ui/input";
2727import { InputWithAddons } from "@/components/ui/input-with-addons";
2828-import { useToast } from "@/components/ui/use-toast";
2928import { useDebounce } from "@/hooks/use-debounce";
2929+import { useToastAction } from "@/hooks/use-toast-action";
3030import { slugify } from "@/lib/utils";
3131import { api } from "@/trpc/client";
3232import { LoadingAnimation } from "../loading-animation";
···6565 const [isPending, startTransition] = useTransition();
6666 const watchSlug = form.watch("slug");
6767 const debouncedSlug = useDebounce(watchSlug, 1000); // using debounce to not exhaust the server
6868- const { toast } = useToast();
6868+ const { toast } = useToastAction();
6969 const checkUniqueSlug = useCallback(async () => {
7070 const isUnique = await api.page.getSlugUniqueness.query({
7171 slug: debouncedSlug,
···108108 if (defaultValues) {
109109 await api.page.updatePage.mutate(props);
110110 } else {
111111- await api.page.createPage.mutate({
111111+ const page = await api.page.createPage.mutate({
112112 ...props,
113113 workspaceSlug,
114114 });
115115+ router.replace(`./edit?id=${page?.id}`); // to stay on same page and enable 'Advanced' tab
115116 }
116116- router.push("./");
117117- router.refresh(); // this will actually revalidate the page after submission
117117+ toast("saved");
118118+ router.refresh();
118119 } catch {
119119- toast({
120120- title: "Something went wrong.",
121121- description: "If you are in the limits, please try again.",
122122- });
120120+ toast("error");
123121 }
124122 });
125123 };
···148146 const isUnique = await checkUniqueSlug();
149147 if (!isUnique) {
150148 // the user will already have the "error" message - we include a toast as well
151151- toast({
152152- title: "Slug is already taken.",
153153- description: "Please select another slug. Every slug is unique.",
154154- });
149149+ toast("unique-slug");
155150 } else {
156151 if (onSubmit) {
157152 void form.handleSubmit(onSubmit)(e);
···167162 <FormItem className="sm:col-span-4">
168163 <FormLabel>Title</FormLabel>
169164 <FormControl>
170170- <Input placeholder="" {...field} />
165165+ <Input placeholder="Documenso Status" {...field} />
171166 </FormControl>
172167 <FormDescription>The title of your page.</FormDescription>
173168 <FormMessage />
···181176 <FormItem className="sm:col-span-5">
182177 <FormLabel>Description</FormLabel>
183178 <FormControl>
184184- <Input placeholder="" {...field} />
179179+ <Input
180180+ placeholder="Stay informed about our api and website health."
181181+ {...field}
182182+ />
185183 </FormControl>
186184 <FormDescription>
187187- Give your user some information about it.
185185+ Provide your users informations about it.
188186 </FormDescription>
189187 <FormMessage />
190188 </FormItem>
···197195 <FormItem className="sm:col-span-3">
198196 <FormLabel>Slug</FormLabel>
199197 <FormControl>
200200- <InputWithAddons {...field} trailing={".openstatus.dev"} />
198198+ <InputWithAddons
199199+ placeholder="documenso"
200200+ trailing={".openstatus.dev"}
201201+ {...field}
202202+ />
201203 </FormControl>
202204 <FormDescription>
203205 The subdomain for your status page. At least 3 chars.
+8
apps/web/src/components/layout/app-header.tsx
···99import { Button } from "../ui/button";
1010import { Skeleton } from "../ui/skeleton";
11111212+/**
1313+ * TODO: work on a better breadcrumb navigation like Vercel
1414+ * [workspace/project/deploymenents/deployment]
1515+ * This will allow us to 'only' save, and not redirect the user to the other pages
1616+ * and therefore, can after saving the monitor/page go to the next tab!
1717+ * Probably, we will need to use useSegements() from vercel, but once done properly, it could be really nice to share
1818+ */
1919+1220export function AppHeader() {
1321 const { isLoaded, isSignedIn } = useUser();
1422
+18
apps/web/src/components/layout/skeleton-tabs.tsx
···11+import { Skeleton } from "@/components/ui/skeleton";
22+33+interface SkeletonTabsProps {
44+ children?: React.ReactNode;
55+}
66+77+export function SkeletonTabs({ children }: SkeletonTabsProps) {
88+ return (
99+ <div className="w-full">
1010+ <div className="flex items-center border-b">
1111+ <Skeleton className="h-9 w-16 px-3 py-1.5" />
1212+ <Skeleton className="h-9 w-16 px-3 py-1.5" />
1313+ </div>
1414+ {/* tbd: if children is empty, we could render a skeleton container */}
1515+ <div className="mt-3">{children}</div>
1616+ </div>
1717+ );
1818+}
···11+import { Button } from "@/components/ui/button";
22+import { useToast } from "@/components/ui/use-toast";
33+import type { Toast } from "@/components/ui/use-toast";
44+55+const config = {
66+ error: {
77+ title: "Something went wrong.",
88+ description: "Please try again.",
99+ variant: "destructive",
1010+ action: (
1111+ <Button variant="outline" asChild className="text-foreground">
1212+ <a href="/discord" target="_blank" rel="noreferrer">
1313+ Discord
1414+ </a>
1515+ </Button>
1616+ ),
1717+ },
1818+ "unique-slug": {
1919+ title: "Slug is already taken.",
2020+ description: "Please select another slug. Every slug is unique.",
2121+ },
2222+ success: { title: "Success" },
2323+ deleted: { title: "Deleted successfully." }, // TODO: we are not informing the user besides the visual changes when an entry has been deleted
2424+ saved: { title: "Saved successfully." },
2525+} as const satisfies Record<string, Toast>;
2626+2727+type ToastAction = keyof typeof config;
2828+2929+export function useToastAction() {
3030+ const { toast: defaultToast } = useToast();
3131+3232+ function toast(action: ToastAction) {
3333+ return defaultToast(config[action]);
3434+ }
3535+3636+ return { toast };
3737+}
+43
apps/web/src/hooks/use-window-size.ts
···11+// CREDITS: https://github.com/steven-tey/precedent/blob/main/lib/hooks/use-window-size.ts
22+import { useEffect, useState } from "react";
33+44+export default function useWindowSize() {
55+ const [windowSize, setWindowSize] = useState<{
66+ width: number | undefined;
77+ height: number | undefined;
88+ }>({
99+ width: undefined,
1010+ height: undefined,
1111+ });
1212+1313+ useEffect(() => {
1414+ // Handler to call on window resize
1515+ function handleResize() {
1616+ // Set window width/height to state
1717+ setWindowSize({
1818+ width: window.innerWidth,
1919+ height: window.innerHeight,
2020+ });
2121+ }
2222+2323+ // Add event listener
2424+ window.addEventListener("resize", handleResize);
2525+2626+ // Call handler right away so state gets updated with initial window size
2727+ handleResize();
2828+2929+ // Remove event listener on cleanup
3030+ return () => window.removeEventListener("resize", handleResize);
3131+ }, []); // Empty array ensures that effect is only run on mount
3232+3333+ return {
3434+ windowSize,
3535+ isMobile: typeof windowSize?.width === "number" && windowSize?.width < 640,
3636+ isTablet:
3737+ typeof windowSize?.width === "number" &&
3838+ windowSize?.width >= 640 &&
3939+ windowSize?.width < 1024,
4040+ isDesktop:
4141+ typeof windowSize?.width === "number" && windowSize?.width >= 1024,
4242+ };
4343+}
+2
packages/api/src/router/monitor.ts
···7070 .returning()
7171 .get();
72727373+ // TODO: check, do we have to await for the two calls? Will slow down user response for our analytics
7374 await analytics.identify(result.user.id, {
7475 userId: result.user.id,
7576 });
···7879 url: newMonitor.url,
7980 periodicity: newMonitor.periodicity,
8081 });
8282+ return newMonitor;
8183 }),
82848385 getMonitorByID: protectedProcedure
+2
packages/api/src/router/page.ts
···6868 await opts.ctx.db.insert(monitorsToPages).values(values).run();
6969 }
70707171+ // TODO: check, do we have to await for the two calls? Will slow down user response for our analytics
7172 await analytics.identify(data.user.id, {
7273 userId: data.user.id,
7374 });
···7576 event: "Page Created",
7677 slug: newPage.slug,
7778 });
7979+ return newPage;
7880 }),
79818082 getPageByID: protectedProcedure