This commit is contained in:
Ahmet Kilinc
2025-08-06 17:11:37 +01:00
parent 975d88a588
commit 39990893e0
4 changed files with 65 additions and 96 deletions

View File

@@ -906,26 +906,6 @@ function EmailComposerBase({
<div className="inline-flex w-full shrink-0 items-end justify-between self-stretch rounded-b-2xl bg-[#FFFFFF] px-3.5 py-2.5 outline-white/5 dark:bg-[#313131]">
<div className="flex flex-col items-start justify-start gap-2">
<div className="flex items-center justify-start gap-2">
<Button
size={'xs'}
onClick={handleSend}
disabled={isLoading || settingsLoading || !isScheduleValid}
>
<div className="flex items-center justify-center">
<div className="text-center text-sm leading-none text-white dark:text-black">
<span>Send </span>
</div>
</div>
<div className="flex h-5 items-center justify-center gap-1 rounded-sm bg-white/10 px-1 dark:bg-black/10">
<Command className="h-3.5 w-3.5 text-white dark:text-black" />
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-black" />
</div>
</Button>
<ScheduleSendPicker
value={scheduleAt}
onChange={handleScheduleChange}
onValidityChange={handleScheduleValidityChange}
/>
<div className="relative">
<AnimatePresence>
{aiGeneratedMessage !== null ? (
@@ -949,32 +929,6 @@ function EmailComposerBase({
/>
) : null}
</AnimatePresence>
<Button
size={'xs'}
variant={'ghost'}
className="border border-[#8B5CF6]"
onClick={async () => {
if (!subjectInput.trim()) {
await handleGenerateSubject();
}
setAiGeneratedMessage(null);
await handleAiGenerate();
}}
disabled={isLoading || aiIsLoading || messageLength < 1}
>
<div className="flex items-center justify-center gap-2.5 pl-0.5">
<div className="flex h-5 items-center justify-center gap-1 rounded-sm">
{aiIsLoading ? (
<Loader className="h-3.5 w-3.5 animate-spin fill-black dark:fill-white" />
) : (
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
)}
</div>
<div className="hidden text-center text-sm leading-none text-black md:block dark:text-white">
Generate
</div>
</div>
</Button>
</div>
<Button
variant={'ghost'}
@@ -1128,14 +1082,22 @@ function EmailComposerBase({
</div>
</div>
<div className="flex items-start justify-start gap-2">
<Button size={'xs'} onClick={handleSend} disabled={isLoading || settingsLoading}>
<ScheduleSendPicker
value={scheduleAt}
onChange={handleScheduleChange}
onValidityChange={handleScheduleValidityChange}
/>
<Button
size={'xs'}
onClick={handleSend}
disabled={isLoading || settingsLoading || !isScheduleValid}
>
<div className="flex items-center justify-center">
<div className="text-center text-sm leading-none text-white dark:text-black">
<span>Send </span>
</div>
</div>
</Button>
<Button variant={'secondary'} size={'xs'}>
<div className="flex h-5 items-center justify-center gap-1 rounded-sm bg-white/10 px-1 dark:bg-black/10">
<Command className="h-3.5 w-3.5 text-white dark:text-black" />
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-black" />

View File

@@ -1,7 +1,8 @@
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Clock } from 'lucide-react';
import { format, isValid } from 'date-fns';
import { useState, useEffect } from 'react';
import { format, isValid } from 'date-fns';
import { Button } from '../ui/button';
import { Clock } from 'lucide-react';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
@@ -79,14 +80,15 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button
<Button
type="button"
size={'xs'}
className={cn(
'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent',
'hover:bg-accent flex items-center gap-1 rounded-md bg-white/10 px-2 py-1 text-sm text-black dark:bg-black/10 dark:text-white',
className,
)}
>
<Clock className="h-4 w-4" />
{/* <Clock className="h-4 w-4" /> */}
<span>
{(() => {
if (!localValue) return 'Send later';
@@ -100,7 +102,7 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
}
})()}
</span>
</button>
</Button>
</PopoverTrigger>
<PopoverContent className="z-[100] w-64 p-4" align="start" side="top" sideOffset={8}>
<div className="flex flex-col gap-4">
@@ -109,10 +111,10 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
type="datetime-local"
value={displayValue}
onChange={handleChange}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:opacity-0"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:opacity-0"
/>
</div>
</PopoverContent>
</Popover>
);
};
};

View File

@@ -1,7 +1,3 @@
import { useTemplates } from '@/hooks/use-templates';
import { useTRPC } from '@/providers/query-provider';
import { Editor } from '@tiptap/react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,10 +7,6 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { toast } from 'sonner';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FileText, Save, Trash2 } from 'lucide-react';
import React, { useState, useMemo, useDeferredValue, useCallback, startTransition } from 'react';
import {
Dialog,
DialogContent,
@@ -22,8 +14,16 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import React, { useState, useMemo, useDeferredValue, useCallback, startTransition } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FileText, Save, Trash2 } from 'lucide-react';
import { useTRPC } from '@/providers/query-provider';
import { useTemplates } from '@/hooks/use-templates';
import { Button } from '@/components/ui/button';
import { TRPCClientError } from '@trpc/client';
import { Input } from '@/components/ui/input';
import { Editor } from '@tiptap/react';
import { toast } from 'sonner';
type RecipientField = 'to' | 'cc' | 'bcc';
@@ -37,9 +37,11 @@ type Template = {
bcc?: string[] | null;
};
type TemplatesQueryData = {
templates: Template[];
} | undefined;
type TemplatesQueryData =
| {
templates: Template[];
}
| undefined;
interface TemplateButtonProps {
editor: Editor | null;
@@ -63,7 +65,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
const trpc = useTRPC();
const queryClient = useQueryClient();
const { data } = useTemplates();
const templates: Template[] = data?.templates ?? [];
const [menuOpen, setMenuOpen] = useState(false);
@@ -76,9 +78,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
const filteredTemplates = useMemo(() => {
if (!deferredSearch.trim()) return templates;
return templates.filter((t) =>
t.name.toLowerCase().includes(deferredSearch.toLowerCase()),
);
return templates.filter((t) => t.name.toLowerCase().includes(deferredSearch.toLowerCase()));
}, [deferredSearch, templates]);
const { mutateAsync: createTemplate } = useMutation(trpc.templates.create.mutationOptions());
@@ -123,16 +123,19 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
}
};
const handleApplyTemplate = useCallback((template: Template) => {
if (!editor) return;
startTransition(() => {
if (template.subject) setSubject(template.subject);
if (template.body) editor.commands.setContent(template.body, false);
if (template.to) setRecipients('to', template.to);
if (template.cc) setRecipients('cc', template.cc);
if (template.bcc) setRecipients('bcc', template.bcc);
});
}, [editor, setSubject, setRecipients]);
const handleApplyTemplate = useCallback(
(template: Template) => {
if (!editor) return;
startTransition(() => {
if (template.subject) setSubject(template.subject);
if (template.body) editor.commands.setContent(template.body, false);
if (template.to) setRecipients('to', template.to);
if (template.cc) setRecipients('cc', template.cc);
if (template.bcc) setRecipients('bcc', template.bcc);
});
},
[editor, setSubject, setRecipients],
);
const handleDeleteTemplate = useCallback(
async (templateId: string) => {
@@ -157,7 +160,13 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
<>
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button size={'xs'} variant={'secondary'} disabled={isSaving}>
<Button
disabled={isSaving}
type="button"
size={'xs'}
variant={'secondary'}
className="hover:bg-accent flex items-center gap-1 rounded-md bg-white/10 px-2 py-1 text-sm text-black dark:bg-black/10 dark:text-white"
>
Templates
</Button>
</DropdownMenuTrigger>
@@ -177,7 +186,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
<FileText className="mr-2 h-3.5 w-3.5" /> Use template
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="z-99999 w-60">
<div className="p-2 border-b border-border sticky top-0 bg-background">
<div className="border-border bg-background sticky top-0 border-b p-2">
<Input
placeholder="Search..."
value={search}
@@ -195,10 +204,10 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
>
<span className="flex-1 truncate text-left">{t.name}</span>
<button
className="p-0.5 text-muted-foreground hover:text-destructive"
className="text-muted-foreground hover:text-destructive p-0.5"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
setMenuOpen(false);
toast(`Delete template "${t.name}"?`, {
duration: 10000,
action: {
@@ -217,7 +226,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
</DropdownMenuItem>
))}
{filteredTemplates.length === 0 && (
<div className="p-2 text-xs text-muted-foreground">No templates</div>
<div className="text-muted-foreground p-2 text-xs">No templates</div>
)}
</div>
</DropdownMenuSubContent>
@@ -231,7 +240,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
<DialogHeader>
<DialogTitle>Save as Template</DialogTitle>
</DialogHeader>
<div className="py-4 space-y-2">
<div className="space-y-2 py-4">
<Input
placeholder="Template name"
value={templateName}
@@ -240,11 +249,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
/>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setSaveDialogOpen(false)}
>
<Button variant="ghost" size="sm" onClick={() => setSaveDialogOpen(false)}>
Cancel
</Button>
<Button size="sm" onClick={handleSaveTemplate} disabled={isSaving}>
@@ -257,4 +262,4 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
);
};
export const TemplateButton = React.memo(TemplateButtonComponent);
export const TemplateButton = React.memo(TemplateButtonComponent);

View File

@@ -197,7 +197,7 @@ export const CurvedArrow = ({ className }: { className?: string }) => (
<svg
width="2em"
height="2em"
fill="none"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
viewBox="0 0 16 16"