create/update actions and redis cahce store completed tested

This commit is contained in:
Berkay 2025-06-21 23:44:52 +03:00
parent 06ca2b0835
commit 311736ce06
19 changed files with 1717 additions and 48 deletions

View File

@ -43,10 +43,44 @@ Network Manager NM Network Manager
Application Manager AM Application Manager Application Manager AM Application Manager
Super User SUE Super User Super User SUE Super User
Daire Sakini Vekili Daire Sakini Vekili FL-REP Daire FL Daire Sakini Vekili Daire Sakini Vekili FL-REP Daire FL
URL Type Tested URL Type Tested
/building/accounts/managment/accounts : flat_representative No /building/accounts/managment/accounts : flat_representative No
/definitions/identifications/people : flat_tenant No /definitions/identifications/people : flat_tenant No
## Table Component with Edit/Create Functionality
### Features Implemented
1. **Table Actions Column**
- Added an actions column with pencil icon edit button as the first column in the table
- Implemented proper styling and visibility for the edit button
- Ensured the button appears in each row of the table
2. **Redis Cache Integration**
- Implemented data caching between list and edit/create views
- Row data is stored in Redis when edit button is clicked
- Cached data is retrieved and populated in the update form
- Boolean values (checkboxes) are properly handled with type conversion
3. **Navigation**
- Added dynamic routing based on activePageUrl
- Edit button navigates to `/panel/${activePageUrl}/update`
- Added "Create New" button that navigates to `/panel/${activePageUrl}/create`
- Added "Back to List" buttons on both create and update forms
4. **UI Improvements**
- Fixed page dropdown to always show at least page 1 when there's only one page
- Added debugging logs for troubleshooting
- Improved form field handling with proper type conversion
### Implementation Details
- Used `@tanstack/react-table` for table rendering
- Leveraged Redis for state management between pages
- Implemented proper error handling and debugging
- Used Next.js routing for navigation

View File

@ -0,0 +1,90 @@
import {
getCacheFromRedis,
setCacheToRedis,
clearCacheFromRedis,
} from "@/fetchers/custom/context/cache/fetch";
import { AuthError } from "@/fetchers/types/context";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
// Get the URL from query parameters
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return new NextResponse(
JSON.stringify({ status: 400, error: "URL parameter is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const cacheData = await getCacheFromRedis(url);
return NextResponse.json({ status: 200, data: cacheData || null });
} catch (error) {
if (error instanceof AuthError) {
return new NextResponse(
JSON.stringify({ status: 401, error: error.message }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
// Check if required parameters are present
if (!body.url || !body.data) {
return new NextResponse(
JSON.stringify({
status: 400,
error: "URL and data parameters are required",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
await setCacheToRedis(body.url, body.data);
return NextResponse.json({ status: 200, success: true });
} catch (error) {
if (error instanceof AuthError) {
return new NextResponse(
JSON.stringify({ status: 401, error: error.message }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
export async function DELETE(request: Request) {
try {
// Get the URL from query parameters
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return new NextResponse(
JSON.stringify({ status: 400, error: "URL parameter is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
await clearCacheFromRedis(url);
return NextResponse.json({ status: 200, success: true });
} catch (error) {
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}

View File

@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import { globalData, ensureDataInitialized, TableItem } from "../data";
export async function POST(request: Request) {
try {
// Parse the request body
const body = await request.json();
// Ensure we have the required fields
if (!body.name || !body.email) {
return NextResponse.json(
{ status: 400, message: "Name and email are required" },
{ status: 400 }
);
}
// Create a new item with an ID
const newItem: TableItem = {
id: `id-${Date.now()}-${globalData.length}`,
...body
};
// Add to global data
ensureDataInitialized();
globalData.unshift(newItem); // Add to beginning of array
console.log(`POST /api/test/create - Added new item with id: ${newItem.id}`);
return NextResponse.json({
status: 201,
data: newItem,
message: "Item added successfully"
}, { status: 201 });
} catch (error) {
console.error('Error in test/create POST API:', error);
return NextResponse.json(
{ status: 500, message: "Error adding data" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,45 @@
// Define the data type
export type TableItem = {
id?: string;
name: string;
email: string;
description: string;
category: string;
priority: string;
notifications: boolean;
terms: boolean;
attachments: string;
};
// Global data store
export let globalData: TableItem[] = [];
// Initialize with mock data if empty
export function ensureDataInitialized() {
if (globalData.length === 0) {
globalData = generateMockData(20);
}
return globalData;
}
// Generate mock data for testing
export function generateMockData(count = 20) {
const categories = ["general", "billing", "technical", "other"];
const priorities = ["low", "medium", "high"];
const attachmentTypes = ["document", "image", "video", ""];
return Array.from({ length: count }, (_, i) => ({
id: `id-${Date.now()}-${i}`,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
description: `This is a sample description for item ${
i + 1
}. It contains enough text to demonstrate how descriptions look in the table.`,
category: categories[Math.floor(Math.random() * categories.length)],
priority: priorities[Math.floor(Math.random() * priorities.length)],
notifications: Math.random() > 0.5,
terms: Math.random() > 0.3, // Most users accept terms
attachments:
attachmentTypes[Math.floor(Math.random() * attachmentTypes.length)],
}));
}

View File

@ -0,0 +1,109 @@
import { NextResponse } from "next/server";
import { ensureDataInitialized } from "../data";
// Helper function to handle pagination logic
function getPaginatedData(page: number, size: number) {
// Get data from global store
const allData = ensureDataInitialized();
// Calculate pagination
const startIndex = (page - 1) * size;
const endIndex = startIndex + size;
const paginatedData = allData.slice(startIndex, endIndex);
return {
paginatedData,
allData,
meta: {
page,
size,
totalCount: allData.length,
totalPages: Math.ceil(allData.length / size),
pageCount: paginatedData.length,
},
};
}
// GET endpoint for listing data with pagination
export async function GET(request: Request) {
try {
// Get query parameters for pagination
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const size = parseInt(url.searchParams.get("size") || "10");
const { paginatedData, meta } = getPaginatedData(page, size);
console.log(
`GET /api/test/list - Returning ${paginatedData.length} items (page ${page}/${meta.totalPages})`
);
// Return paginated response with the structure that useTableData expects
return NextResponse.json({
success: true,
data: {
data: paginatedData,
pagination: {
size: meta.size,
page: meta.page,
allCount: meta.totalCount,
totalCount: meta.totalCount,
totalPages: meta.totalPages,
pageCount: meta.pageCount,
orderField: [],
orderType: [],
next: meta.page < meta.totalPages,
back: meta.page > 1,
},
},
});
} catch (error) {
console.error("Error in test/list GET API:", error);
return NextResponse.json(
{ success: false, message: "Error fetching data" },
{ status: 500 }
);
}
}
// POST endpoint for listing data with pagination (for useTableData compatibility)
export async function POST(request: Request) {
try {
// Parse the request body for pagination parameters
const body = await request.json();
const page = parseInt(body.page?.toString() || "1");
const size = parseInt(body.size?.toString() || "10");
const { paginatedData, meta } = getPaginatedData(page, size);
console.log(
`POST /api/test/list - Returning ${paginatedData.length} items (page ${page}/${meta.totalPages})`
);
// Return paginated response with the structure that useTableData expects
return NextResponse.json({
success: true,
data: {
data: paginatedData,
pagination: {
size: meta.size,
page: meta.page,
allCount: meta.totalCount,
totalCount: meta.totalCount,
totalPages: meta.totalPages,
pageCount: meta.pageCount,
orderField: [],
orderType: [],
next: meta.page < meta.totalPages,
back: meta.page > 1,
},
},
});
} catch (error) {
console.error("Error in test/list POST API:", error);
return NextResponse.json(
{ success: false, message: "Error fetching data" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,50 @@
import { NextResponse } from "next/server";
import { globalData, ensureDataInitialized } from "../data";
export async function PATCH(request: Request) {
try {
// Parse the request body
const body = await request.json();
// Ensure we have an ID to update
if (!body.id) {
return NextResponse.json(
{ status: 400, message: "ID is required for updates" },
{ status: 400 }
);
}
// Find the item to update
ensureDataInitialized();
const itemIndex = globalData.findIndex(item => item.id === body.id);
if (itemIndex === -1) {
return NextResponse.json(
{ status: 404, message: "Item not found" },
{ status: 404 }
);
}
// Update the item
const updatedItem = {
...globalData[itemIndex],
...body
};
globalData[itemIndex] = updatedItem;
console.log(`PATCH /api/test/update - Updated item with id: ${updatedItem.id}`);
return NextResponse.json({
status: 200,
data: updatedItem,
message: "Item updated successfully"
});
} catch (error) {
console.error('Error in test/update PATCH API:', error);
return NextResponse.json(
{ status: 500, message: "Error updating data" },
{ status: 500 }
);
}
}

View File

@ -15,6 +15,12 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
updateOnline, updateOnline,
refreshUser, refreshUser,
updateUser, updateUser,
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache
}) => { }) => {
const pageComponents = pageIndexMulti[activePageUrl]; const pageComponents = pageIndexMulti[activePageUrl];
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> } if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
@ -25,6 +31,7 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
activePageUrl={activePageUrl} searchParams={searchParams} activePageUrl={activePageUrl} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser} userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache}
/>; />;
} }

View File

@ -33,6 +33,9 @@ import {
TableDataItem, TableDataItem,
} from "@/validations/mutual/table/validations"; } from "@/validations/mutual/table/validations";
import LoadingContent from "@/components/mutual/loader/component"; import LoadingContent from "@/components/mutual/loader/component";
import { Pencil } from "lucide-react";
import { useRouter } from "next/navigation";
import { setCacheData } from "@/components/mutual/context/cache/context";
interface DataTableProps { interface DataTableProps {
table: ReturnType<typeof useReactTable>; table: ReturnType<typeof useReactTable>;
@ -42,6 +45,7 @@ interface DataTableProps {
getSortingIcon: (columnId: string) => React.ReactNode; getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender; flexRender: typeof flexRender;
t: Translations; t: Translations;
handleEditRow?: (row: TableDataItem) => void;
} }
interface TableFormProps { interface TableFormProps {
@ -105,7 +109,8 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
handleSortingChange, handleSortingChange,
getSortingIcon, getSortingIcon,
flexRender, flexRender,
t t,
handleEditRow
}) => { }) => {
return ( return (
<div className="overflow-x-auto relative"> <div className="overflow-x-auto relative">
@ -120,20 +125,23 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
<thead className="bg-gray-50"> <thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => {
<th console.log('Rendering header:', header.id);
key={header.id} return (
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" <th
onClick={() => handleSortingChange(header.column.id)} key={header.id}
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined} className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
scope="col" onClick={() => handleSortingChange(header.column.id)}
> aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
<div className="flex items-center space-x-1"> scope="col"
{flexRender(header.column.columnDef.header, header.getContext())} >
<span>{getSortingIcon(header.column.id)}</span> <div className="flex items-center space-x-1">
</div> {flexRender(header.column.columnDef.header, header.getContext())}
</th> <span>{getSortingIcon(header.column.id)}</span>
))} </div>
</th>
);
})}
</tr> </tr>
))} ))}
</thead> </thead>
@ -147,11 +155,14 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
) : ( ) : (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50"> <tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => {
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> console.log('Rendering cell:', cell.column.id);
{flexRender(cell.column.columnDef.cell, cell.getContext())} return (
</td> <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
))} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr> </tr>
)) ))
)} )}
@ -196,11 +207,17 @@ const TableForm: React.FC<TableFormProps> = ({
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{renderPageOptions().map((option: { key: string | number; value: string; label: string }) => ( {renderPageOptions().length > 0 ? (
<SelectItem key={option.key} value={option.value}> renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
{option.label} <SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
))
) : (
<SelectItem key="1" value="1">
1
</SelectItem> </SelectItem>
))} )}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
@ -328,6 +345,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
// Initialize translation with English as default // Initialize translation with English as default
const language = props.onlineData?.lang as LanguageTypes || 'en'; const language = props.onlineData?.lang as LanguageTypes || 'en';
const t = translations[language]; const t = translations[language];
const router = useRouter();
const { const {
form, form,
@ -351,30 +369,90 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
isNextDisabled, isNextDisabled,
pageSizeOptions, pageSizeOptions,
renderPageOptions, renderPageOptions,
} = useTableData({ apiUrl: `${API_BASE_URL}/test` }); } = useTableData({ apiUrl: `${API_BASE_URL}/test/list` });
const activePageUrl = props.activePageUrl || '';
console.log("activePageUrl", activePageUrl);
// Function to handle editing a row
const handleEditRow = async (row: TableDataItem) => {
try {
// Store the row data in the cache
await setCacheData(`${activePageUrl}/update`, row);
console.log('Row data stored in cache:', row);
// Navigate to the update form
router.push(`/panel/${activePageUrl}/update`);
} catch (error) {
console.error('Error storing row data in cache:', error);
}
};
const columnHelper = createColumnHelper<TableDataItem>(); const columnHelper = createColumnHelper<TableDataItem>();
// Make sure columns are properly defined with the actions column
const columns = React.useMemo(() => [ const columns = React.useMemo(() => [
columnHelper.accessor('uu_id', { columnHelper.display({
cell: info => info.getValue(), id: 'actions',
header: () => <span>UUID</span>, header: () => <span>Actions</span>,
footer: info => info.column.id cell: (info) => {
console.log('Rendering action cell for row:', info.row.id);
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
console.log('Edit button clicked');
handleEditRow && handleEditRow(info.row.original);
}}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
);
}
}), }),
columnHelper.accessor('process_name', { columnHelper.accessor('name', {
cell: info => info.getValue(), cell: info => info.getValue(),
header: () => <span>Name</span>, header: () => <span>Name</span>,
footer: info => info.column.id footer: info => info.column.id
}), }),
columnHelper.accessor('bank_date', { columnHelper.accessor('email', {
header: () => <span>Bank Date</span>, cell: info => info.getValue(),
header: () => <span>Email</span>,
footer: info => info.column.id
}),
columnHelper.accessor('description', {
header: () => <span>Description</span>,
cell: info => info.getValue(), cell: info => info.getValue(),
footer: info => info.column.id footer: info => info.column.id
}), }),
columnHelper.accessor('currency_value', { columnHelper.accessor('category', {
header: () => <span>Currency Value</span>, header: () => <span>Category</span>,
cell: info => String(info.getValue()), cell: info => String(info.getValue()),
footer: info => info.column.id footer: info => info.column.id
}), }),
columnHelper.accessor('priority', {
header: () => <span>Priority</span>,
cell: info => String(info.getValue()),
footer: info => info.column.id
}),
columnHelper.accessor('notifications', {
header: () => <span>Notifications</span>,
cell: info => info.getValue() ? 'Yes' : 'No',
footer: info => info.column.id
}),
columnHelper.accessor('terms', {
header: () => <span>Terms Accepted</span>,
cell: info => info.getValue() ? 'Yes' : 'No',
footer: info => info.column.id
}),
columnHelper.accessor('attachments', {
header: () => <span>Attachments</span>,
cell: info => String(info.getValue() || '-'),
footer: info => info.column.id
}),
], [columnHelper]) as ColumnDef<TableDataItem>[]; ], [columnHelper]) as ColumnDef<TableDataItem>[];
const table = useReactTable({ const table = useReactTable({
@ -387,8 +465,12 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
manualPagination: true, manualPagination: true,
pageCount: apiPagination.totalPages || 1, pageCount: apiPagination.totalPages || 1,
debugTable: true,
}); });
// Check if actions column is included
const actionColumnExists = table.getVisibleLeafColumns().some(col => col.id === 'actions');
return ( return (
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> : isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20"> <div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
@ -401,6 +483,13 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
error={error} error={error}
t={t} t={t}
/> />
<Button
onClick={() => router.push(`/panel/${activePageUrl}/create`)}
className="bg-primary text-white hover:bg-primary/90"
>
<span className="mr-2">Create New</span>
<span className="sr-only">Create new item</span>
</Button>
</div> </div>
<TableForm <TableForm
@ -435,6 +524,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
getSortingIcon={getSortingIcon} getSortingIcon={getSortingIcon}
flexRender={flexRender} flexRender={flexRender}
t={t} t={t}
handleEditRow={handleEditRow}
/> />
</div> </div>

View File

@ -40,6 +40,7 @@ const FallbackContent: FC<{ lang: LanguageTypes; activePageUrl: string }> = memo
const ContentComponent: FC<ContentProps> = ({ const ContentComponent: FC<ContentProps> = ({
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser, searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
onlineData, onlineLoading, onlineError, refreshOnline, updateOnline, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
cacheData, cacheLoading, cacheError, refreshCache, updateCache, clearCache
}) => { }) => {
const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />; const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />;
const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]"; const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]";
@ -62,7 +63,8 @@ const ContentComponent: FC<ContentProps> = ({
<MemoizedMultiPage <MemoizedMultiPage
activePageUrl={activePageUrl || ''} searchParams={searchParams} activePageUrl={activePageUrl || ''} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser} userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache} />
</div> </div>
</Suspense> </Suspense>
</div> </div>

View File

@ -0,0 +1,403 @@
import React, { useState, useEffect } from "react";
import { apiPostFetcher } from "@/lib/fetcher";
import { withCache } from "@/components/mutual/context/cache/withCache";
import { Input } from "@/components/mutual/ui/input";
import { Label } from "@/components/mutual/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { Textarea } from "@/components/mutual/ui/textarea";
import { Checkbox } from "@/components/mutual/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/mutual/ui/radio-group";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/mutual/ui/dropdown-menu";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { getCacheData, setCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { useRouter } from "next/navigation";
// Define form schema with zod
const zodSchema = z.object({
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
email: z.string().email({ message: "Please enter a valid email address." }),
description: z.string().min(10, { message: "Description must be at least 10 characters." }),
category: z.string({ required_error: "Please select a category." }),
priority: z.string({ required_error: "Please select a priority level." }),
notifications: z.boolean(),
terms: z.boolean().refine(val => val === true, { message: "You must accept the terms." }),
attachments: z.string().optional()
});
// Define the form type
type FormValues = z.infer<typeof zodSchema>;
function CreateFromComponentBase({
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache,
activePageUrl
}: {
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
activePageUrl?: string;
}) {
const [open, setOpen] = useState<boolean>(false);
const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/create", "")}`;
const form = useForm<FormValues>({
resolver: zodResolver(zodSchema),
defaultValues: {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
}
});
// Load cached form values only once when component mounts
useEffect(() => {
const fetchCacheDirectly = async () => {
if (activePageUrl && !cacheLoaded) {
try {
console.log("Directly fetching cache for URL:", activePageUrl);
// Directly fetch cache data using the imported function
const cachedData = await getCacheData(activePageUrl);
console.log("Directly fetched cache data:", cachedData);
if (cachedData) {
const formValues = form.getValues();
// Create a merged data object with proper typing
const mergedData: FormValues = {
name: cachedData.name || formValues.name || "",
email: cachedData.email || formValues.email || "",
description: cachedData.description || formValues.description || "",
category: cachedData.category || formValues.category || "",
priority: cachedData.priority || formValues.priority || "",
notifications: cachedData.notifications ?? formValues.notifications ?? false,
terms: cachedData.terms ?? formValues.terms ?? false,
attachments: cachedData.attachments || formValues.attachments || ""
};
console.log("Setting form with direct cache data:", mergedData);
form.reset(mergedData);
}
setCacheLoaded(true);
// Also call the context refresh if available (for state consistency)
if (refreshCache) {
refreshCache(activePageUrl);
}
} catch (error) {
console.error("Error fetching cache directly:", error);
}
}
};
fetchCacheDirectly();
}, []); // Empty dependency array since we only want to run once on mount
// Function to handle input blur events
const handleFieldBlur = async (fieldName: keyof FormValues, value: any) => {
// Only update if the value is not empty
if (value && activePageUrl) {
try {
// Get current form values
const currentValues = form.getValues();
// Prepare updated data
const updatedData = {
...currentValues,
[fieldName]: value
};
console.log("Directly updating cache with:", updatedData);
// Directly update cache using imported function
await setCacheData(activePageUrl, updatedData);
// Also use the context method if available (for state consistency)
if (updateCache) {
updateCache(activePageUrl, updatedData);
}
} catch (error) {
console.error("Error updating cache:", error);
}
}
};
// Type-safe submit handler
const onSubmit = async (data: FormValues) => {
console.log("Form submitted with data:", data);
try {
// Submit form data to API
const response = await apiPostFetcher<any>({ url: "/tst/create", isNoCache: true, body: data });
console.log("API response:", response);
// Clear cache on successful submission
if (activePageUrl) {
try {
console.log("Directly clearing cache for URL:", activePageUrl);
// Directly clear cache using imported function
await clearCacheData(activePageUrl);
// Also use the context method if available (for state consistency)
if (clearCache) {
clearCache(activePageUrl);
}
} catch (error) {
console.error("Error clearing cache:", error);
}
}
// Reset form with empty values
const emptyValues: FormValues = {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
};
form.reset(emptyValues);
} catch (error) {
console.error("Error submitting form:", error);
}
}
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* back to list button */}
<div className="flex justify-end">
<Button onClick={() => router.push(listUrl)}>Back to List</Button>
</div>
<h2 className="text-2xl font-bold mb-6">Example Form</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Input example */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Enter your name"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("name", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email input example */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your@email.com"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("email", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea example */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter a detailed description"
className="min-h-[100px]"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("description", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select example */}
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<Select
onValueChange={(value) => { field.onChange(value); handleFieldBlur("category", value) }}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* RadioGroup example */}
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Priority Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => { field.onChange(value); handleFieldBlur("priority", value) }}
value={field.value}
className="flex flex-col space-y-1"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="low" id="priority-low" />
<Label htmlFor="priority-low">Low</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="priority-medium" />
<Label htmlFor="priority-medium">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="high" id="priority-high" />
<Label htmlFor="priority-high">High</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox example */}
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md p-4 border">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => { field.onChange(checked); handleFieldBlur("notifications", checked) }}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Email notifications</FormLabel>
<p className="text-sm text-gray-500">Receive email notifications when updates occur</p>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Terms checkbox example */}
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => { field.onChange(checked); handleFieldBlur("terms", checked) }}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>I accept the terms and conditions</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* DropdownMenu example */}
<FormField
control={form.control}
name="attachments"
render={({ field }) => (
<FormItem>
<FormLabel>Attachments</FormLabel>
<FormControl>
<div>
<DropdownMenu open={open} onOpenChange={(isOpen: boolean) => setOpen(isOpen)}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<span>{field.value || "Select attachment type"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => { field.onChange("document"); handleFieldBlur("attachments", "document"); setOpen(false) }}>
Document
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("image"); handleFieldBlur("attachments", "image"); setOpen(false) }}>
Image
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("video"); handleFieldBlur("attachments", "video"); setOpen(false) }}>
Video
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Submit</Button>
</form>
</Form>
</div>
);
}
export const CreateFromComponent = withCache(CreateFromComponentBase);
// Add default export to maintain compatibility with existing imports
export default withCache(CreateFromComponentBase);

View File

@ -0,0 +1,425 @@
import React, { useState, useEffect } from "react";
import { apiPatchFetcher } from "@/lib/fetcher";
import { withCache } from "@/components/mutual/context/cache/withCache";
import { Input } from "@/components/mutual/ui/input";
import { Label } from "@/components/mutual/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { Textarea } from "@/components/mutual/ui/textarea";
import { Checkbox } from "@/components/mutual/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/mutual/ui/radio-group";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/mutual/ui/dropdown-menu";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { getCacheData, setCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { useRouter } from "next/navigation";
// Define form schema with zod
const zodSchema = z.object({
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
email: z.string().email({ message: "Please enter a valid email address." }),
description: z.string().min(10, { message: "Description must be at least 10 characters." }),
category: z.string({ required_error: "Please select a category." }),
priority: z.string({ required_error: "Please select a priority level." }),
notifications: z.boolean(),
terms: z.boolean().refine(val => val === true, { message: "You must accept the terms." }),
attachments: z.string().optional()
});
// Define the form type
type FormValues = z.infer<typeof zodSchema>;
function UpdateFromComponentBase({
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache,
activePageUrl
}: {
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
activePageUrl?: string;
}) {
const [open, setOpen] = useState<boolean>(false);
const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/update", "")}`;
const form = useForm<FormValues>({
resolver: zodResolver(zodSchema),
defaultValues: {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
}
});
// Load cached form values only once when component mounts
useEffect(() => {
const fetchCacheDirectly = async () => {
if (activePageUrl && !cacheLoaded) {
try {
console.log("Directly fetching cache for URL:", activePageUrl);
// Directly fetch cache data using the imported function
const cachedData = await getCacheData(activePageUrl);
console.log("Directly fetched cache data:", cachedData);
if (cachedData) {
const formValues = form.getValues();
// Create a merged data object with proper typing
// Convert boolean values explicitly to handle potential string representations
const notificationsValue = typeof cachedData.notifications === 'string'
? cachedData.notifications === 'true'
: Boolean(cachedData.notifications);
const termsValue = typeof cachedData.terms === 'string'
? cachedData.terms === 'true'
: Boolean(cachedData.terms);
console.log("Raw notification value from cache:", cachedData.notifications, "type:", typeof cachedData.notifications);
console.log("Raw terms value from cache:", cachedData.terms, "type:", typeof cachedData.terms);
console.log("Converted notification value:", notificationsValue);
console.log("Converted terms value:", termsValue);
const mergedData: FormValues = {
name: cachedData.name || formValues.name || "",
email: cachedData.email || formValues.email || "",
description: cachedData.description || formValues.description || "",
category: cachedData.category || formValues.category || "",
priority: cachedData.priority || formValues.priority || "",
notifications: notificationsValue,
terms: termsValue,
attachments: cachedData.attachments || formValues.attachments || ""
};
console.log("Setting form with direct cache data:", mergedData);
form.reset(mergedData);
}
setCacheLoaded(true);
// Also call the context refresh if available (for state consistency)
if (refreshCache) {
refreshCache(activePageUrl);
}
} catch (error) {
console.error("Error fetching cache directly:", error);
}
}
};
fetchCacheDirectly();
}, []); // Empty dependency array since we only want to run once on mount
// Function to handle input blur events
const handleFieldBlur = async (fieldName: keyof FormValues, value: any) => {
// Only update if the value is not empty
if (value && activePageUrl) {
try {
// Get current form values
const currentValues = form.getValues();
// Prepare updated data
const updatedData = {
...currentValues,
[fieldName]: value
};
console.log("Directly updating cache with:", updatedData);
// Directly update cache using imported function
await setCacheData(activePageUrl, updatedData);
// Also use the context method if available (for state consistency)
if (updateCache) {
updateCache(activePageUrl, updatedData);
}
} catch (error) {
console.error("Error updating cache:", error);
}
}
};
// Type-safe submit handler
const onSubmit = async (data: FormValues) => {
console.log("Form submitted with data:", data);
try {
// Submit form data to API using PATCH for updates
const response = await apiPatchFetcher<any>({ url: "/api/test/update", isNoCache: true, body: data });
console.log("API response:", response);
// Clear cache on successful submission
if (activePageUrl) {
try {
console.log("Directly clearing cache for URL:", activePageUrl);
// Directly clear cache using imported function
await clearCacheData(activePageUrl);
// Also use the context method if available (for state consistency)
if (clearCache) {
clearCache(activePageUrl);
}
} catch (error) {
console.error("Error clearing cache:", error);
}
}
// Reset form with empty values
const emptyValues: FormValues = {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
};
form.reset(emptyValues);
} catch (error) {
console.error("Error submitting form:", error);
}
}
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* back to list button */}
<div className="flex justify-end">
<Button onClick={() => router.push(listUrl)}>Back to List</Button>
</div>
<h2 className="text-2xl font-bold mb-6">Update Form</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Input example */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Enter your name"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("name", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email input example */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your@email.com"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("email", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea example */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter a detailed description"
className="min-h-[100px]"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("description", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select example */}
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<Select
onValueChange={(value) => { field.onChange(value); handleFieldBlur("category", value) }}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* RadioGroup example */}
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Priority Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => { field.onChange(value); handleFieldBlur("priority", value) }}
value={field.value}
className="flex flex-col space-y-1"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="low" id="priority-low" />
<Label htmlFor="priority-low">Low</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="priority-medium" />
<Label htmlFor="priority-medium">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="high" id="priority-high" />
<Label htmlFor="priority-high">High</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox example */}
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md p-4 border">
<FormControl>
<Checkbox
checked={Boolean(field.value)}
onCheckedChange={(checked) => {
console.log("Notification checkbox changed to:", checked);
field.onChange(checked);
handleFieldBlur("notifications", checked);
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Email notifications</FormLabel>
<p className="text-sm text-gray-500">Receive email notifications when updates occur</p>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Terms checkbox example */}
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={Boolean(field.value)}
onCheckedChange={(checked) => {
console.log("Terms checkbox changed to:", checked);
field.onChange(checked);
handleFieldBlur("terms", checked);
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>I accept the terms and conditions</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* DropdownMenu example */}
<FormField
control={form.control}
name="attachments"
render={({ field }) => (
<FormItem>
<FormLabel>Attachments</FormLabel>
<FormControl>
<div>
<DropdownMenu open={open} onOpenChange={(isOpen: boolean) => setOpen(isOpen)}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<span>{field.value || "Select attachment type"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => { field.onChange("document"); handleFieldBlur("attachments", "document"); setOpen(false) }}>
Document
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("image"); handleFieldBlur("attachments", "image"); setOpen(false) }}>
Image
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("video"); handleFieldBlur("attachments", "video"); setOpen(false) }}>
Video
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Update</Button>
</form>
</Form>
</div>
);
}
export const UpdateFromComponent = withCache(UpdateFromComponentBase);
// Add default export to maintain compatibility with existing imports
export default withCache(UpdateFromComponentBase);

View File

@ -0,0 +1,211 @@
"use client";
import React, { ReactNode } from 'react';
import { createContext, useContext } from 'react';
import { createContextHook } from "../hookFactory";
// Define the shape of the cache data
type CacheData = {
[url: string]: any;
};
// Original fetch functions for backward compatibility
async function getCacheData(url: string): Promise<any | null> {
try {
const response = await fetch(`/api/context/cache?url=${encodeURIComponent(url)}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (data.status === 200) return data.data;
return null;
} catch (error) {
console.error("Error fetching cache data:", error);
return null;
}
}
async function setCacheData(url: string, data: any): Promise<boolean> {
try {
const response = await fetch(`/api/context/cache`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, data }),
});
if (!response.ok) {
return false;
}
const result = await response.json();
return result.status === 200;
} catch (error) {
console.error("Error setting cache data:", error);
return false;
}
}
async function clearCacheData(url: string): Promise<boolean> {
try {
const response = await fetch(`/api/context/cache?url=${encodeURIComponent(url)}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
return false;
}
const result = await response.json();
return result.status === 200;
} catch (error) {
console.error("Error clearing cache data:", error);
return false;
}
}
// Create the cache hook using the factory
const useContextCache = createContextHook<CacheData>({
endpoint: "/context/cache",
contextName: "cache",
enablePeriodicRefresh: false,
customFetch: async () => {
// For initial load, we don't have a specific URL to fetch
// Return an empty object as the initial state
console.log("Initial cache fetch");
return {};
},
customUpdate: async (newData: CacheData) => {
// This won't be used directly, as we'll provide custom methods
console.log("Cache update with:", newData);
return true;
},
defaultValue: {}
});
// Custom hook for cache data with the expected interface
interface UseCacheResult {
cacheData: CacheData | null;
isLoading: boolean;
error: string | null;
refreshCache: (url?: string) => Promise<void>;
updateCache: (url: string, data: any) => Promise<void>;
clearCache: (url: string) => Promise<void>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useCache(): UseCacheResult {
const { data, isLoading, error, refresh, update } = useContextCache();
// Custom refresh function that fetches data for a specific URL
const refreshCache = async (url?: string): Promise<void> => {
if (!url && typeof window !== 'undefined') {
url = window.location.pathname;
}
if (!url) return;
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Fetching cache for normalized URL:", normalizedUrl);
try {
const urlData = await getCacheData(normalizedUrl);
console.log("Received cache data:", urlData);
// Update the local state with the fetched data
const updatedData = {
...data,
[normalizedUrl]: urlData
};
console.log("Updating cache state with:", updatedData);
await update(updatedData);
// Force a refresh to ensure the component gets the updated data
await refresh();
} catch (error) {
console.error("Error refreshing cache:", error);
}
};
// Custom update function for a specific URL
const updateCache = async (url: string, urlData: any): Promise<void> => {
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Updating cache for normalized URL:", normalizedUrl);
try {
const success = await setCacheData(normalizedUrl, urlData);
console.log("Cache update success:", success);
if (success && data) {
// Update local state
await update({
...data,
[normalizedUrl]: urlData
});
}
return;
} catch (error) {
console.error("Error updating cache:", error);
return;
}
};
// Custom clear function for a specific URL
const clearCache = async (url: string): Promise<void> => {
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Clearing cache for normalized URL:", normalizedUrl);
try {
const success = await clearCacheData(normalizedUrl);
console.log("Cache clear success:", success);
if (success && data) {
// Update local state
const newData = { ...data };
delete newData[normalizedUrl];
await update(newData);
}
return;
} catch (error) {
console.error("Error clearing cache:", error);
return;
}
};
return {
cacheData: data,
isLoading,
error,
refreshCache,
updateCache,
clearCache
};
}
// Create a provider component that uses the hook factory
// Create a context for the cache
const CacheContext = createContext<UseCacheResult | undefined>(undefined);
// Provider component
export function CacheProvider({ children }: { children: ReactNode }) {
const cacheHook = useCache();
return (
<CacheContext.Provider value={cacheHook}>
{children}
</CacheContext.Provider>
);
}
// Export the original functions for backward compatibility
export { getCacheData, setCacheData, clearCacheData };

View File

@ -0,0 +1,21 @@
"use client";
import React from 'react';
import { useCache } from './context';
// Higher-order component to provide cache functionality
export function withCache<P extends object>(Component: React.ComponentType<P & { activePageUrl?: string }>) {
return function WithCacheComponent(props: P & { activePageUrl?: string }) {
try {
// Try to use the cache context
const cacheProps = useCache();
// Pass both the original props and the cache props to the wrapped component
return <Component {...props} {...cacheProps} />;
} catch (error) {
// If the cache context is not available, render the component with just its original props
console.warn("Cache context not available, rendering component without cache functionality");
return <Component {...props} />;
}
};
}

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { OnlineProvider } from '@/components/mutual/context/online/provider'; import { OnlineProvider } from '@/components/mutual/context/online/provider';
import { CacheProvider } from '@/components/mutual/context/cache/context';
interface ClientProvidersProps { interface ClientProvidersProps {
children: ReactNode; children: ReactNode;
@ -14,7 +15,9 @@ export function ClientProviders({ children }: ClientProvidersProps) {
return ( return (
<OnlineProvider> <OnlineProvider>
{children} <CacheProvider>
{children}
</CacheProvider>
</OnlineProvider> </OnlineProvider>
); );
} }

View File

@ -0,0 +1,123 @@
"use server";
import { functionRetrieveUserSelection } from "@/fetchers/fecther";
import { AuthError } from "@/fetchers/types/context";
import { getCompleteFromRedis, setCompleteToRedis } from "@/fetchers/custom/context/complete/fetch";
import { safeRedisGet, safeJsonParse } from "@/utils/redisOperations";
import { REDIS_TIMEOUT } from "@/fetchers/base";
// Import or define the ClientRedisToken type
import type { ClientRedisToken } from "@/fetchers/types/context";
// Define the cache structure
type CacheData = {
[url: string]: {
[key: string]: any;
};
};
// Extend the ClientRedisToken type to include cache
type ExtendedClientRedisToken = ClientRedisToken & {
cache: CacheData;
};
/**
* Get cached form values from Redis for a specific URL
* @param url The URL key to retrieve cached form values for
* @returns The cached form values or null if not found
*/
const getCacheFromRedis = async (url: string): Promise<any> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Use safe Redis get operation with proper connection handling
const result = await safeRedisGet(`${redisKey}`, REDIS_TIMEOUT);
if (!result) return null;
// Use safe JSON parsing with proper default object type
const parsedResult = safeJsonParse(result, { cache: {} }) as { cache: CacheData };
// Return the cached data for the specific URL or null if not found
return parsedResult.cache && parsedResult.cache[url] ? parsedResult.cache[url] : null;
} catch (error) {
console.error("Error getting cache from Redis:", error);
return null;
}
};
/**
* Set cached form values in Redis for a specific URL
* @param url The URL key to store the form values under
* @param formValues The form values to cache
* @returns True if successful, false otherwise
*/
const setCacheToRedis = async (url: string, formValues: any): Promise<boolean> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Get the complete data from Redis
const completeData = await getCompleteFromRedis() as ExtendedClientRedisToken;
if (!completeData) throw new AuthError("No complete data found in Redis");
// Initialize cache object if it doesn't exist
if (!completeData.cache) {
completeData.cache = {} as CacheData;
}
// Update the cache with the new form values for the specific URL
completeData.cache[url] = formValues;
// Save the updated data back to Redis
await setCompleteToRedis(completeData);
return true;
} catch (error) {
console.error("Error setting cache to Redis:", error);
if (error instanceof AuthError) {
throw error;
} else {
throw new AuthError(error instanceof Error ? error.message : "Unknown error");
}
}
};
/**
* Clear cached form values in Redis for a specific URL
* @param url The URL key to clear cached form values for
* @returns True if successful, false otherwise
*/
const clearCacheFromRedis = async (url: string): Promise<boolean> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Get the complete data from Redis
const completeData = await getCompleteFromRedis() as ExtendedClientRedisToken;
if (!completeData) throw new AuthError("No complete data found in Redis");
// If cache exists and has the URL, delete it
if (completeData.cache && completeData.cache[url]) {
delete completeData.cache[url];
// Save the updated data back to Redis
await setCompleteToRedis(completeData as any);
}
return true;
} catch (error) {
console.error("Error clearing cache from Redis:", error);
return false;
}
};
export { getCacheFromRedis, setCacheToRedis, clearCacheFromRedis };

View File

@ -6,6 +6,7 @@ import { useOnline } from "@/components/mutual/context/online/context";
import { useSelection } from "@/components/mutual/context/selection/context"; import { useSelection } from "@/components/mutual/context/selection/context";
import { useUser } from "@/components/mutual/context/user/context"; import { useUser } from "@/components/mutual/context/user/context";
import { useConfig } from "@/components/mutual/context/config/context"; import { useConfig } from "@/components/mutual/context/config/context";
import { useCache } from "@/components/mutual/context/cache/context";
import { ModeTypes } from "@/validations/mutual/dashboard/props"; import { ModeTypes } from "@/validations/mutual/dashboard/props";
import HeaderComponent from "@/components/custom/header/component"; import HeaderComponent from "@/components/custom/header/component";
@ -22,6 +23,7 @@ const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) =>
const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu(); const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu();
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection(); const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig(); const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
const { cacheData, isLoading: cacheLoading, error: cacheError, refreshCache, updateCache, clearCache } = useCache();
const prefix = "/panel" const prefix = "/panel"
const mode = (searchParams?.mode as ModeTypes) || 'shortList'; const mode = (searchParams?.mode as ModeTypes) || 'shortList';
@ -38,7 +40,8 @@ const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) =>
menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} /> menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} />
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams} <ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser} userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache} />
{/* <FooterComponent activePageUrl={activePageUrl} searchParams={searchParams} {/* <FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig} configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */} onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */}

View File

@ -1,12 +1,14 @@
// import { DashboardPage } from "@/components/custom/content/DashboardPage"; // import { DashboardPage } from "@/components/custom/content/DashboardPage";
import { DPage } from "@/components/custom/content/DPage"; import { DPage } from "@/components/custom/content/DPage";
import TableCardComponentImproved from "@/components/custom/content/TableCardComponentImproved"; import TableCardComponentImproved from "@/components/custom/content/TableCardComponentImproved";
import CreateFromComponent from "@/components/custom/content/createFromComponent";
import UpdateFromComponent from "@/components/custom/content/updateFromComponent";
const pageIndexMulti: Record<string, Record<string, React.FC<any>>> = { const pageIndexMulti: Record<string, Record<string, React.FC<any>>> = {
"/dashboard": { DashboardPage: TableCardComponentImproved }, "/dashboard": { DashboardPage: TableCardComponentImproved },
"/build": { DashboardPage: DPage }, "/build": { DashboardPage: TableCardComponentImproved },
"/build/create": { DashboardPage: DPage }, "/build/create": { DashboardPage: CreateFromComponent },
"/build/update": { DashboardPage: DPage }, "/build/update": { DashboardPage: UpdateFromComponent },
"/people": { DashboardPage: DPage }, "/people": { DashboardPage: DPage },
"/people/create": { DashboardPage: DPage }, "/people/create": { DashboardPage: DPage },
"/people/update": { DashboardPage: DPage }, "/people/update": { DashboardPage: DPage },

View File

@ -29,6 +29,12 @@ interface ContentProps {
userError: any; userError: any;
refreshUser: () => Promise<void>; refreshUser: () => Promise<void>;
updateUser: (newUser: any) => Promise<boolean>; updateUser: (newUser: any) => Promise<boolean>;
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
} }
interface MenuProps { interface MenuProps {

View File

@ -56,10 +56,14 @@ interface DashboardPageProps {
} }
interface TableDataItem { interface TableDataItem {
uu_id: string; name: string;
process_name: string; email: string;
bank_date: string; description: string;
currency_value: number | string; category: string;
priority: string;
notifications: boolean;
terms: boolean;
attachments: string;
[key: string]: any; // For any additional fields [key: string]: any; // For any additional fields
} }