ghost-listmonk-connector/ui/repopack-output-ui.txt
troneras 1fd7dad8e4 # This is a combination of 2 commits.
# This is the 1st commit message:

first commit

# This is the commit message #2:

fixed cache
2024-08-21 02:08:01 +02:00

1984 lines
56 KiB
Text

================================================================
Repopack Output File
================================================================
This file was generated by Repopack on: 2024-08-11T23:27:27.749Z
Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
File Format:
------------
The content is organized as follows:
1. This header section
2. Repository structure
3. Multiple file entries, each consisting of:
a. A separator line (================)
b. The file path (File: path/to/file)
c. Another separator line
d. The full contents of the file
e. A blank line
Usage Guidelines:
-----------------
1. This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
2. When processing this file, use the separators and "File:" markers to
distinguish between different files in the repository.
3. Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
Notes:
------
- Some files may have been excluded based on .gitignore rules and Repopack's
configuration.
- Binary files are not included in this packed representation. Please refer to
the Repository Structure section for a complete list of file paths, including
binary files.
For more information about Repopack, visit: https://github.com/yamadashy/repopack
================================================================
Repository Structure
================================================================
app/
sons/
[id]/
page.tsx
new/
page.tsx
page.tsx
favicon.ico
layout.tsx
page.tsx
components/
ActionForm.tsx
CampaignActionFields.tsx
Dashboard.tsx
layout.tsx
ManageSubscriberActionFields.tsx
SonCreationForm.tsx
SonDetailsForm.tsx
SonList.tsx
hooks/
useCustomToast.ts
useLists.ts
useSon.ts
useSons.ts
useTemplates.ts
lib/
api-client.ts
schemas.ts
types.ts
utils.ts
public/
next.svg
vercel.svg
styles/
globals.css
.eslintrc.json
.gitignore
components.json
next.config.mjs
package.json
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
================================================================
Repository Files
================================================================
================
File: app/sons/[id]/page.tsx
================
"use client";
import React, { useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { PlusCircle } from "lucide-react";
import { useSon } from "@/hooks/useSon";
import { useSons } from "@/hooks/useSons";
import { useLists } from "@/hooks/useLists";
import { useTemplates } from "@/hooks/useTemplates";
import { useCustomToast } from "@/hooks/useCustomToast";
import { SonDetailsForm } from "@/components/SonDetailsForm";
import { ActionForm } from "@/components/ActionForm";
import { editableSonSchema } from "@/lib/schemas";
import { EditableSon, Son } from "@/lib/types";
// Define a type guard function
function isValidTrigger(trigger: string): trigger is EditableSon["trigger"] {
return [
"member_created",
"member_deleted",
"member_updated",
"post_published",
"post_scheduled",
].includes(trigger);
}
export default function SonDetailPage() {
const params = useParams();
const router = useRouter();
const {
son,
loading: sonLoading,
error: sonError,
} = useSon(params.id as string);
const { updateSon } = useSons();
const { showToast } = useCustomToast();
const { lists, loading: listsLoading, error: listsError } = useLists();
const {
templates,
loading: templatesLoading,
error: templatesError,
} = useTemplates();
const form = useForm<EditableSon>({
resolver: zodResolver(editableSonSchema),
defaultValues: {
name: "",
trigger: "member_created",
delay: 0,
actions: [],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "actions",
});
useEffect(() => {
if (son) {
const { name, trigger, delay, actions } = son;
// Use the type guard to ensure the trigger is valid
if (isValidTrigger(trigger)) {
form.reset({ name, trigger, delay, actions: [] });
} else {
console.error(`Invalid trigger value: ${trigger}`);
// You might want to set a default value or handle this case differently
form.reset({ name, trigger: "member_created", delay, actions: [] });
}
}
}, [son, form]);
if (sonLoading || listsLoading || templatesLoading) {
return (
<div className="flex justify-center items-center h-full">Loading...</div>
);
}
if (sonError || listsError || templatesError) {
return (
<div className="text-red-500">
Error:{" "}
{sonError?.message || listsError?.message || templatesError?.message}
</div>
);
}
if (!son) return <div>Son not found</div>;
const onSubmit = async (data: EditableSon) => {
try {
await updateSon(son.id, data);
showToast("Success", "Son updated successfully");
router.push("/sons");
} catch (error) {
showToast("Error", "Failed to update Son", "destructive");
console.error("Failed to update Son:", error);
}
};
console.log("son", son);
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Edit Son: {son.name}</h1>
<p className="text-sm text-gray-500">
Created at: {new Date(son.created_at).toLocaleString()}
</p>
<p className="text-sm text-gray-500">
Last updated: {new Date(son.updated_at).toLocaleString()}
</p>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<SonDetailsForm form={form} />
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{fields.map((field, index) => (
<ActionForm
key={field.id}
form={form}
index={index}
remove={remove}
lists={lists}
templates={templates}
/>
))}
<Button
type="button"
onClick={() =>
append({ type: "send_transactional_email", parameters: {} })
}
variant="outline"
className="w-full"
>
<PlusCircle className="mr-2 h-4 w-4" /> Add Action
</Button>
</CardContent>
</Card>
<Button type="submit" className="w-full">
Save Changes
</Button>
</form>
</Form>
</div>
);
}
================
File: app/sons/new/page.tsx
================
import SonCreationForm from "@/components/SonCreationForm";
export default function NewSonPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">Create New Son</h1>
<SonCreationForm />
</div>
);
}
================
File: app/sons/page.tsx
================
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SonList } from "@/components/SonList";
export default function SonListPage() {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Manage Sons</h1>
<Link href="/sons/new">
<Button>Create New Son</Button>
</Link>
</div>
<SonList />
</div>
);
}
================
File: app/layout.tsx
================
import "@/styles/globals.css";
import { Inter as FontSans } from "next/font/google";
import { cn } from "@/lib/utils";
import Layout from "@/components/layout";
import { Toaster } from "@/components/ui/toaster";
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable
)}
>
<Layout>{children}</Layout>
<Toaster />
</body>
</html>
);
}
================
File: app/page.tsx
================
import Dashboard from "@/components/Dashboard";
export default function HomePage() {
return <Dashboard />;
}
================
File: components/ActionForm.tsx
================
import React from "react";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Trash2 } from "lucide-react";
import { CampaignActionFields } from "./CampaignActionFields";
import { ManageSubscriberActionFields } from "./ManageSubscriberActionFields";
import { ActionFormProps } from "@/lib/types";
export function ActionForm({
form,
index,
remove,
lists,
templates,
}: ActionFormProps) {
const actionType = form.watch(`actions.${index}.type`);
return (
<Card className="border border-gray-200">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Action {index + 1}
</CardTitle>
<Button
type="button"
onClick={() => remove(index)}
variant="ghost"
size="sm"
>
<Trash2 className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name={`actions.${index}.type`}
render={({ field }) => (
<FormItem>
<FormLabel>Action Type</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue(`actions.${index}.parameters`, {});
}}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an action type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="send_transactional_email">
Send Transactional Email
</SelectItem>
<SelectItem value="manage_subscriber">
Manage Subscriber
</SelectItem>
<SelectItem value="create_campaign">
Create Campaign
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{actionType === "create_campaign" && (
<CampaignActionFields
form={form}
index={index}
lists={lists}
templates={templates}
/>
)}
{actionType === "manage_subscriber" && (
<ManageSubscriberActionFields
form={form}
index={index}
lists={lists}
/>
)}
{actionType === "send_transactional_email" && (
<FormField
control={form.control}
name={`actions.${index}.parameters`}
render={({ field }) => (
<FormItem>
<FormLabel>Parameters</FormLabel>
<FormControl>
<Textarea
{...field}
onChange={(e) => {
try {
const parsedValue = JSON.parse(e.target.value);
field.onChange(parsedValue);
} catch (error) {
field.onChange(e.target.value);
}
}}
value={
typeof field.value === "object"
? JSON.stringify(field.value, null, 2)
: field.value
}
className="font-mono text-sm"
rows={5}
/>
</FormControl>
<FormDescription>
Enter the parameters for the transactional email as JSON.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</CardContent>
</Card>
);
}
================
File: components/CampaignActionFields.tsx
================
import React from "react";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { CampaignActionFieldsProps } from "@/lib/types";
export function CampaignActionFields({ form, index, lists, templates }: CampaignActionFieldsProps) {
return (
<div className="space-y-4">
<FormField
control={form.control}
name={`actions.${index}.parameters.subject`}
render={({ field }) => (
<FormItem>
<FormLabel>Subject</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter campaign subject" />
</FormControl>
<FormDescription>
The subject line for your campaign email.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`actions.${index}.parameters.lists`}
render={({ field }) => (
<FormItem>
<FormLabel>Lists</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
const newLists = [...(field.value || []), parseInt(value)];
field.onChange(newLists);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select lists" />
</SelectTrigger>
<SelectContent>
{lists.map((list) => (
<SelectItem key={list.id} value={list.id.toString()}>
{list.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<div className="mt-2 flex flex-wrap gap-2">
{field.value?.map((listId: number) => {
const list = lists.find((l) => l.id === listId);
return (
<Badge key={listId} variant="secondary" className="px-2 py-1">
{list ? list.name : `List ${listId}`}
<Button
variant="ghost"
size="sm"
className="ml-1 h-4 w-4 p-0"
onClick={() => {
const newLists = field.value.filter(
(id: number) => id !== listId
);
field.onChange(newLists);
}}
>
<X className="h-3 w-3" />
</Button>
</Badge>
);
})}
</div>
<FormDescription>
Select the lists to send this campaign to.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`actions.${index}.parameters.template_id`}
render={({ field }) => (
<FormItem>
<FormLabel>Template</FormLabel>
<FormControl>
<Select
onValueChange={(value) => field.onChange(parseInt(value))}
value={field.value?.toString()}
>
<SelectTrigger>
<SelectValue placeholder="Select template" />
</SelectTrigger>
<SelectContent>
{templates.map((template) => (
<SelectItem
key={template.id}
value={template.id.toString()}
>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Choose the template for your campaign email.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`actions.${index}.parameters.tags`}
render={({ field }) => (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter comma-separated tags" />
</FormControl>
<FormDescription>
Add tags to categorize your campaign (comma-separated).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`actions.${index}.parameters.send_now`}
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Send Now</FormLabel>
<FormDescription>
Toggle to send the campaign immediately upon creation.
</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
</div>
);
}
================
File: components/Dashboard.tsx
================
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
import { ChartContainer, ChartConfig } from "@/components/ui/chart";
// Mock data - replace with actual API calls in production
const recentActivity = [
{
id: 1,
action: 'Son "Welcome Email" triggered',
timestamp: "2024-08-10T10:30:00Z",
},
{
id: 2,
action: 'New Son "Survey Request" created',
timestamp: "2024-08-10T09:15:00Z",
},
{
id: 3,
action: 'Son "Monthly Newsletter" modified',
timestamp: "2024-08-09T16:45:00Z",
},
];
const sonStats = [
{ name: "Welcome Email", executions: 120, success: 115, failure: 5 },
{ name: "Survey Request", executions: 80, success: 78, failure: 2 },
{ name: "Monthly Newsletter", executions: 50, success: 50, failure: 0 },
];
const chartConfig = {
executions: {
label: "Executions",
color: "#2563eb",
},
success: {
label: "Success",
color: "#60a5fa",
},
failure: {
label: "Failure",
color: "#d1d5db",
},
} satisfies ChartConfig;
export default function Dashboard() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate API call
setTimeout(() => setIsLoading(false), 1000);
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Dashboard</h1>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
{recentActivity.map((activity) => (
<Alert key={activity.id} className="mb-2">
<AlertTitle>{activity.action}</AlertTitle>
<AlertDescription>
{new Date(activity.timestamp).toLocaleString()}
</AlertDescription>
</Alert>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Son Performance</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer
config={chartConfig}
className="min-h-[200px] w-full"
>
<BarChart data={sonStats}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="success" fill="var(--color-success)" />
<Bar dataKey="failure" fill="var(--color-failure)" />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</div>
);
}
================
File: components/layout.tsx
================
"use client";
import { useState } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
LayoutDashboard,
ListPlus,
ListTree,
Settings,
Menu,
} from "lucide-react";
const Sidebar = ({ isOpen }: { isOpen: boolean }) => {
const pathname = usePathname();
const menuItems = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/sons/new", label: "Create Son", icon: ListPlus },
{ href: "/sons", label: "Manage Sons", icon: ListTree },
{ href: "/settings", label: "Settings", icon: Settings },
];
return (
<aside
className={`bg-gray-800 text-white w-64 min-h-screen p-4 ${
isOpen ? "" : "hidden"
} md:block`}
>
<nav>
<ul>
{menuItems.map((item) => (
<li key={item.href} className="mb-2">
<Link href={item.href}>
<Button
variant={pathname === item.href ? "secondary" : "ghost"}
className="w-full justify-start"
>
<item.icon className="mr-2 h-4 w-4" />
{item.label}
</Button>
</Link>
</li>
))}
</ul>
</nav>
</aside>
);
};
export default function Layout({ children }: { children: React.ReactNode }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
return (
<div className="flex min-h-screen">
<Sidebar isOpen={isSidebarOpen} />
<div className="flex-1">
<header className="bg-white shadow p-4 flex justify-between items-center">
<Button
variant="ghost"
className="md:hidden"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
<Menu className="h-6 w-6" />
</Button>
<h1 className="text-xl font-bold">Ghost-Listmonk Connector</h1>
</header>
<main className="p-4 max-w-3xl mx-auto">{children}</main>
</div>
</div>
);
}
================
File: components/ManageSubscriberActionFields.tsx
================
import React from "react";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { ManageSubscriberActionFieldsProps } from "@/lib/types";
export function ManageSubscriberActionFields({ form, index, lists } : ManageSubscriberActionFieldsProps) {
return (
<div className="space-y-4">
<FormField
control={form.control}
name={`actions.${index}.parameters.lists`}
render={({ field }) => (
<FormItem>
<FormLabel>Lists</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
const newLists = [...(field.value || []), parseInt(value)];
field.onChange(newLists);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select lists" />
</SelectTrigger>
<SelectContent>
{lists.map((list) => (
<SelectItem key={list.id} value={list.id.toString()}>
{list.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<div className="mt-2 flex flex-wrap gap-2">
{field.value?.map((listId: number) => {
const list = lists.find((l) => l.id === listId);
return (
<Badge key={listId} variant="secondary" className="px-2 py-1">
{list ? list.name : `List ${listId}`}
<Button
variant="ghost"
size="sm"
className="ml-1 h-4 w-4 p-0"
onClick={() => {
const newLists = field.value.filter(
(id: number) => id !== listId
);
field.onChange(newLists);
}}
>
<X className="h-3 w-3" />
</Button>
</Badge>
);
})}
</div>
<FormDescription>
Default lists to add this user on creation.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Add more fields specific to manage_subscriber action if needed */}
</div>
);
}
================
File: components/SonCreationForm.tsx
================
"use client";
import React from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { PlusCircle } from "lucide-react";
import { useSons } from "@/hooks/useSons";
import { useLists } from "@/hooks/useLists";
import { useTemplates } from "@/hooks/useTemplates";
import { useCustomToast } from "@/hooks/useCustomToast";
import { SonDetailsForm } from "./SonDetailsForm";
import { ActionForm } from "./ActionForm";
// Import or define your schema here
import { sonSchema } from "@/lib/schemas";
type SonFormValues = z.infer<typeof sonSchema>;
export default function SonCreationForm() {
const { createSon } = useSons();
const { showToast } = useCustomToast();
const router = useRouter();
const { lists, loading: listsLoading, error: listsError } = useLists();
const {
templates,
loading: templatesLoading,
error: templatesError,
} = useTemplates();
const form = useForm<SonFormValues>({
resolver: zodResolver(sonSchema),
defaultValues: {
name: "",
trigger: "member_created",
delay: 0,
actions: [
{
type: "send_transactional_email",
parameters: {},
},
],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "actions",
});
const onSubmit = async (data: SonFormValues) => {
try {
await createSon(data);
showToast("Success", "Son created successfully");
router.push("/sons");
} catch (error) {
showToast("Error", "Failed to create Son", "destructive");
console.error("Failed to create Son:", error);
}
};
if (listsLoading || templatesLoading) {
return (
<div className="flex justify-center items-center h-full">Loading...</div>
);
}
if (listsError || templatesError) {
return (
<div className="text-red-500">
Error: {listsError?.message || templatesError?.message}
</div>
);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<SonDetailsForm form={form} />
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{fields.map((field, index) => (
<ActionForm
key={field.id}
form={form}
index={index}
remove={remove}
lists={lists}
templates={templates}
/>
))}
<Button
type="button"
onClick={() =>
append({ type: "send_transactional_email", parameters: {} })
}
variant="outline"
className="w-full"
>
<PlusCircle className="mr-2 h-4 w-4" /> Add Action
</Button>
</CardContent>
</Card>
<Button type="submit" className="w-full">
Create Son
</Button>
</form>
</Form>
);
}
================
File: components/SonDetailsForm.tsx
================
import React from "react";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SonDetailsFormProps } from "@/lib/types";
export function SonDetailsForm({ form }: SonDetailsFormProps) {
return (
<Card>
<CardHeader>
<CardTitle>Son Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Son Name</FormLabel>
<FormControl>
<Input placeholder="Enter Son name" {...field} />
</FormControl>
<FormDescription>
Give your Son a unique and descriptive name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="trigger"
render={({ field }) => (
<FormItem>
<FormLabel>Trigger</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a trigger" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="member_created">Member Created</SelectItem>
<SelectItem value="member_deleted">Member Deleted</SelectItem>
<SelectItem value="member_updated">Member Updated</SelectItem>
<SelectItem value="post_published">Post Published</SelectItem>
<SelectItem value="post_scheduled">Post Scheduled</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose the event that will trigger this Son.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="delay"
render={({ field }) => (
<FormItem>
<FormLabel>Delay (minutes)</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value))}
/>
</FormControl>
<FormDescription>
Set a delay before the Son executes its actions.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
);
}
================
File: components/SonList.tsx
================
"use client";
import React from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Pencil, Trash2 } from "lucide-react";
import { useSons } from "@/hooks/useSons";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
export function SonList() {
const { sons, loading, error, deleteSon } = useSons();
if (loading) {
return <div>Loading sons...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<Card>
<CardHeader>
<CardTitle>Your Sons</CardTitle>
</CardHeader>
<CardContent>
{sons.length === 0 && <div>No sons found. Create your first son!</div>}
{sons.length > 0 && (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Trigger</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sons.map((son) => (
<TableRow key={son.id}>
<TableCell>{son.name}</TableCell>
<TableCell>{son.trigger}</TableCell>
<TableCell>
<div className="flex space-x-2">
<Link href={`/sons/${son.id}`}>
<Button variant="outline" size="icon">
<Pencil className="h-4 w-4" />
</Button>
</Link>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will
permanently delete the Son.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteSon(son.id)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
);
}
================
File: hooks/useCustomToast.ts
================
import { useToast } from "@/components/ui/use-toast"
export function useCustomToast() {
const { toast } = useToast()
const showToast = (title: string, description: string, variant: "default" | "destructive" = "default") => {
toast({
title,
description,
variant,
})
}
return { showToast }
}
================
File: hooks/useLists.ts
================
import { useState, useEffect, useCallback } from 'react';
import { apiClient } from '@/lib/api-client';
import { ListmonkList } from '@/lib/types';
export function useLists() {
const [lists, setLists] = useState<ListmonkList[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchLists = useCallback(async () => {
setLoading(true);
try {
const response = await apiClient.get<{ data: ListmonkList[] }>('/lists');
setLists(response.data.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchLists();
}, [fetchLists]);
return { lists, loading, error, fetchLists };
}
================
File: hooks/useSon.ts
================
// src/hooks/useSon.ts
import { useState, useEffect, useCallback } from 'react';
import { apiClient } from '@/lib/api-client';
import { Son } from '@/lib/types';
export function useSon(id: string) {
const [son, setSon] = useState<Son | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchSon = useCallback(async () => {
setLoading(true);
try {
const response = await apiClient.get<Son>(`/sons/${id}`);
setSon(response.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
fetchSon();
}, [fetchSon]);
return { son, loading, error, fetchSon };
}
================
File: hooks/useSons.ts
================
// src/hooks/useSons.ts
import { useState, useEffect, useCallback } from 'react';
import { apiClient } from '@/lib/api-client';
import { Son } from '@/lib/types';
export function useSons() {
const [sons, setSons] = useState<Son[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchSons = useCallback(async () => {
setLoading(true);
try {
const response = await apiClient.get<Son[]>('/sons');
setSons(response.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
} finally {
setLoading(false);
}
}, []);
const createSon = useCallback(async (sonData: Omit<Son, 'id' | 'created_at' | 'updated_at'>) => {
setLoading(true);
try {
const response = await apiClient.post<Son>('/sons', sonData);
setSons(prevSons => [...prevSons, response.data]);
setError(null);
return response.data;
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
throw err;
} finally {
setLoading(false);
}
}, []);
const updateSon = useCallback(async (id: string, sonData: Partial<Son>) => {
setLoading(true);
try {
const response = await apiClient.put<Son>(`/sons/${id}`, sonData);
setSons(prevSons => prevSons.map(son => son.id === id ? response.data : son));
setError(null);
return response.data;
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
throw err;
} finally {
setLoading(false);
}
}, []);
const deleteSon = useCallback(async (id: string) => {
setLoading(true);
try {
await apiClient.delete(`/sons/${id}`);
setSons(prevSons => prevSons.filter(son => son.id !== id));
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
throw err;
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchSons();
}, [fetchSons]);
return { sons, loading, error, fetchSons, createSon, updateSon, deleteSon };
}
================
File: hooks/useTemplates.ts
================
import { useState, useEffect, useCallback } from 'react';
import { apiClient } from '@/lib/api-client';
import { ListmonkTemplate } from '@/lib/types';
export function useTemplates() {
const [templates, setTemplates] = useState<ListmonkTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchTemplates = useCallback(async () => {
setLoading(true);
try {
const response = await apiClient.get<{ data: ListmonkTemplate[] }>('/templates');
setTemplates(response.data.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchTemplates();
}, [fetchTemplates]);
return { templates, loading, error, fetchTemplates };
}
================
File: lib/api-client.ts
================
// src/lib/api-client.ts
import axios from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8808';
const API_KEY = process.env.NEXT_PUBLIC_API_KEY || "your-api-key";
export const apiClient = axios.create({
baseURL: API_BASE_URL.replace(/\/$/, ''), // Remove trailing slash if present
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
withCredentials: true,
});
================
File: lib/schemas.ts
================
import * as z from 'zod';
// Define the schema for action parameters
const actionParametersSchema = z.object({
subject: z.string().optional(),
lists: z.array(z.number()).optional(),
template_id: z.number().optional(),
tags: z.string().optional(),
send_now: z.boolean().optional(),
}).strict().or(z.record(z.any())); // Allow any other properties for flexibility
// Define the schema for a single action
const actionSchema = z.object({
type: z.enum(['send_transactional_email', 'manage_subscriber', 'create_campaign']),
parameters: actionParametersSchema,
});
// Define the schema for the entire Son object
export const sonSchema = z.object({
id: z.string().optional(), // Optional because it might not be present when creating a new Son
name: z.string().min(1, 'Name is required'),
trigger: z.enum([
'member_created',
'member_deleted',
'member_updated',
'post_published',
'post_scheduled',
]),
delay: z.number().min(0, 'Delay must be a positive number'),
actions: z.array(actionSchema).min(1, 'At least one action is required'),
created_at: z.date().optional(),
updated_at: z.date().optional(),
});
export const editableSonSchema = z.object({
name: z.string().min(1, 'Name is required'),
trigger: z.enum([
'member_created',
'member_deleted',
'member_updated',
'post_published',
'post_scheduled',
]),
delay: z.number().min(0, 'Delay must be a positive number'),
actions: z.array(z.object({
type: z.enum(['send_transactional_email', 'manage_subscriber', 'create_campaign']),
parameters: z.record(z.any()),
})),
});
// Define a schema for creating a new Son (without id, createdAt, and updatedAt)
export const createSonSchema = sonSchema.omit({ id: true, created_at: true, updated_at: true });
// Define a schema for updating an existing Son
export const updateSonSchema = sonSchema.partial().extend({
id: z.string(),
});
// Define types for create and update operations
export type CreateSonInput = z.infer<typeof createSonSchema>;
export type UpdateSonInput = z.infer<typeof updateSonSchema>;
================
File: lib/types.ts
================
import { UseFormReturn, FieldValues } from 'react-hook-form';
import { editableSonSchema } from '@/lib/schemas'; // Assuming this is where your Son type is defined
import { z } from 'zod';
// Define a type for the Son object based on the schema
export interface Son extends z.infer<typeof editableSonSchema> {
id: string;
created_at: string;
updated_at: string;
}
// Type for editable Son fields
export type EditableSon = z.infer<typeof editableSonSchema>;
export interface ListmonkList {
id: number;
name: string;
type: string;
optin: string;
tags: string[];
}
export interface ListmonkTemplate {
id: number;
name: string;
type: string;
is_default: boolean;
}
// Props for CampaignActionFields
export interface CampaignActionFieldsProps {
form: UseFormReturn<EditableSon>;
index: number;
lists: ListmonkList[];
templates: ListmonkTemplate[];
}
// Props for ManageSubscriberActionFields
export interface ManageSubscriberActionFieldsProps {
form: UseFormReturn<EditableSon>;
index: number;
lists: ListmonkList[];
}
// Props for SonDetailsForm
export interface SonDetailsFormProps {
form: UseFormReturn<EditableSon>;
}
export interface ActionFormProps {
form: UseFormReturn<EditableSon>;
index: number;
remove: (index: number) => void;
lists: ListmonkList[];
templates: ListmonkTemplate[];
}
================
File: lib/utils.ts
================
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
================
File: public/next.svg
================
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
================
File: public/vercel.svg
================
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
================
File: styles/globals.css
================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
================
File: .eslintrc.json
================
{
"extends": "next/core-web-vitals"
}
================
File: .gitignore
================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================
File: components.json
================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
================
File: next.config.mjs
================
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
================
File: package.json
================
{
"name": "ghost-listmonk-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"axios": "^1.7.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.427.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.2",
"recharts": "^2.12.7",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
================
File: postcss.config.mjs
================
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
================
File: README.md
================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
================
File: tailwind.config.ts
================
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config
================
File: tsconfig.json
================
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}