From 1c6ed9bc6dcd0d3ee8eada003d3ab320fd1816b7 Mon Sep 17 00:00:00 2001
From: sabaimran <65192171+sabaimran@users.noreply.github.com>
Date: Mon, 15 Jul 2024 09:12:33 -0700
Subject: [PATCH] Migrate the existing automations page to use React (#849)
Migrates the Automations page to React, mostly keeping the overall design consistent with organization. Use component library, with some changes in color. Add easier management with straightforward form and editing experience.
Use system preference for determining dark mode if not explicitly set.
---
.../app/automations/automations.module.css | 11 +
.../automations/automationsLayout.module.css | 11 +
src/interface/web/app/automations/layout.tsx | 28 +
src/interface/web/app/automations/page.tsx | 983 ++++++++++++++++++
src/interface/web/app/common/utils.ts | 24 +
.../web/app/components/navMenu/navMenu.tsx | 25 +-
.../app/components/shareLink/shareLink.tsx | 50 +-
src/interface/web/components/ui/alert.tsx | 59 ++
src/interface/web/components/ui/form.tsx | 178 ++++
src/interface/web/components/ui/select.tsx | 160 +++
src/interface/web/components/ui/toast.tsx | 129 +++
src/interface/web/components/ui/toaster.tsx | 35 +
src/interface/web/components/ui/use-toast.ts | 194 ++++
src/interface/web/package.json | 8 +-
src/interface/web/yarn.lock | 62 +-
15 files changed, 1933 insertions(+), 24 deletions(-)
create mode 100644 src/interface/web/app/automations/automations.module.css
create mode 100644 src/interface/web/app/automations/automationsLayout.module.css
create mode 100644 src/interface/web/app/automations/layout.tsx
create mode 100644 src/interface/web/app/automations/page.tsx
create mode 100644 src/interface/web/components/ui/alert.tsx
create mode 100644 src/interface/web/components/ui/form.tsx
create mode 100644 src/interface/web/components/ui/select.tsx
create mode 100644 src/interface/web/components/ui/toast.tsx
create mode 100644 src/interface/web/components/ui/toaster.tsx
create mode 100644 src/interface/web/components/ui/use-toast.ts
diff --git a/src/interface/web/app/automations/automations.module.css b/src/interface/web/app/automations/automations.module.css
new file mode 100644
index 00000000..206d4c36
--- /dev/null
+++ b/src/interface/web/app/automations/automations.module.css
@@ -0,0 +1,11 @@
+div.automationsLayout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media screen and (max-width: 768px) {
+ div.automationsLayout {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/interface/web/app/automations/automationsLayout.module.css b/src/interface/web/app/automations/automationsLayout.module.css
new file mode 100644
index 00000000..6e27ee7a
--- /dev/null
+++ b/src/interface/web/app/automations/automationsLayout.module.css
@@ -0,0 +1,11 @@
+.automationsLayout {
+ max-width: 70vw;
+ margin: auto;
+ margin-bottom: 2rem;
+}
+
+@media screen and (max-width: 700px) {
+ .automationsLayout {
+ max-width: 90vw;
+ }
+}
diff --git a/src/interface/web/app/automations/layout.tsx b/src/interface/web/app/automations/layout.tsx
new file mode 100644
index 00000000..23493c75
--- /dev/null
+++ b/src/interface/web/app/automations/layout.tsx
@@ -0,0 +1,28 @@
+
+import type { Metadata } from "next";
+import NavMenu from '../components/navMenu/navMenu';
+import styles from './automationsLayout.module.css';
+import { Toaster } from "@/components/ui/toaster";
+
+
+export const metadata: Metadata = {
+ title: "Khoj AI - Automations",
+ description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.",
+ icons: {
+ icon: '/static/favicon.ico',
+ },
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/interface/web/app/automations/page.tsx b/src/interface/web/app/automations/page.tsx
new file mode 100644
index 00000000..8c20918e
--- /dev/null
+++ b/src/interface/web/app/automations/page.tsx
@@ -0,0 +1,983 @@
+'use client'
+
+import useSWR from 'swr';
+import Loading, { InlineLoading } from '../components/loading/loading';
+import {
+ Card,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle
+
+} from '@/components/ui/card';
+import { Button, buttonVariants } from '@/components/ui/button';
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface AutomationsData {
+ id: number;
+ subject: string;
+ query_to_run: string;
+ scheduling_request: string;
+ schedule: string;
+ crontime: string;
+ next: string;
+}
+
+import cronstrue from 'cronstrue';
+import { zodResolver } from "@hookform/resolvers/zod"
+import { UseFormReturn, useForm } from "react-hook-form"
+import { z } from "zod"
+import { Suspense, useEffect, useState } from 'react';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
+import { DialogTitle } from '@radix-ui/react-dialog';
+import { Textarea } from '@/components/ui/textarea';
+import { LocationData, useIPLocationData } from '../common/utils';
+
+import styles from './automations.module.css';
+import ShareLink from '../components/shareLink/shareLink';
+import { useSearchParams } from 'next/navigation';
+import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
+import { Clock, DotsThreeVertical, Envelope, Info, MapPinSimple, Pencil, Play, Plus, Trash } from '@phosphor-icons/react';
+import { useAuthenticatedData } from '../common/auth';
+import LoginPrompt from '../components/loginPrompt/loginPrompt';
+import { useToast } from '@/components/ui/use-toast';
+import { ToastAction } from '@/components/ui/toast';
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
+
+const automationsFetcher = () => window.fetch('/api/automations').then(res => res.json()).catch(err => console.log(err));
+
+// Standard cron format: minute hour dayOfMonth month dayOfWeek
+
+function getEveryBlahFromCron(cron: string) {
+ const cronParts = cron.split(' ');
+ const dayOfMonth = cronParts[2];
+ const dayOfWeek = cronParts[4];
+
+ // If both dayOfMonth and dayOfWeek are '*', it runs every day
+ if (dayOfMonth === '*' && dayOfWeek === '*') {
+ return 'Day';
+ }
+ // If dayOfWeek is not '*', it suggests a specific day of the week, implying a weekly schedule
+ else if (dayOfWeek !== '*') {
+ return 'Week';
+ }
+ // If dayOfMonth is not '*', it suggests a specific day of the month, implying a monthly schedule
+ else if (dayOfMonth !== '*') {
+ return 'Month';
+ }
+ // Default to 'Day' if none of the above conditions are met
+ else {
+ return 'Day';
+ }
+}
+
+function getDayOfWeekFromCron(cron: string) {
+ const cronParts = cron.split(' ');
+ if (cronParts[3] === '*' && cronParts[4] !== '*') {
+ return Number(cronParts[4]);
+ }
+
+ return undefined;
+}
+
+function getTimeRecurrenceFromCron(cron: string) {
+ const cronParts = cron.split(' ');
+ const hour = cronParts[1];
+ const minute = cronParts[0];
+ const period = Number(hour) >= 12 ? 'PM' : 'AM';
+
+ let friendlyHour = Number(hour) > 12 ? Number(hour) - 12 : hour;
+ if (friendlyHour === '00') {
+ friendlyHour = '12';
+ }
+
+ let friendlyMinute = minute;
+ if (Number(friendlyMinute) < 10 && friendlyMinute !== '00') {
+ friendlyMinute = `0${friendlyMinute}`;
+ }
+ return `${friendlyHour}:${friendlyMinute} ${period}`;
+}
+
+function getDayOfMonthFromCron(cron: string) {
+ const cronParts = cron.split(' ');
+
+ return String(cronParts[2]);
+}
+
+function cronToHumanReadableString(cron: string) {
+ return cronstrue.toString(cron);
+}
+
+const frequencies = ['Day', 'Week', 'Month'];
+
+const daysOfMonth = Array.from({ length: 31 }, (_, i) => String(i + 1));
+
+const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+const timeOptions: string[] = [];
+
+const timePeriods = ['AM', 'PM'];
+
+// Populate the time selector with options for each hour of the day
+for (var i = 0; i < timePeriods.length; i++) {
+ for (var hour = 0; hour < 12; hour++) {
+ for (var minute = 0; minute < 60; minute += 15) {
+ // Ensure all minutes are two digits
+ const paddedMinute = String(minute).padStart(2, '0');
+ const friendlyHour = hour === 0 ? 12 : hour;
+ timeOptions.push(`${friendlyHour}:${paddedMinute} ${timePeriods[i]}`);
+ }
+ }
+}
+
+const timestamp = Date.now();
+
+const suggestedAutomationsMetadata: AutomationsData[] = [
+ {
+ "subject": "Weekly Newsletter",
+ "query_to_run": "Compile a message including: 1. A recap of news from last week 2. An at-home workout I can do before work 3. A quote to inspire me for the week ahead",
+ "schedule": "9AM every Monday",
+ "next": "Next run at 9AM on Monday",
+ "crontime": "0 9 * * 1",
+ "id": timestamp,
+ "scheduling_request": "",
+ },
+ {
+ "subject": "Daily Bedtime Story",
+ "query_to_run": "Compose a bedtime story that a five-year-old might enjoy. It should not exceed five paragraphs. Appeal to the imagination, but weave in learnings.",
+ "schedule": "9PM every night",
+ "next": "Next run at 9PM today",
+ "crontime": "0 21 * * *",
+ "id": timestamp + 1,
+ "scheduling_request": "",
+ },
+ {
+ "subject": "Front Page of Hacker News",
+ "query_to_run": "Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links",
+ "schedule": "9PM on every Wednesday",
+ "next": "Next run at 9PM on Wednesday",
+ "crontime": "0 21 * * 3",
+ "id": timestamp + 2,
+ "scheduling_request": "",
+ },
+ {
+ "subject": "Market Summary",
+ "query_to_run": "Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.",
+ "schedule": "9AM on every weekday",
+ "next": "Next run at 9AM on Monday",
+ "crontime": "0 9 * * *",
+ "id": timestamp + 3,
+ "scheduling_request": "",
+ }
+];
+
+function createShareLink(automation: AutomationsData) {
+ const encodedSubject = encodeURIComponent(automation.subject);
+ const encodedQuery = encodeURIComponent(automation.query_to_run);
+ const encodedCrontime = encodeURIComponent(automation.crontime);
+
+ const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`;
+
+ return shareLink;
+}
+
+function deleteAutomation(automationId: string, setIsDeleted: (isDeleted: boolean) => void) {
+ fetch(`/api/automation?automation_id=${automationId}`, { method: 'DELETE' }
+ ).then(response => response.json())
+ .then(data => {
+ setIsDeleted(true);
+ });
+}
+
+function sendAPreview(automationId: string, setToastMessage: (toastMessage: string) => void) {
+ fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: 'POST' })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response;
+ })
+ .then(automations => {
+ setToastMessage("Automation triggered. Check your inbox in a few minutes!");
+ })
+ .catch(error => {
+ setToastMessage("Sorry, something went wrong. Try again later.");
+ })
+}
+
+interface AutomationsCardProps {
+ automation: AutomationsData;
+ locationData?: LocationData | null;
+ suggestedCard?: boolean;
+ setNewAutomationData?: (data: AutomationsData) => void;
+ isLoggedIn: boolean;
+ setShowLoginPrompt: (showLoginPrompt: boolean) => void;
+}
+
+
+function AutomationsCard(props: AutomationsCardProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [updatedAutomationData, setUpdatedAutomationData] = useState(null);
+ const [isDeleted, setIsDeleted] = useState(false);
+ const [toastMessage, setToastMessage] = useState('');
+ const { toast } = useToast();
+
+ const automation = props.automation;
+
+ useEffect(() => {
+ const toastTitle = `Automation: ${updatedAutomationData?.subject || automation.subject}`;
+ if (toastMessage) {
+ toast({
+ title: toastTitle,
+ description: toastMessage,
+ action: (
+ Ok
+ ),
+ })
+ setToastMessage('');
+ }
+ }, [toastMessage]);
+
+ if (isDeleted) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {updatedAutomationData?.subject || automation.subject}
+
+
+
+
+
+
+ {
+ !props.suggestedCard && (
+
+ )
+ }
+ {
+ !props.suggestedCard && (
+
+ )
+ }
+
+
+
+
+
+ {updatedAutomationData?.schedule || cronToHumanReadableString(automation.crontime)}
+
+
+
+ {updatedAutomationData?.query_to_run || automation.query_to_run}
+
+
+ {
+ props.suggestedCard && props.setNewAutomationData && (
+
+ )
+ }
+ {
+ navigator.clipboard.writeText(createShareLink(automation));
+ }} />
+
+
+
+ )
+}
+
+interface SharedAutomationCardProps {
+ locationData?: LocationData | null;
+ setNewAutomationData: (data: AutomationsData) => void;
+ isLoggedIn: boolean;
+ setShowLoginPrompt: (showLoginPrompt: boolean) => void;
+}
+
+function SharedAutomationCard(props: SharedAutomationCardProps) {
+ const searchParams = useSearchParams();
+ const [isCreating, setIsCreating] = useState(true);
+
+ const subject = searchParams.get('subject');
+ const query = searchParams.get('query');
+ const crontime = searchParams.get('crontime');
+
+ if (!subject || !query || !crontime) {
+ return null;
+ }
+
+ const automation: AutomationsData = {
+ id: 0,
+ subject: decodeURIComponent(subject),
+ query_to_run: decodeURIComponent(query),
+ scheduling_request: '',
+ schedule: cronToHumanReadableString(decodeURIComponent(crontime)),
+ crontime: decodeURIComponent(crontime),
+ next: '',
+ }
+
+ return (
+
+ )
+}
+
+const EditAutomationSchema = z.object({
+ subject: z.optional(z.string()),
+ everyBlah: z.string({ required_error: "Every is required" }),
+ dayOfWeek: z.optional(z.number()),
+ dayOfMonth: z.optional(z.string()),
+ timeRecurrence: z.string({ required_error: "Time Recurrence is required" }),
+ queryToRun: z.string({ required_error: "Query to Run is required" }),
+});
+
+interface EditCardProps {
+ automation?: AutomationsData;
+ setIsEditing: (completed: boolean) => void;
+ setUpdatedAutomationData: (data: AutomationsData) => void;
+ locationData?: LocationData | null;
+ createNew?: boolean;
+ isLoggedIn: boolean;
+ setShowLoginPrompt: (showLoginPrompt: boolean) => void;
+}
+
+function EditCard(props: EditCardProps) {
+ const automation = props.automation;
+
+ const form = useForm>({
+ resolver: zodResolver(EditAutomationSchema),
+ defaultValues: {
+ subject: automation?.subject,
+ everyBlah: (automation?.crontime ? getEveryBlahFromCron(automation.crontime) : 'Day'),
+ dayOfWeek: (automation?.crontime ? getDayOfWeekFromCron(automation.crontime) : undefined),
+ timeRecurrence: (automation?.crontime ? getTimeRecurrenceFromCron(automation.crontime) : '12:00 PM'),
+ dayOfMonth: (automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1"),
+ queryToRun: automation?.query_to_run,
+ },
+ })
+
+ const onSubmit = (values: z.infer) => {
+ const cronFrequency = convertFrequencyToCron(values.everyBlah, values.timeRecurrence, values.dayOfWeek, values.dayOfMonth);
+
+ let updateQueryUrl = `/api/automation?`;
+
+ updateQueryUrl += `q=${values.queryToRun}`;
+
+ if (automation?.id && !props.createNew) {
+ updateQueryUrl += `&automation_id=${automation.id}`;
+ }
+
+ if (values.subject) {
+ updateQueryUrl += `&subject=${values.subject}`;
+ }
+
+ updateQueryUrl += `&crontime=${cronFrequency}`;
+
+ if (props.locationData) {
+ updateQueryUrl += `&city=${props.locationData.city}`;
+ updateQueryUrl += `®ion=${props.locationData.region}`;
+ updateQueryUrl += `&country=${props.locationData.country}`;
+ updateQueryUrl += `&timezone=${props.locationData.timezone}`;
+ }
+
+ let method = props.createNew ? 'POST' : 'PUT';
+
+ fetch(updateQueryUrl, { method: method })
+ .then(response => response.json())
+ .then
+ ((data: AutomationsData) => {
+ props.setIsEditing(false);
+ props.setUpdatedAutomationData({
+ id: data.id,
+ subject: data.subject || '',
+ query_to_run: data.query_to_run,
+ scheduling_request: data.scheduling_request,
+ schedule: cronToHumanReadableString(data.crontime),
+ crontime: data.crontime,
+ next: data.next,
+ });
+ });
+ }
+
+ function convertFrequencyToCron(frequency: string, timeRecurrence: string, dayOfWeek?: number, dayOfMonth?: string) {
+ let cronString = '';
+
+ const minutes = timeRecurrence.split(':')[1].split(' ')[0];
+ const period = timeRecurrence.split(':')[1].split(' ')[1];
+ const rawHourAsNumber = Number(timeRecurrence.split(':')[0]);
+ const hours = period === 'PM' && (rawHourAsNumber < 12) ? String(rawHourAsNumber + 12) : rawHourAsNumber;
+
+ const dayOfWeekNumber = dayOfWeek ? dayOfWeek : '*';
+
+ switch (frequency) {
+ case 'Day':
+ cronString = `${minutes} ${hours} * * *`;
+ break;
+ case 'Week':
+ cronString = `${minutes} ${hours} * * ${dayOfWeekNumber}`;
+ break;
+ case 'Month':
+ cronString = `${minutes} ${hours} ${dayOfMonth} * *`;
+ break;
+ }
+
+ return cronString;
+ }
+
+ return (
+
+ )
+
+}
+
+interface AutomationModificationFormProps {
+ form: UseFormReturn>;
+ onSubmit: (values: z.infer) => void;
+ create?: boolean;
+ isLoggedIn: boolean;
+ setShowLoginPrompt: (showLoginPrompt: boolean) => void;
+}
+
+function AutomationModificationForm(props: AutomationModificationFormProps) {
+
+ const [isSaving, setIsSaving] = useState(false);
+ const { errors } = props.form.formState;
+
+ console.log(errors);
+
+ function recommendationPill(recommendationText: string, onChange: (value: any, event: React.MouseEvent) => void) {
+ return (
+
+ )
+ }
+
+ const recommendationPills = [
+ "Make a picture of",
+ "Generate a summary of",
+ "Create a newsletter of",
+ "Notify me when"
+ ];
+
+ return (
+
+
+ )
+}
+
+
+export default function Automations() {
+ const authenticatedData = useAuthenticatedData();
+ const { data: personalAutomations, error, isLoading } = useSWR(authenticatedData ? 'automations' : null, automationsFetcher, { revalidateOnFocus: false });
+
+ const [isCreating, setIsCreating] = useState(false);
+ const [newAutomationData, setNewAutomationData] = useState(null);
+ const [allNewAutomations, setAllNewAutomations] = useState([]);
+ const [suggestedAutomations, setSuggestedAutomations] = useState([]);
+ const [showLoginPrompt, setShowLoginPrompt] = useState(false);
+
+ const ipLocationData = useIPLocationData();
+
+ useEffect(() => {
+ if (newAutomationData) {
+ setAllNewAutomations([...allNewAutomations, newAutomationData]);
+ setNewAutomationData(null);
+ }
+ }, [newAutomationData]);
+
+ useEffect(() => {
+
+ const allAutomations = personalAutomations ? personalAutomations.concat(allNewAutomations) : allNewAutomations;
+
+ if (allAutomations) {
+ setSuggestedAutomations(suggestedAutomationsMetadata.filter((suggestedAutomation) => {
+ return allAutomations.find(
+ (automation) => suggestedAutomation.subject === automation.subject) === undefined;
+ }));
+ }
+ }, [personalAutomations, allNewAutomations]);
+
+ if (error) return Failed to load
;
+
+ if (isLoading) return ;
+
+ return (
+
+
+ Automations
+
+ {
+ showLoginPrompt && (
+
+ )
+ }
+
+
+ How this works!
+
+ Automations help you structure your time by automating tasks you do regularly. Build your own, or try out our presets. Get results straight to your inbox.
+
+ {
+ authenticatedData ? (
+ {authenticatedData.email}
+ )
+ : (
+ Sign in to create your own automations.
+ )
+ }
+ {
+ ipLocationData && (
+ {ipLocationData ? `${ipLocationData.city}, ${ipLocationData.country}` : 'Unknown'}
+ )
+ }
+ {
+ ipLocationData && (
+ {ipLocationData ? `${ipLocationData.timezone}` : 'Unknown'}
+
+ )
+ }
+
+
+
+ Your Creations
+
+
+
+
+ {
+ authenticatedData ? (
+
+ )
+ : (
+
+ )
+ }
+ {
+ ((!personalAutomations || personalAutomations.length === 0) && (allNewAutomations.length == 0)) && (
+
+ So empty! Create your own automation to get started.
+
+ {
+ authenticatedData ? (
+
+ )
+ : (
+
+ )
+ }
+
+
+ )
+ }
+
+ {
+ personalAutomations && personalAutomations.map((automation) => (
+
+ ))}
+ {
+ allNewAutomations.map((automation) => (
+
+ ))
+ }
+
+
+ Try these out
+
+
+ {
+ suggestedAutomations.map((automation) => (
+
+ ))
+ }
+
+
+ );
+}
diff --git a/src/interface/web/app/common/utils.ts b/src/interface/web/app/common/utils.ts
index 5cc9d767..6dde9d4f 100644
--- a/src/interface/web/app/common/utils.ts
+++ b/src/interface/web/app/common/utils.ts
@@ -1,3 +1,18 @@
+import useSWR from "swr";
+
+export interface LocationData {
+ ip: string;
+ city: string;
+ region: string;
+ country: string;
+ postal: string;
+ latitude: number;
+ longitude: number;
+ timezone: string;
+}
+
+const locationFetcher = () => window.fetch("https://ipapi.co/json").then((res) => res.json()).catch((err) => console.log(err));
+
export function welcomeConsole() {
console.log(`%c %s`, "font-family:monospace", `
__ __ __ __ ______ __ _____ __
@@ -15,3 +30,12 @@ export function welcomeConsole() {
Read my operating manual at https://docs.khoj.dev
`);
}
+
+export function useIPLocationData() {
+ const {data: locationData, error: locationDataError } = useSWR("/api/ip", locationFetcher, { revalidateOnFocus: false });
+
+ if (locationDataError) return null;
+ if (!locationData) return null;
+
+ return locationData;
+}
diff --git a/src/interface/web/app/components/navMenu/navMenu.tsx b/src/interface/web/app/components/navMenu/navMenu.tsx
index 1c7d8b65..2409db3e 100644
--- a/src/interface/web/app/components/navMenu/navMenu.tsx
+++ b/src/interface/web/app/components/navMenu/navMenu.tsx
@@ -24,6 +24,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Toggle } from '@/components/ui/toggle';
import { Moon } from '@phosphor-icons/react';
+import Image from 'next/image';
interface NavMenuProps {
@@ -35,18 +36,25 @@ interface NavMenuProps {
export default function NavMenu(props: NavMenuProps) {
const userData = useAuthenticatedData();
- const [displayTitle, setDisplayTitle] = useState(props.title || props.selected.toUpperCase());
+ const [displayTitle, setDisplayTitle] = useState(props.title);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
- setDisplayTitle(props.title || props.selected.toUpperCase());
+ if (props.title) {
+ setDisplayTitle(props.title);
+ }
}, [props.title]);
useEffect(() => {
+
+ const mq = window.matchMedia(
+ "(prefers-color-scheme: dark)"
+ );
+
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
});
@@ -54,6 +62,9 @@ export default function NavMenu(props: NavMenuProps) {
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark');
setDarkMode(true);
+ } else if (mq.matches) {
+ document.documentElement.classList.add('dark');
+ setDarkMode(true);
}
}, []);
@@ -74,6 +85,16 @@ export default function NavMenu(props: NavMenuProps) {
{displayTitle &&
{displayTitle}
}
+ {
+ !displayTitle && props.showLogo &&
+
+
+
+ }
{
isMobileWidth ?
diff --git a/src/interface/web/app/components/shareLink/shareLink.tsx b/src/interface/web/app/components/shareLink/shareLink.tsx
index bb006479..ff92fada 100644
--- a/src/interface/web/app/components/shareLink/shareLink.tsx
+++ b/src/interface/web/app/components/shareLink/shareLink.tsx
@@ -7,9 +7,10 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
+import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Share } from "@phosphor-icons/react";
interface ShareLinkProps {
buttonTitle: string;
@@ -17,6 +18,8 @@ interface ShareLinkProps {
description: string;
url: string;
onShare: () => void;
+ buttonVariant?: keyof typeof buttonVariants;
+ includeIcon?: boolean;
}
function copyToClipboard(text: string) {
@@ -31,32 +34,39 @@ export default function ShareLink(props: ShareLinkProps) {
return (