Skip to content

Commit

Permalink
Merge pull request #39 from sireto/31-frontend/template
Browse files Browse the repository at this point in the history
UI for templates
  • Loading branch information
ankit-sapkota authored Feb 14, 2025
2 parents 06d4aeb + 57375ea commit 24ccb14
Show file tree
Hide file tree
Showing 25 changed files with 2,781 additions and 397 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target*
.env*

diesel.toml*
diesel.toml*
.vscode/
3 changes: 2 additions & 1 deletion backend/src/repositories/template_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl TemplateRepository for TemplateRespositoryImpl {
name.eq(payload.name),
template_data.eq(payload.template_data),
content_html.eq(payload.content_html),
content_plaintext.eq(payload.content_plaintext)
content_plaintext.eq(payload.content_plaintext),
updated_at.eq(diesel::dsl::now),
))
.get_result(&mut conn)
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ yarn-error.log*
next-env.d.ts

*storybook.log

.editorconfig
.gitattributes
36 changes: 36 additions & 0 deletions frontend/app/api/parse-mjml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import mjml2html from "mjml";

/**
* @description A POST function to parse MJML to HTML
* @returns Parsed HTML string or an error response
*/
export async function POST(req: NextRequest) {
try {
const { mjml_content } = await req.json();

if (!mjml_content) {
return NextResponse.json({ error: "MJML content is required" }, { status: 400 });
}

// console.log("Received MJML content:", mjml_content);

console.warn("BEFORE THE PARSING");
const { html } = mjml2html(mjml_content);
console.warn("AFTER THE PARSING");


// if (errors.length) {
// console.error("MJML Parsing Errors:", errors);
// return NextResponse.json({ error: "MJML parsing failed", details: errors }, { status: 400 });
// }
// const stringified_json = JSON.stringify({
// html
// });

return NextResponse.json({ data: html }, { status: 200 });
} catch (err) {
console.error("Server Error:", err);
return NextResponse.json({ error: "Internal Server Error", details: (err as Error).message }, { status: 500 });
}
}
10 changes: 7 additions & 3 deletions frontend/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@

// app/dashboard/layout.tsx
import Sidebar from "@/components/Sidebar";
import ReduxProvider from "@/providers/redux-provider";


export default function DashboardLayout({
children,
Expand All @@ -9,9 +12,10 @@ export default function DashboardLayout({
return (
<div className="flex h-[calc(100vh-64px)]">
{" "}
{/* Adjust 64px based on your header height */}
<Sidebar />
<div className="flex-1 overflow-auto p-8">{children}</div>
<ReduxProvider>
<Sidebar />
<div className="flex-1 overflow-auto p-8">{children}</div>
</ReduxProvider>
</div>
);
}
78 changes: 78 additions & 0 deletions frontend/app/dashboard/templates/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import React from 'react'
import { ColumnDef } from '@tanstack/react-table';
import { formatDate } from '@/lib/utils';
import { Edit3, ScanEye, Trash2 } from 'lucide-react';
import { EditTemplateForm } from '@/components/TemplateForms';
import Modal from '@/components/Modal';
import { useDeleteTemplateMutation, useGetTemplatesQuery } from '@/app/services/TemplateApi';


const updateBtn = {
label: "Edit",
icon: <Edit3 size={20} />
};

export const columns: ColumnDef<any>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span>{row.getValue("name")}</span>,
},
{
accessorKey: "created_at",
header: "Created",
cell: ({ row }) => <span>{formatDate(row.getValue("created_at"))}</span>,
},
{
accessorKey: "updated_at",
header: "Updated",
cell: ({ row }) => <span>{formatDate(row.getValue("updated_at"))}</span>,
},
{
accessorKey: "actions",
header: "",
cell: ({ row }) => {
const templateId = row.original.id;

const { refetch } = useGetTemplatesQuery();

const [ deleteTemplate, { isLoading: isDeleting, error: deletionError } ] = useDeleteTemplateMutation();

const deleteTemplateHandler = async (id: string) => {
if (deletionError) {
return <div>Error deleting the template</div>
}

await deleteTemplate(id);
refetch();
}


return (
<div className='flex space-x-4 text-primary items-center'>
<button className='transition-all duration-300 ease-in-out hover:scale-105'><ScanEye size={20} /></button>
{/* <button className='transition-all duration-300 ease-in-out hover:scale-105'><Edit3 size={16} /></button> */}
<Modal
triggerLabel={updateBtn}
dialogBody={<EditTemplateForm templateId={templateId}/>}
dialogTitle={"Edit Template"}
dialogDescription={"Edit your template"}
/>
<button
className='transition-all duration-300 ease-in-out hover:scale-105'
onClick={() => {
deleteTemplateHandler(templateId);
}}
>
<Trash2 size={20} className='text-red-400' />
</button>
</div>
)
}

},
]

export default columns
50 changes: 50 additions & 0 deletions frontend/app/dashboard/templates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import React from 'react'

import Modal from '@/components/Modal';
import { Plus } from "lucide-react";
import { AddTemplateForm } from '@/components/TemplateForms';
import { useGetTemplatesQuery } from '@/app/services/TemplateApi';
import DataTable from '@/components/DataTable';
import columns from './columns';

const page = () => {
const addBtn = {
label: "New",
icon: <Plus size={24} />
};

const { data: templates, error, isLoading } = useGetTemplatesQuery();

if (error) {
return <div>There was an error fetching templates...</div>
}

if (isLoading) {
return <div> Loading... </div>
}

return (
<div className=''>
{/* Template page heading... */}
<div className='w-full flex justify-between items-center'>
<h1 className='text-xl font-bold'>
Templates
<span>({templates?.length})</span>
</h1>
<Modal
triggerLabel={addBtn}
dialogBody={<AddTemplateForm />}
dialogTitle={"New template"}
dialogDescription={"Add a new template"}
/>
</div>
<div className='my-12'>
<DataTable data={templates || []} columns={columns}/>
</div>
</div>
)
}

export default page
52 changes: 52 additions & 0 deletions frontend/app/services/TemplateApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { z } from 'zod';
import {
TemplateDTO,
CreateTemplateResponseDTO,
CreateTemplateRequestDTO,
UpdateTemplateRequestDTO,
UpdateTemplateResponseDTO
} from '@/lib/type';

type Template = z.infer<typeof TemplateDTO>;
type CreateTemplateRequest = z.infer<typeof CreateTemplateRequestDTO>;
type CreateTemplateResponse = z.infer<typeof CreateTemplateResponseDTO>;
type UpdateTemplateRequest = z.infer<typeof UpdateTemplateRequestDTO>;
type UpdateTemplateResponse = z.infer<typeof UpdateTemplateResponseDTO>;

// Template API slice...
export const templateApi = createApi({
reducerPath: "templateApi",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:8000/api/templates"}),
endpoints: (builder) => ({
// Query to fetch all templates...
getTemplates: builder.query<Template[], void>({query: () => ""}),
createTemplate: builder.mutation<CreateTemplateResponse, CreateTemplateRequest>({
query: (newTemplate) => ({
url: "",
method: "POST",
body: newTemplate
})
}),
updateTemplate: builder.mutation<UpdateTemplateResponse, {templateId: string , updatedTemplate: UpdateTemplateRequest}>({
query: ({templateId, updatedTemplate}) => ({
url: `/${templateId}`,
method: "PATCH",
body: updatedTemplate
})
}),
deleteTemplate: builder.mutation<void, string>({
query: (templateId) => ({
url: `/${templateId}`,
method: "DELETE",
})
})
})
});

export const {
useGetTemplatesQuery,
useCreateTemplateMutation,
useUpdateTemplateMutation,
useDeleteTemplateMutation
} = templateApi;
75 changes: 75 additions & 0 deletions frontend/components/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';

import React from 'react';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Table, TableCaption, TableHeader, TableRow, TableHead, TableBody, TableCell } from './ui/table';

interface DataTableProps<TData, TValue> {
data: TData[]
columns: ColumnDef<TData, TValue>[]
};

/**
*
* @param param0
* @description create a data table component with any number of columns...
* @returns
*/
export function DataTable<TData, TValue>({
data,
columns
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});

return (
<Table>
<TableCaption>A list of templates</TableCaption>
<TableHeader className='bg-secondary'>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className='font-bold'>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className='cursor-pointer'>
{row.getVisibleCells().map((cell, idx) => (
<TableCell key={cell.id} className={`${idx === 0 ? 'text-primary' : 'text-secondary-foreground/60'} min-w-[80px]`}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No templates.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}

export default DataTable;
Loading

0 comments on commit 24ccb14

Please sign in to comment.