Add OpenMemory (#2676)

Co-authored-by: Saket Aryan <94069182+whysosaket@users.noreply.github.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
Deshraj Yadav
2025-05-13 08:30:59 -07:00
committed by GitHub
parent 8d61d73d2f
commit f51b39db91
172 changed files with 17846 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { PauseIcon, Loader2, PlayIcon } from "lucide-react";
import { useAppsApi } from "@/hooks/useAppsApi";
import Image from "next/image";
import { useDispatch, useSelector } from "react-redux";
import { setAppDetails } from "@/store/appsSlice";
import { BiEdit } from "react-icons/bi";
import { constants } from "@/components/shared/source-app";
import { RootState } from "@/store/store";
const capitalize = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
const AppDetailCard = ({
appId,
selectedApp,
}: {
appId: string;
selectedApp: any;
}) => {
const { updateAppDetails } = useAppsApi();
const [isLoading, setIsLoading] = useState(false);
const dispatch = useDispatch();
const apps = useSelector((state: RootState) => state.apps.apps);
const currentApp = apps.find((app: any) => app.id === appId);
const appConfig = currentApp
? constants[currentApp.name as keyof typeof constants] || constants.default
: constants.default;
const handlePauseAccess = async () => {
setIsLoading(true);
try {
await updateAppDetails(appId, {
is_active: !selectedApp.details.is_active,
});
dispatch(
setAppDetails({ appId, isActive: !selectedApp.details.is_active })
);
} catch (error) {
console.error("Failed to toggle app pause state:", error);
} finally {
setIsLoading(false);
}
};
const buttonText = selectedApp.details.is_active
? "Pause Access"
: "Unpause Access";
return (
<div>
<div className="bg-zinc-900 border w-[320px] border-zinc-800 rounded-xl mb-6">
<div className="flex items-center gap-2 mb-4 bg-zinc-800 rounded-t-xl p-3">
<div className="w-5 h-5 flex items-center justify-center">
{appConfig.iconImage ? (
<div>
<div className="w-6 h-6 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
<Image
src={appConfig.iconImage}
alt={appConfig.name}
width={40}
height={40}
/>
</div>
</div>
) : (
<div className="w-5 h-5 flex items-center justify-center bg-zinc-700 rounded-full">
<BiEdit className="w-4 h-4 text-zinc-400" />
</div>
)}
</div>
<h2 className="text-md font-semibold">{appConfig.name}</h2>
</div>
<div className="space-y-4 p-3">
<div>
<p className="text-xs text-zinc-400">Access Status</p>
<p
className={`font-medium ${
selectedApp.details.is_active
? "text-emerald-500"
: "text-red-500"
}`}
>
{capitalize(
selectedApp.details.is_active ? "active" : "inactive"
)}
</p>
</div>
<div>
<p className="text-xs text-zinc-400">Total Memories Created</p>
<p className="font-medium">
{selectedApp.details.total_memories_created} Memories
</p>
</div>
<div>
<p className="text-xs text-zinc-400">Total Memories Accessed</p>
<p className="font-medium">
{selectedApp.details.total_memories_accessed} Memories
</p>
</div>
<div>
<p className="text-xs text-zinc-400">First Accessed</p>
<p className="font-medium">
{selectedApp.details.first_accessed
? new Date(
selectedApp.details.first_accessed
).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "numeric",
})
: "Never"}
</p>
</div>
<div>
<p className="text-xs text-zinc-400">Last Accessed</p>
<p className="font-medium">
{selectedApp.details.last_accessed
? new Date(
selectedApp.details.last_accessed
).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "numeric",
})
: "Never"}
</p>
</div>
<hr className="border-zinc-800" />
<div className="flex gap-2 justify-end">
<Button
onClick={handlePauseAccess}
className="flex bg-transparent w-[170px] bg-zinc-800 border-zinc-800 hover:bg-zinc-800 text-white"
size="sm"
disabled={isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : buttonText === "Pause Access" ? (
<PauseIcon className="h-4 w-4" />
) : (
<PlayIcon className="h-4 w-4" />
)}
{buttonText}
</Button>
</div>
</div>
</div>
</div>
);
};
export default AppDetailCard;

View File

@@ -0,0 +1,115 @@
import { ArrowRight } from "lucide-react";
import Categories from "@/components/shared/categories";
import Link from "next/link";
import { constants } from "@/components/shared/source-app";
import Image from "next/image";
interface MemoryCardProps {
id: string;
content: string;
created_at: string;
metadata?: Record<string, any>;
categories?: string[];
access_count?: number;
app_name: string;
state: string;
}
export function MemoryCard({
id,
content,
created_at,
metadata,
categories,
access_count,
app_name,
state,
}: MemoryCardProps) {
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900 overflow-hidden">
<div className="p-4">
<div className="border-l-2 border-primary pl-4 mb-4">
<p
className={`${state !== "active" ? "text-zinc-400" : "text-white"}`}
>
{content}
</p>
</div>
{metadata && Object.keys(metadata).length > 0 && (
<div className="mb-4">
<p className="text-xs text-zinc-500 uppercase mb-2">METADATA</p>
<div className="bg-zinc-800 rounded p-3 text-zinc-400">
<pre className="whitespace-pre-wrap">
{JSON.stringify(metadata, null, 2)}
</pre>
</div>
</div>
)}
<div className="mb-2">
<Categories
categories={categories as any}
isPaused={state !== "active"}
/>
</div>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-zinc-400 text-sm">
{access_count ? (
<span className="relative top-1">
Accessed {access_count} times
</span>
) : (
new Date(created_at + "Z").toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
})
)}
</span>
{state !== "active" && (
<span className="inline-block px-3 border border-yellow-600 text-yellow-600 font-semibold text-xs rounded-full bg-yellow-400/10 backdrop-blur-sm">
{state === "paused" ? "Paused" : "Archived"}
</span>
)}
</div>
{!app_name && (
<Link
href={`/memory/${id}`}
className="hover:cursor-pointer bg-zinc-800 hover:bg-zinc-700 flex items-center px-3 py-1 text-sm rounded-lg text-white p-0 hover:text-white"
>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
)}
{app_name && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-zinc-700 px-3 py-1 rounded-lg">
<span className="text-sm text-zinc-400">Created by:</span>
<div className="w-5 h-5 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
<Image
src={
constants[app_name as keyof typeof constants]
?.iconImage || ""
}
alt="OpenMemory"
width={24}
height={24}
/>
</div>
<p className="text-sm text-zinc-100 font-semibold">
{constants[app_name as keyof typeof constants]?.name}
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,219 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useAppsApi } from "@/hooks/useAppsApi";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MemoryCard } from "./components/MemoryCard";
import AppDetailCard from "./components/AppDetailCard";
import "@/styles/animation.css";
import NotFound from "@/app/not-found";
import { AppDetailCardSkeleton } from "@/skeleton/AppDetailCardSkeleton";
import { MemoryCardSkeleton } from "@/skeleton/MemoryCardSkeleton";
export default function AppDetailsPage() {
const params = useParams();
const appId = params.appId as string;
const [activeTab, setActiveTab] = useState("created");
const {
fetchAppDetails,
fetchAppMemories,
fetchAppAccessedMemories,
fetchApps,
} = useAppsApi();
const selectedApp = useSelector((state: RootState) => state.apps.selectedApp);
useEffect(() => {
fetchApps({});
}, [fetchApps]);
useEffect(() => {
const loadData = async () => {
if (appId) {
try {
// Load all data in parallel
await Promise.all([
fetchAppDetails(appId),
fetchAppMemories(appId),
fetchAppAccessedMemories(appId),
]);
} catch (error) {
console.error("Error loading app data:", error);
}
}
};
loadData();
}, [appId, fetchAppDetails, fetchAppMemories, fetchAppAccessedMemories]);
if (selectedApp.error) {
return (
<NotFound message={selectedApp.error} title="Error loading app details" />
);
}
if (!selectedApp.details) {
return (
<div className="flex-1 py-6 text-white">
<div className="container flex justify-between">
<div className="flex-1 p-4 max-w-4xl animate-fade-slide-down">
<div className="mb-6">
<div className="h-10 w-64 bg-zinc-800 rounded animate-pulse mb-6" />
<div className="space-y-6">
{[...Array(3)].map((_, i) => (
<MemoryCardSkeleton key={i} />
))}
</div>
</div>
</div>
<div className="p-14 animate-fade-slide-down delay-2">
<AppDetailCardSkeleton />
</div>
</div>
</div>
);
}
const renderCreatedMemories = () => {
const memories = selectedApp.memories.created;
if (memories.loading) {
return (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<MemoryCardSkeleton key={i} />
))}
</div>
);
}
if (memories.error) {
return (
<NotFound message={memories.error} title="Error loading memories" />
);
}
if (memories.items.length === 0) {
return (
<div className="text-zinc-400 text-center py-8">No memories found</div>
);
}
return memories.items.map((memory) => (
<MemoryCard
key={memory.id + memory.created_at}
id={memory.id}
content={memory.content}
created_at={memory.created_at}
metadata={memory.metadata_}
categories={memory.categories}
app_name={memory.app_name}
state={memory.state}
/>
));
};
const renderAccessedMemories = () => {
const memories = selectedApp.memories.accessed;
if (memories.loading) {
return (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<MemoryCardSkeleton key={i} />
))}
</div>
);
}
if (memories.error) {
return (
<div className="text-red-400 bg-red-400/10 p-4 rounded-lg">
Error loading memories: {memories.error}
</div>
);
}
if (memories.items.length === 0) {
return (
<div className="text-zinc-400 text-center py-8">
No accessed memories found
</div>
);
}
return memories.items.map((accessedMemory) => (
<div
key={accessedMemory.memory.id + accessedMemory.memory.created_at}
className="relative"
>
<MemoryCard
id={accessedMemory.memory.id}
content={accessedMemory.memory.content}
created_at={accessedMemory.memory.created_at}
metadata={accessedMemory.memory.metadata_}
categories={accessedMemory.memory.categories}
access_count={accessedMemory.access_count}
app_name={accessedMemory.memory.app_name}
state={accessedMemory.memory.state}
/>
</div>
));
};
return (
<div className="flex-1 py-6 text-white">
<div className="container flex justify-between">
{/* Main content area */}
<div className="flex-1 p-4 max-w-4xl animate-fade-slide-down">
<Tabs
defaultValue="created"
className="mb-6"
onValueChange={setActiveTab}
>
<TabsList className="bg-transparent border-b border-zinc-800 rounded-none w-full justify-start gap-8 p-0">
<TabsTrigger
value="created"
className={`px-0 pb-2 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:shadow-none ${
activeTab === "created" ? "text-white" : "text-zinc-400"
}`}
>
Created ({selectedApp.memories.created.total})
</TabsTrigger>
<TabsTrigger
value="accessed"
className={`px-0 pb-2 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:shadow-none ${
activeTab === "accessed" ? "text-white" : "text-zinc-400"
}`}
>
Accessed ({selectedApp.memories.accessed.total})
</TabsTrigger>
</TabsList>
<TabsContent
value="created"
className="mt-6 space-y-6 animate-fade-slide-down delay-1"
>
{renderCreatedMemories()}
</TabsContent>
<TabsContent
value="accessed"
className="mt-6 space-y-6 animate-fade-slide-down delay-1"
>
{renderAccessedMemories()}
</TabsContent>
</Tabs>
</div>
{/* Sidebar */}
<div className="p-14 animate-fade-slide-down delay-2">
<AppDetailCard appId={appId} selectedApp={selectedApp} />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,84 @@
import type React from "react";
import { ArrowRight } from "lucide-react";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { constants } from "@/components/shared/source-app";
import { App } from "@/store/appsSlice";
import Image from "next/image";
import { useRouter } from "next/navigation";
interface AppCardProps {
app: App;
}
export function AppCard({ app }: AppCardProps) {
const router = useRouter();
const appConfig =
constants[app.name as keyof typeof constants] || constants.default;
const isActive = app.is_active;
return (
<Card className="bg-zinc-900 text-white border-zinc-800">
<CardHeader className="pb-2">
<div className="flex items-center gap-1">
<div className="relative z-10 rounded-full overflow-hidden bg-[#2a2a2a] w-6 h-6 flex items-center justify-center flex-shrink-0">
{appConfig.iconImage ? (
<div className="w-6 h-6 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
<Image
src={appConfig.iconImage}
alt={appConfig.name}
width={28}
height={28}
/>
</div>
) : (
<div className="w-6 h-6 flex items-center justify-center">
{appConfig.icon}
</div>
)}
</div>
<h2 className="text-xl font-semibold">{appConfig.name}</h2>
</div>
</CardHeader>
<CardContent className="pb-4 my-1">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-zinc-400 text-sm mb-1">Memories Created</p>
<p className="text-xl font-medium">
{app.total_memories_created.toLocaleString()} Memories
</p>
</div>
<div>
<p className="text-zinc-400 text-sm mb-1">Memories Accessed</p>
<p className="text-xl font-medium">
{app.total_memories_accessed.toLocaleString()} Memories
</p>
</div>
</div>
</CardContent>
<CardFooter className="border-t border-zinc-800 p-0 px-6 py-2 flex justify-between items-center">
<div
className={`${
isActive
? "bg-green-800 text-white hover:bg-green-500/20"
: "bg-red-500/20 text-red-400 hover:bg-red-500/20"
} rounded-lg px-2 py-0.5 flex items-center text-sm`}
>
<span className="h-2 w-2 my-auto mr-1 rounded-full inline-block bg-current"></span>
{isActive ? "Active" : "Inactive"}
</div>
<div
onClick={() => router.push(`/apps/${app.id}`)}
className="border hover:cursor-pointer border-zinc-700 bg-zinc-950 flex items-center px-3 py-1 text-sm rounded-lg text-white p-0 hover:bg-zinc-950/50 hover:text-white"
>
View Details <ArrowRight className="ml-2 h-4 w-4" />
</div>
</CardFooter>
</Card>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import { useEffect, useState } from "react";
import { Search, ChevronDown, SortAsc, SortDesc } from "lucide-react";
import { useDispatch, useSelector } from "react-redux";
import {
setSearchQuery,
setActiveFilter,
setSortBy,
setSortDirection,
} from "@/store/appsSlice";
import { RootState } from "@/store/store";
import { useCallback } from "react";
import debounce from "lodash/debounce";
import { useAppsApi } from "@/hooks/useAppsApi";
import { AppFiltersSkeleton } from "@/skeleton/AppFiltersSkeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
const sortOptions = [
{ value: "name", label: "Name" },
{ value: "memories", label: "Memories Created" },
{ value: "memories_accessed", label: "Memories Accessed" },
];
export function AppFilters() {
const dispatch = useDispatch();
const filters = useSelector((state: RootState) => state.apps.filters);
const [localSearch, setLocalSearch] = useState(filters.searchQuery);
const { isLoading } = useAppsApi();
const debouncedSearch = useCallback(
debounce((query: string) => {
dispatch(setSearchQuery(query));
}, 300),
[dispatch]
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setLocalSearch(query);
debouncedSearch(query);
};
const handleActiveFilterChange = (value: string) => {
dispatch(setActiveFilter(value === "all" ? "all" : value === "true"));
};
const setSorting = (sortBy: "name" | "memories" | "memories_accessed") => {
const newDirection =
filters.sortBy === sortBy && filters.sortDirection === "asc"
? "desc"
: "asc";
dispatch(setSortBy(sortBy));
dispatch(setSortDirection(newDirection));
};
useEffect(() => {
setLocalSearch(filters.searchQuery);
}, [filters.searchQuery]);
if (isLoading) {
return <AppFiltersSkeleton />;
}
return (
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-500" />
<Input
placeholder="Search Apps..."
className="pl-8 bg-zinc-950 border-zinc-800 max-w-[500px]"
value={localSearch}
onChange={handleSearchChange}
/>
</div>
<Select
value={String(filters.isActive)}
onValueChange={handleActiveFilterChange}
>
<SelectTrigger className="w-[130px] border-zinc-700/50 bg-zinc-900 hover:bg-zinc-800">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent className="border-zinc-700/50 bg-zinc-900 hover:bg-zinc-800">
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="true">Active</SelectItem>
<SelectItem value="false">Inactive</SelectItem>
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="h-9 px-4 border-zinc-700 bg-zinc-900 hover:bg-zinc-800"
>
{filters.sortDirection === "asc" ? (
<SortDesc className="h-4 w-4 mr-2" />
) : (
<SortAsc className="h-4 w-4 mr-2" />
)}
Sort: {sortOptions.find((o) => o.value === filters.sortBy)?.label}
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 bg-zinc-900 border-zinc-800 text-zinc-100">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-zinc-800" />
<DropdownMenuGroup>
{sortOptions.map((option) => (
<DropdownMenuItem
key={option.value}
onClick={() =>
setSorting(
option.value as "name" | "memories" | "memories_accessed"
)
}
className="cursor-pointer flex justify-between items-center"
>
{option.label}
{filters.sortBy === option.value &&
(filters.sortDirection === "asc" ? (
<SortAsc className="h-4 w-4 text-primary" />
) : (
<SortDesc className="h-4 w-4 text-primary" />
))}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -0,0 +1,48 @@
"use client";
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useAppsApi } from "@/hooks/useAppsApi";
import { AppCard } from "./AppCard";
import { AppCardSkeleton } from "@/skeleton/AppCardSkeleton";
export function AppGrid() {
const { fetchApps, isLoading } = useAppsApi();
const apps = useSelector((state: RootState) => state.apps.apps);
const filters = useSelector((state: RootState) => state.apps.filters);
useEffect(() => {
fetchApps({
name: filters.searchQuery,
is_active: filters.isActive === "all" ? undefined : filters.isActive,
sort_by: filters.sortBy,
sort_direction: filters.sortDirection,
});
}, [fetchApps, filters]);
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => (
<AppCardSkeleton key={i} />
))}
</div>
);
}
if (apps.length === 0) {
return (
<div className="text-center text-zinc-500 py-8">
No apps found matching your filters
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{apps.map((app) => (
<AppCard key={app.id} app={app} />
))}
</div>
);
}

View File

@@ -0,0 +1,20 @@
"use client";
import { AppFilters } from "./components/AppFilters";
import { AppGrid } from "./components/AppGrid";
import "@/styles/animation.css";
export default function AppsPage() {
return (
<main className="flex-1 py-6">
<div className="container">
<div className="mt-1 pb-4 animate-fade-slide-down">
<AppFilters />
</div>
<div className="animate-fade-slide-down delay-1">
<AppGrid />
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 260 94% 59%;
--primary-foreground: 355.7 100% 97.3%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 260 94% 59%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 260 94% 59%;
--primary-foreground: 355.7 100% 97.3%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 260 94% 59%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,38 @@
import type React from "react";
import "@/app/globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Navbar } from "@/components/Navbar";
import { Toaster } from "@/components/ui/toaster";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Providers } from "./providers";
export const metadata = {
title: "OpenMemory - Developer Dashboard",
description: "Manage your OpenMemory integration and stored memories",
generator: "v0.dev",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className="h-screen font-sans antialiased flex flex-col bg-zinc-950">
<Providers>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<Navbar />
<ScrollArea className="h-[calc(100vh-64px)]">{children}</ScrollArea>
<Toaster />
</ThemeProvider>
</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null;
}

View File

@@ -0,0 +1,88 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { useState, useRef } from "react";
import { GoPlus } from "react-icons/go";
import { Loader2 } from "lucide-react";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { toast } from "sonner";
import { Textarea } from "@/components/ui/textarea";
export function CreateMemoryDialog() {
const { createMemory, isLoading, fetchMemories } = useMemoriesApi();
const [open, setOpen] = useState(false);
const textRef = useRef<HTMLTextAreaElement>(null);
const handleCreateMemory = async (text: string) => {
try {
await createMemory(text);
toast.success("Memory created successfully");
// close the dialog
setOpen(false);
// refetch memories
await fetchMemories();
} catch (error) {
console.error(error);
toast.error("Failed to create memory");
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="bg-primary hover:bg-primary/90 text-white"
>
<GoPlus />
Create Memory
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[525px] bg-zinc-900 border-zinc-800">
<DialogHeader>
<DialogTitle>Create New Memory</DialogTitle>
<DialogDescription>
Add a new memory to your OpenMemory instance
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="memory">Memory</Label>
<Textarea
ref={textRef}
id="memory"
placeholder="e.g., Lives in San Francisco"
className="bg-zinc-950 border-zinc-800 min-h-[150px]"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
disabled={isLoading}
onClick={() => handleCreateMemory(textRef?.current?.value || "")}
>
{isLoading ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
"Save Memory"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,411 @@
"use client";
import { useEffect, useState } from "react";
import { Filter, X, ChevronDown, SortAsc, SortDesc } from "lucide-react";
import { useDispatch, useSelector } from "react-redux";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
} from "@/components/ui/dropdown-menu";
import { RootState } from "@/store/store";
import { useAppsApi } from "@/hooks/useAppsApi";
import { useFiltersApi } from "@/hooks/useFiltersApi";
import {
setSelectedApps,
setSelectedCategories,
clearFilters,
} from "@/store/filtersSlice";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
const columns = [
{
label: "Memory",
value: "memory",
},
{
label: "App Name",
value: "app_name",
},
{
label: "Created On",
value: "created_at",
},
];
export default function FilterComponent() {
const dispatch = useDispatch();
const { fetchApps } = useAppsApi();
const { fetchCategories, updateSort } = useFiltersApi();
const { fetchMemories } = useMemoriesApi();
const [isOpen, setIsOpen] = useState(false);
const [tempSelectedApps, setTempSelectedApps] = useState<string[]>([]);
const [tempSelectedCategories, setTempSelectedCategories] = useState<
string[]
>([]);
const [showArchived, setShowArchived] = useState(false);
const apps = useSelector((state: RootState) => state.apps.apps);
const categories = useSelector(
(state: RootState) => state.filters.categories.items
);
const filters = useSelector((state: RootState) => state.filters.apps);
useEffect(() => {
fetchApps();
fetchCategories();
}, [fetchApps, fetchCategories]);
useEffect(() => {
// Initialize temporary selections with current active filters when dialog opens
if (isOpen) {
setTempSelectedApps(filters.selectedApps);
setTempSelectedCategories(filters.selectedCategories);
setShowArchived(filters.showArchived || false);
}
}, [isOpen, filters]);
useEffect(() => {
handleClearFilters();
}, []);
const toggleAppFilter = (app: string) => {
setTempSelectedApps((prev) =>
prev.includes(app) ? prev.filter((a) => a !== app) : [...prev, app]
);
};
const toggleCategoryFilter = (category: string) => {
setTempSelectedCategories((prev) =>
prev.includes(category)
? prev.filter((c) => c !== category)
: [...prev, category]
);
};
const toggleAllApps = (checked: boolean) => {
setTempSelectedApps(checked ? apps.map((app) => app.id) : []);
};
const toggleAllCategories = (checked: boolean) => {
setTempSelectedCategories(checked ? categories.map((cat) => cat.name) : []);
};
const handleClearFilters = async () => {
setTempSelectedApps([]);
setTempSelectedCategories([]);
setShowArchived(false);
dispatch(clearFilters());
await fetchMemories();
};
const handleApplyFilters = async () => {
try {
// Get category IDs for selected category names
const selectedCategoryIds = categories
.filter((cat) => tempSelectedCategories.includes(cat.name))
.map((cat) => cat.id);
// Get app IDs for selected app names
const selectedAppIds = apps
.filter((app) => tempSelectedApps.includes(app.id))
.map((app) => app.id);
// Update the global state with temporary selections
dispatch(setSelectedApps(tempSelectedApps));
dispatch(setSelectedCategories(tempSelectedCategories));
dispatch({ type: "filters/setShowArchived", payload: showArchived });
await fetchMemories(undefined, 1, 10, {
apps: selectedAppIds,
categories: selectedCategoryIds,
sortColumn: filters.sortColumn,
sortDirection: filters.sortDirection,
showArchived: showArchived,
});
setIsOpen(false);
} catch (error) {
console.error("Failed to apply filters:", error);
}
};
const handleDialogChange = (open: boolean) => {
setIsOpen(open);
if (!open) {
// Reset temporary selections to active filters when dialog closes without applying
setTempSelectedApps(filters.selectedApps);
setTempSelectedCategories(filters.selectedCategories);
setShowArchived(filters.showArchived || false);
}
};
const setSorting = async (column: string) => {
const newDirection =
filters.sortColumn === column && filters.sortDirection === "asc"
? "desc"
: "asc";
updateSort(column, newDirection);
// Get category IDs for selected category names
const selectedCategoryIds = categories
.filter((cat) => tempSelectedCategories.includes(cat.name))
.map((cat) => cat.id);
// Get app IDs for selected app names
const selectedAppIds = apps
.filter((app) => tempSelectedApps.includes(app.id))
.map((app) => app.id);
try {
await fetchMemories(undefined, 1, 10, {
apps: selectedAppIds,
categories: selectedCategoryIds,
sortColumn: column,
sortDirection: newDirection,
});
} catch (error) {
console.error("Failed to apply sorting:", error);
}
};
const hasActiveFilters =
filters.selectedApps.length > 0 ||
filters.selectedCategories.length > 0 ||
filters.showArchived;
const hasTempFilters =
tempSelectedApps.length > 0 ||
tempSelectedCategories.length > 0 ||
showArchived;
return (
<div className="flex items-center gap-2">
<Dialog open={isOpen} onOpenChange={handleDialogChange}>
<DialogTrigger asChild>
<Button
variant="outline"
className={`h-9 px-4 border-zinc-700/50 bg-zinc-900 hover:bg-zinc-800 ${
hasActiveFilters ? "border-primary" : ""
}`}
>
<Filter
className={`h-4 w-4 ${hasActiveFilters ? "text-primary" : ""}`}
/>
Filter
{hasActiveFilters && (
<Badge className="ml-2 bg-primary hover:bg-primary/80 text-xs">
{filters.selectedApps.length +
filters.selectedCategories.length +
(filters.showArchived ? 1 : 0)}
</Badge>
)}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] bg-zinc-900 border-zinc-800 text-zinc-100">
<DialogHeader>
<DialogTitle className="text-zinc-100 flex justify-between items-center">
<span>Filters</span>
</DialogTitle>
</DialogHeader>
<Tabs defaultValue="apps" className="w-full">
<TabsList className="grid grid-cols-3 bg-zinc-800">
<TabsTrigger
value="apps"
className="data-[state=active]:bg-zinc-700"
>
Apps
</TabsTrigger>
<TabsTrigger
value="categories"
className="data-[state=active]:bg-zinc-700"
>
Categories
</TabsTrigger>
<TabsTrigger
value="archived"
className="data-[state=active]:bg-zinc-700"
>
Archived
</TabsTrigger>
</TabsList>
<TabsContent value="apps" className="mt-4">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="select-all-apps"
checked={
apps.length > 0 && tempSelectedApps.length === apps.length
}
onCheckedChange={(checked) =>
toggleAllApps(checked as boolean)
}
className="border-zinc-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor="select-all-apps"
className="text-sm font-normal text-zinc-300 cursor-pointer"
>
Select All
</Label>
</div>
{apps.map((app) => (
<div key={app.id} className="flex items-center space-x-2">
<Checkbox
id={`app-${app.id}`}
checked={tempSelectedApps.includes(app.id)}
onCheckedChange={() => toggleAppFilter(app.id)}
className="border-zinc-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor={`app-${app.id}`}
className="text-sm font-normal text-zinc-300 cursor-pointer"
>
{app.name}
</Label>
</div>
))}
</div>
</TabsContent>
<TabsContent value="categories" className="mt-4">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="select-all-categories"
checked={
categories.length > 0 &&
tempSelectedCategories.length === categories.length
}
onCheckedChange={(checked) =>
toggleAllCategories(checked as boolean)
}
className="border-zinc-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor="select-all-categories"
className="text-sm font-normal text-zinc-300 cursor-pointer"
>
Select All
</Label>
</div>
{categories.map((category) => (
<div
key={category.name}
className="flex items-center space-x-2"
>
<Checkbox
id={`category-${category.name}`}
checked={tempSelectedCategories.includes(category.name)}
onCheckedChange={() =>
toggleCategoryFilter(category.name)
}
className="border-zinc-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor={`category-${category.name}`}
className="text-sm font-normal text-zinc-300 cursor-pointer"
>
{category.name}
</Label>
</div>
))}
</div>
</TabsContent>
<TabsContent value="archived" className="mt-4">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="show-archived"
checked={showArchived}
onCheckedChange={(checked) =>
setShowArchived(checked as boolean)
}
className="border-zinc-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor="show-archived"
className="text-sm font-normal text-zinc-300 cursor-pointer"
>
Show Archived Memories
</Label>
</div>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end mt-4 gap-3">
{/* Clear all button */}
{hasTempFilters && (
<Button
onClick={handleClearFilters}
className="bg-zinc-800 hover:bg-zinc-700 text-zinc-300"
>
Clear All
</Button>
)}
{/* Apply filters button */}
<Button
onClick={handleApplyFilters}
className="bg-primary hover:bg-primary/80 text-white"
>
Apply Filters
</Button>
</div>
</DialogContent>
</Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="h-9 px-4 border-zinc-700/50 bg-zinc-900 hover:bg-zinc-800"
>
{filters.sortDirection === "asc" ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
Sort: {columns.find((c) => c.value === filters.sortColumn)?.label}
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 bg-zinc-900 border-zinc-800 text-zinc-100">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-zinc-800" />
<DropdownMenuGroup>
{columns.map((column) => (
<DropdownMenuItem
key={column.value}
onClick={() => setSorting(column.value)}
className="cursor-pointer flex justify-between items-center"
>
{column.label}
{filters.sortColumn === column.value &&
(filters.sortDirection === "asc" ? (
<SortAsc className="h-4 w-4 text-primary" />
) : (
<SortDesc className="h-4 w-4 text-primary" />
))}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -0,0 +1,145 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Category, Client } from "../../../components/types";
import { MemoryTable } from "./MemoryTable";
import { MemoryPagination } from "./MemoryPagination";
import { CreateMemoryDialog } from "./CreateMemoryDialog";
import { PageSizeSelector } from "./PageSizeSelector";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { useRouter, useSearchParams } from "next/navigation";
import { MemoryTableSkeleton } from "@/skeleton/MemoryTableSkeleton";
export function MemoriesSection() {
const router = useRouter();
const searchParams = useSearchParams();
const { fetchMemories } = useMemoriesApi();
const [memories, setMemories] = useState<any[]>([]);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const currentPage = Number(searchParams.get("page")) || 1;
const itemsPerPage = Number(searchParams.get("size")) || 10;
const [selectedCategory, setSelectedCategory] = useState<Category | "all">(
"all"
);
const [selectedClient, setSelectedClient] = useState<Client | "all">("all");
useEffect(() => {
const loadMemories = async () => {
setIsLoading(true);
try {
const searchQuery = searchParams.get("search") || "";
const result = await fetchMemories(
searchQuery,
currentPage,
itemsPerPage
);
setMemories(result.memories);
setTotalItems(result.total);
setTotalPages(result.pages);
} catch (error) {
console.error("Failed to fetch memories:", error);
}
setIsLoading(false);
};
loadMemories();
}, [currentPage, itemsPerPage, fetchMemories, searchParams]);
const setCurrentPage = (page: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString());
params.set("size", itemsPerPage.toString());
router.push(`?${params.toString()}`);
};
const handlePageSizeChange = (size: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1"); // Reset to page 1 when changing page size
params.set("size", size.toString());
router.push(`?${params.toString()}`);
};
if (isLoading) {
return (
<div className="w-full bg-transparent">
<MemoryTableSkeleton />
<div className="flex items-center justify-between mt-4">
<div className="h-8 w-32 bg-zinc-800 rounded animate-pulse" />
<div className="h-8 w-48 bg-zinc-800 rounded animate-pulse" />
<div className="h-8 w-32 bg-zinc-800 rounded animate-pulse" />
</div>
</div>
);
}
return (
<div className="w-full bg-transparent">
<div>
{memories.length > 0 ? (
<>
<MemoryTable />
<div className="flex items-center justify-between mt-4">
<PageSizeSelector
pageSize={itemsPerPage}
onPageSizeChange={handlePageSizeChange}
/>
<div className="text-sm text-zinc-500 mr-2">
Showing {(currentPage - 1) * itemsPerPage + 1} to{" "}
{Math.min(currentPage * itemsPerPage, totalItems)} of{" "}
{totalItems} memories
</div>
<MemoryPagination
currentPage={currentPage}
totalPages={totalPages}
setCurrentPage={setCurrentPage}
/>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="rounded-full bg-zinc-800 p-3 mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-6 w-6 text-zinc-400"
>
<path d="M21 9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"></path>
<path d="M16 2v6h6"></path>
<path d="M12 18v-6"></path>
<path d="M9 15h6"></path>
</svg>
</div>
<h3 className="text-lg font-medium">No memories found</h3>
<p className="text-zinc-400 mt-1 mb-4">
{selectedCategory !== "all" || selectedClient !== "all"
? "Try adjusting your filters"
: "Create your first memory to see it here"}
</p>
{selectedCategory !== "all" || selectedClient !== "all" ? (
<Button
variant="outline"
onClick={() => {
setSelectedCategory("all");
setSelectedClient("all");
}}
>
Clear Filters
</Button>
) : (
<CreateMemoryDialog />
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,154 @@
"use client";
import { Archive, Pause, Play, Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { FiTrash2 } from "react-icons/fi";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/store";
import { clearSelection } from "@/store/memoriesSlice";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useRouter, useSearchParams } from "next/navigation";
import { debounce } from "lodash";
import { useEffect, useRef } from "react";
import FilterComponent from "./FilterComponent";
import { clearFilters } from "@/store/filtersSlice";
export function MemoryFilters() {
const dispatch = useDispatch();
const selectedMemoryIds = useSelector(
(state: RootState) => state.memories.selectedMemoryIds
);
const { deleteMemories, updateMemoryState, fetchMemories } = useMemoriesApi();
const router = useRouter();
const searchParams = useSearchParams();
const activeFilters = useSelector((state: RootState) => state.filters.apps);
const inputRef = useRef<HTMLInputElement>(null);
const handleDeleteSelected = async () => {
try {
await deleteMemories(selectedMemoryIds);
dispatch(clearSelection());
} catch (error) {
console.error("Failed to delete memories:", error);
}
};
const handleArchiveSelected = async () => {
try {
await updateMemoryState(selectedMemoryIds, "archived");
} catch (error) {
console.error("Failed to archive memories:", error);
}
};
const handlePauseSelected = async () => {
try {
await updateMemoryState(selectedMemoryIds, "paused");
} catch (error) {
console.error("Failed to pause memories:", error);
}
};
const handleResumeSelected = async () => {
try {
await updateMemoryState(selectedMemoryIds, "active");
} catch (error) {
console.error("Failed to resume memories:", error);
}
};
// add debounce
const handleSearch = debounce(async (query: string) => {
router.push(`/memories?search=${query}`);
}, 500);
useEffect(() => {
// if the url has a search param, set the input value to the search param
if (searchParams.get("search")) {
if (inputRef.current) {
inputRef.current.value = searchParams.get("search") || "";
inputRef.current.focus();
}
}
}, []);
const handleClearAllFilters = async () => {
dispatch(clearFilters());
await fetchMemories(); // Fetch memories without any filters
};
const hasActiveFilters =
activeFilters.selectedApps.length > 0 ||
activeFilters.selectedCategories.length > 0;
return (
<div className="flex flex-col md:flex-row gap-4 mb-4">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-500" />
<Input
ref={inputRef}
placeholder="Search memories..."
className="pl-8 bg-zinc-950 border-zinc-800 max-w-[500px]"
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
<div className="flex gap-2">
<FilterComponent />
{hasActiveFilters && (
<Button
variant="outline"
className="bg-zinc-900 text-zinc-300 hover:bg-zinc-800"
onClick={handleClearAllFilters}
>
Clear Filters
</Button>
)}
{selectedMemoryIds.length > 0 && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="border-zinc-700/50 bg-zinc-900 hover:bg-zinc-800"
>
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-zinc-900 border-zinc-800"
>
<DropdownMenuItem onClick={handleArchiveSelected}>
<Archive className="mr-2 h-4 w-4" />
Archive Selected
</DropdownMenuItem>
<DropdownMenuItem onClick={handlePauseSelected}>
<Pause className="mr-2 h-4 w-4" />
Pause Selected
</DropdownMenuItem>
<DropdownMenuItem onClick={handleResumeSelected}>
<Play className="mr-2 h-4 w-4" />
Resume Selected
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDeleteSelected}
className="text-red-500"
>
<FiTrash2 className="mr-2 h-4 w-4" />
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
interface MemoryPaginationProps {
currentPage: number;
totalPages: number;
setCurrentPage: (page: number) => void;
}
export function MemoryPagination({
currentPage,
totalPages,
setCurrentPage,
}: MemoryPaginationProps) {
return (
<div className="flex items-center justify-between my-auto">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => setCurrentPage(Math.max(currentPage - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="text-sm">
Page {currentPage} of {totalPages}
</div>
<Button
variant="outline"
size="icon"
onClick={() => setCurrentPage(Math.min(currentPage + 1, totalPages))}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,302 @@
import {
Edit,
MoreHorizontal,
Trash2,
Pause,
Archive,
Play,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import {
selectMemory,
deselectMemory,
selectAllMemories,
clearSelection,
} from "@/store/memoriesSlice";
import SourceApp from "@/components/shared/source-app";
import { HiMiniRectangleStack } from "react-icons/hi2";
import { PiSwatches } from "react-icons/pi";
import { GoPackage } from "react-icons/go";
import { CiCalendar } from "react-icons/ci";
import { useRouter } from "next/navigation";
import Categories from "@/components/shared/categories";
import { useUI } from "@/hooks/useUI";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatDate } from "@/lib/helpers";
export function MemoryTable() {
const { toast } = useToast();
const router = useRouter();
const dispatch = useDispatch();
const selectedMemoryIds = useSelector(
(state: RootState) => state.memories.selectedMemoryIds
);
const memories = useSelector((state: RootState) => state.memories.memories);
const { deleteMemories, updateMemoryState, isLoading } = useMemoriesApi();
const handleDeleteMemory = (id: string) => {
deleteMemories([id]);
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
dispatch(selectAllMemories());
} else {
dispatch(clearSelection());
}
};
const handleSelectMemory = (id: string, checked: boolean) => {
if (checked) {
dispatch(selectMemory(id));
} else {
dispatch(deselectMemory(id));
}
};
const { handleOpenUpdateMemoryDialog } = useUI();
const handleEditMemory = (memory_id: string, memory_content: string) => {
handleOpenUpdateMemoryDialog(memory_id, memory_content);
};
const handleUpdateMemoryState = async (id: string, newState: string) => {
try {
await updateMemoryState([id], newState);
} catch (error) {
toast({
title: "Error",
description: "Failed to update memory state",
variant: "destructive",
});
}
};
const isAllSelected =
memories.length > 0 && selectedMemoryIds.length === memories.length;
const isPartiallySelected =
selectedMemoryIds.length > 0 && selectedMemoryIds.length < memories.length;
const handleMemoryClick = (id: string) => {
router.push(`/memory/${id}`);
};
return (
<div className="rounded-md border">
<Table className="">
<TableHeader>
<TableRow className="bg-zinc-800 hover:bg-zinc-800">
<TableHead className="w-[50px] pl-4">
<Checkbox
className="data-[state=checked]:border-primary border-zinc-500/50"
checked={isAllSelected}
data-state={
isPartiallySelected
? "indeterminate"
: isAllSelected
? "checked"
: "unchecked"
}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead className="border-zinc-700">
<div className="flex items-center min-w-[600px]">
<HiMiniRectangleStack className="mr-1" />
Memory
</div>
</TableHead>
<TableHead className="border-zinc-700">
<div className="flex items-center">
<PiSwatches className="mr-1" size={15} />
Categories
</div>
</TableHead>
<TableHead className="w-[140px] border-zinc-700">
<div className="flex items-center">
<GoPackage className="mr-1" />
Source App
</div>
</TableHead>
<TableHead className="w-[140px] border-zinc-700">
<div className="flex items-center w-full justify-center">
<CiCalendar className="mr-1" size={16} />
Created On
</div>
</TableHead>
<TableHead className="text-right border-zinc-700 flex justify-center">
<div className="flex items-center justify-end">
<MoreHorizontal className="h-4 w-4 mr-2" />
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{memories.map((memory) => (
<TableRow
key={memory.id}
className={`hover:bg-zinc-900/50 ${
memory.state === "paused" || memory.state === "archived"
? "text-zinc-400"
: ""
} ${isLoading ? "animate-pulse opacity-50" : ""}`}
>
<TableCell className="pl-4">
<Checkbox
className="data-[state=checked]:border-primary border-zinc-500/50"
checked={selectedMemoryIds.includes(memory.id)}
onCheckedChange={(checked) =>
handleSelectMemory(memory.id, checked as boolean)
}
/>
</TableCell>
<TableCell className="">
{memory.state === "paused" || memory.state === "archived" ? (
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<div
onClick={() => handleMemoryClick(memory.id)}
className={`font-medium ${
memory.state === "paused" ||
memory.state === "archived"
? "text-zinc-400"
: "text-white"
} cursor-pointer`}
>
{memory.memory}
</div>
</TooltipTrigger>
<TooltipContent>
<p>
This memory is{" "}
<span className="font-bold">
{memory.state === "paused" ? "paused" : "archived"}
</span>{" "}
and <span className="font-bold">disabled</span>.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div
onClick={() => handleMemoryClick(memory.id)}
className={`font-medium text-white cursor-pointer`}
>
{memory.memory}
</div>
)}
</TableCell>
<TableCell className="">
<div className="flex flex-wrap gap-1">
<Categories
categories={memory.categories}
isPaused={
memory.state === "paused" || memory.state === "archived"
}
concat={true}
/>
</div>
</TableCell>
<TableCell className="w-[140px] text-center">
<SourceApp source={memory.app_name} />
</TableCell>
<TableCell className="w-[140px] text-center">
{formatDate(memory.created_at)}
</TableCell>
<TableCell className="text-right flex justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-zinc-900 border-zinc-800"
>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
const newState =
memory.state === "active" ? "paused" : "active";
handleUpdateMemoryState(memory.id, newState);
}}
>
{memory?.state === "active" ? (
<>
<Pause className="mr-2 h-4 w-4" />
Pause
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Resume
</>
)}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
const newState =
memory.state === "active" ? "archived" : "active";
handleUpdateMemoryState(memory.id, newState);
}}
>
<Archive className="mr-2 h-4 w-4" />
{memory?.state !== "archived" ? (
<>Archive</>
) : (
<>Unarchive</>
)}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => handleEditMemory(memory.id, memory.memory)}
>
<Edit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-red-500 focus:text-red-500"
onClick={() => handleDeleteMemory(memory.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface PageSizeSelectorProps {
pageSize: number;
onPageSizeChange: (size: number) => void;
}
export function PageSizeSelector({
pageSize,
onPageSizeChange,
}: PageSizeSelectorProps) {
const pageSizeOptions = [10, 20, 50, 100];
return (
<div className="flex items-center gap-2">
<span className="text-sm text-zinc-500">Show</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => onPageSizeChange(Number(value))}
>
<SelectTrigger className="w-[70px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{pageSizeOptions.map((size) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-sm text-zinc-500">items</span>
</div>
);
}
export default PageSizeSelector;

View File

@@ -0,0 +1,45 @@
"use client";
import { useEffect } from "react";
import { MemoriesSection } from "@/app/memories/components/MemoriesSection";
import { MemoryFilters } from "@/app/memories/components/MemoryFilters";
import { useRouter, useSearchParams } from "next/navigation";
import "@/styles/animation.css";
import UpdateMemory from "@/components/shared/update-memory";
import { useUI } from "@/hooks/useUI";
export default function MemoriesPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { updateMemoryDialog, handleCloseUpdateMemoryDialog } = useUI();
useEffect(() => {
// Set default pagination values if not present in URL
if (!searchParams.has("page") || !searchParams.has("size")) {
const params = new URLSearchParams(searchParams.toString());
if (!searchParams.has("page")) params.set("page", "1");
if (!searchParams.has("size")) params.set("size", "10");
router.push(`?${params.toString()}`);
}
}, []);
return (
<div className="">
<UpdateMemory
memoryId={updateMemoryDialog.memoryId || ""}
memoryContent={updateMemoryDialog.memoryContent || ""}
open={updateMemoryDialog.isOpen}
onOpenChange={handleCloseUpdateMemoryDialog}
/>
<main className="flex-1 py-6">
<div className="container">
<div className="mt-1 pb-4 animate-fade-slide-down">
<MemoryFilters />
</div>
<div className="animate-fade-slide-down delay-1">
<MemoriesSection />
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import Image from "next/image";
import { useEffect, useState } from "react";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { constants } from "@/components/shared/source-app";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { ScrollArea } from "@/components/ui/scroll-area";
interface AccessLogEntry {
id: string;
app_name: string;
accessed_at: string;
}
interface AccessLogProps {
memoryId: string;
}
export function AccessLog({ memoryId }: AccessLogProps) {
const { fetchAccessLogs } = useMemoriesApi();
const accessEntries = useSelector(
(state: RootState) => state.memories.accessLogs
);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadAccessLogs = async () => {
try {
await fetchAccessLogs(memoryId);
} catch (error) {
console.error("Failed to fetch access logs:", error);
} finally {
setIsLoading(false);
}
};
loadAccessLogs();
}, []);
if (isLoading) {
return (
<div className="w-full max-w-md mx-auto rounded-3xl overflow-hidden bg-[#1c1c1c] text-white p-6">
<p className="text-center text-zinc-500">Loading access logs...</p>
</div>
);
}
return (
<div className="w-full max-w-md mx-auto rounded-lg overflow-hidden bg-zinc-900 border border-zinc-800 text-white pb-1">
<div className="px-6 py-4 flex justify-between items-center bg-zinc-800 border-b border-zinc-800">
<h2 className="font-semibold">Access Log</h2>
{/* <button className="px-3 py-1 text-sm rounded-lg border border-[#ff5533] text-[#ff5533] flex items-center gap-2 hover:bg-[#ff5533]/10 transition-colors">
<PauseIcon size={18} />
<span>Pause Access</span>
</button> */}
</div>
<ScrollArea className="p-6 max-h-[450px]">
{accessEntries.length === 0 && (
<div className="w-full max-w-md mx-auto rounded-3xl overflow-hidden min-h-[110px] flex items-center justify-center text-white p-6">
<p className="text-center text-zinc-500">
No access logs available
</p>
</div>
)}
<ul className="space-y-8">
{accessEntries.map((entry: AccessLogEntry, index: number) => {
const appConfig =
constants[entry.app_name as keyof typeof constants] ||
constants.default;
return (
<li key={entry.id} className="relative flex items-start gap-4">
<div className="relative z-10 rounded-full overflow-hidden bg-[#2a2a2a] w-8 h-8 flex items-center justify-center flex-shrink-0">
{appConfig.iconImage ? (
<Image
src={appConfig.iconImage}
alt={`${appConfig.name} icon`}
width={30}
height={30}
className="w-8 h-8 object-contain"
/>
) : (
<div className="w-8 h-8 flex items-center justify-center">
{appConfig.icon}
</div>
)}
</div>
{index < accessEntries.length - 1 && (
<div className="absolute left-4 top-6 bottom-0 w-[1px] h-[calc(100%+1rem)] bg-[#333333] transform -translate-x-1/2"></div>
)}
<div className="flex flex-col">
<span className="font-medium">{appConfig.name}</span>
<span className="text-zinc-400 text-sm">
{new Date(entry.accessed_at + "Z").toLocaleDateString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
}
)}
</span>
</div>
</li>
);
})}
</ul>
</ScrollArea>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import { Button } from "@/components/ui/button";
import { Pencil, Archive, Trash, Pause, Play, ChevronDown } from "lucide-react";
import { useUI } from "@/hooks/useUI";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface MemoryActionsProps {
memoryId: string;
memoryContent: string;
memoryState: string;
}
export function MemoryActions({
memoryId,
memoryContent,
memoryState,
}: MemoryActionsProps) {
const { handleOpenUpdateMemoryDialog } = useUI();
const { updateMemoryState, isLoading } = useMemoriesApi();
const handleEdit = () => {
handleOpenUpdateMemoryDialog(memoryId, memoryContent);
};
const handleStateChange = (newState: string) => {
updateMemoryState([memoryId], newState);
};
const getStateLabel = () => {
switch (memoryState) {
case "archived":
return "Archived";
case "paused":
return "Paused";
default:
return "Active";
}
};
const getStateIcon = () => {
switch (memoryState) {
case "archived":
return <Archive className="h-3 w-3 mr-2" />;
case "paused":
return <Pause className="h-3 w-3 mr-2" />;
default:
return <Play className="h-3 w-3 mr-2" />;
}
};
return (
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
disabled={isLoading}
variant="outline"
size="sm"
className="shadow-md bg-zinc-900 border border-zinc-700/50 hover:bg-zinc-950 text-zinc-400"
>
<span className="font-semibold">{getStateLabel()}</span>
<ChevronDown className="h-3 w-3 mt-1 -ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40 bg-zinc-900 border-zinc-800 text-zinc-100">
<DropdownMenuLabel>Change State</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-zinc-800" />
<DropdownMenuItem
onClick={() => handleStateChange("active")}
className="cursor-pointer flex items-center"
disabled={memoryState === "active"}
>
<Play className="h-3 w-3 mr-2" />
<span className="font-semibold">Active</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleStateChange("paused")}
className="cursor-pointer flex items-center"
disabled={memoryState === "paused"}
>
<Pause className="h-3 w-3 mr-2" />
<span className="font-semibold">Pause</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleStateChange("archived")}
className="cursor-pointer flex items-center"
disabled={memoryState === "archived"}
>
<Archive className="h-3 w-3 mr-2" />
<span className="font-semibold">Archive</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
disabled={isLoading}
variant="outline"
size="sm"
onClick={handleEdit}
className="shadow-md bg-zinc-900 border border-zinc-700/50 hover:bg-zinc-950 text-zinc-400"
>
<Pencil className="h-3 w-3 -mr-1" />
<span className="font-semibold">Edit</span>
</Button>
</div>
);
}

View File

@@ -0,0 +1,151 @@
"use client";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { MemoryActions } from "./MemoryActions";
import { ArrowLeft, Copy, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { AccessLog } from "./AccessLog";
import Image from "next/image";
import Categories from "@/components/shared/categories";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { constants } from "@/components/shared/source-app";
import { RelatedMemories } from "./RelatedMemories";
interface MemoryDetailsProps {
memory_id: string;
}
export function MemoryDetails({ memory_id }: MemoryDetailsProps) {
const router = useRouter();
const { fetchMemoryById, hasUpdates } = useMemoriesApi();
const memory = useSelector(
(state: RootState) => state.memories.selectedMemory
);
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (memory?.id) {
await navigator.clipboard.writeText(memory.id);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
useEffect(() => {
fetchMemoryById(memory_id);
}, []);
return (
<div className="container mx-auto py-6 px-4">
<Button
variant="ghost"
className="mb-4 text-zinc-400 hover:text-white"
onClick={() => router.back()}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Memories
</Button>
<div className="flex gap-4 w-full">
<div className="rounded-lg w-2/3 border h-fit pb-2 border-zinc-800 bg-zinc-900 overflow-hidden">
<div className="">
<div className="flex px-6 py-3 justify-between items-center mb-6 bg-zinc-800 border-b border-zinc-800">
<div className="flex items-center gap-2">
<h1 className="font-semibold text-white">
Memory{" "}
<span className="ml-1 text-zinc-400 text-sm font-normal">
#{memory?.id?.slice(0, 6)}
</span>
</h1>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 text-zinc-400 hover:text-white -ml-[5px] mt-1"
onClick={handleCopy}
>
{copied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<MemoryActions
memoryId={memory?.id || ""}
memoryContent={memory?.text || ""}
memoryState={memory?.state || ""}
/>
</div>
<div className="px-6 py-2">
<div className="border-l-2 border-primary pl-4 mb-6">
<p
className={`${
memory?.state === "archived" || memory?.state === "paused"
? "text-zinc-400"
: "text-white"
}`}
>
{memory?.text}
</p>
</div>
<div className="mt-6 pt-4 border-t border-zinc-800">
<div className="flex justify-between items-center">
<div className="">
<Categories
categories={memory?.categories || []}
isPaused={
memory?.state === "archived" ||
memory?.state === "paused"
}
/>
</div>
<div className="flex items-center gap-2 min-w-[300px] justify-end">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-zinc-700 px-3 py-1 rounded-lg">
<span className="text-sm text-zinc-400">
Created by:
</span>
<div className="w-4 h-4 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
<Image
src={
constants[
memory?.app_name as keyof typeof constants
]?.iconImage || ""
}
alt="OpenMemory"
width={24}
height={24}
/>
</div>
<p className="text-sm text-zinc-100 font-semibold">
{
constants[
memory?.app_name as keyof typeof constants
]?.name
}
</p>
</div>
</div>
</div>
</div>
{/* <div className="flex justify-end gap-2 w-full mt-2">
<p className="text-sm font-semibold text-primary my-auto">
{new Date(memory.created_at).toLocaleString()}
</p>
</div> */}
</div>
</div>
</div>
</div>
<div className="w-1/3 flex flex-col gap-4">
<AccessLog memoryId={memory?.id || ""} />
<RelatedMemories memoryId={memory?.id || ""} />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { useEffect, useState } from "react";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Memory } from "@/components/types";
import Categories from "@/components/shared/categories";
import Link from "next/link";
import { formatDate } from "@/lib/helpers";
interface RelatedMemoriesProps {
memoryId: string;
}
export function RelatedMemories({ memoryId }: RelatedMemoriesProps) {
const { fetchRelatedMemories } = useMemoriesApi();
const relatedMemories = useSelector(
(state: RootState) => state.memories.relatedMemories
);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadRelatedMemories = async () => {
try {
await fetchRelatedMemories(memoryId);
} catch (error) {
console.error("Failed to fetch related memories:", error);
} finally {
setIsLoading(false);
}
};
loadRelatedMemories();
}, []);
if (isLoading) {
return (
<div className="w-full max-w-2xl mx-auto rounded-lg overflow-hidden bg-zinc-900 text-white p-6">
<p className="text-center text-zinc-500">Loading related memories...</p>
</div>
);
}
if (!relatedMemories.length) {
return (
<div className="w-full max-w-2xl mx-auto rounded-lg overflow-hidden bg-zinc-900 text-white p-6">
<p className="text-center text-zinc-500">No related memories found</p>
</div>
);
}
return (
<div className="w-full max-w-2xl mx-auto rounded-lg overflow-hidden bg-zinc-900 border border-zinc-800 text-white">
<div className="px-6 py-4 flex justify-between items-center bg-zinc-800 border-b border-zinc-800">
<h2 className="font-semibold">Related Memories</h2>
</div>
<div className="space-y-6 p-6">
{relatedMemories.map((memory: Memory) => (
<div
key={memory.id}
className="border-l-2 border-zinc-800 pl-6 py-1 hover:bg-zinc-700/10 transition-colors cursor-pointer"
>
<Link href={`/memory/${memory.id}`}>
<h3 className="font-medium mb-3">{memory.memory}</h3>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Categories
categories={memory.categories}
isPaused={
memory.state === "paused" || memory.state === "archived"
}
concat={true}
/>
{memory.state !== "active" && (
<span className="inline-block px-3 border border-yellow-600 text-yellow-600 font-semibold text-xs rounded-full bg-yellow-400/10 backdrop-blur-sm">
{memory.state === "paused" ? "Paused" : "Archived"}
</span>
)}
</div>
<div className="flex items-center gap-4">
<div className="text-zinc-400 text-sm">
{formatDate(memory.created_at)}
</div>
</div>
</div>
</Link>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import "@/styles/animation.css";
import { useEffect } from "react";
import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import { use } from "react";
import { MemorySkeleton } from "@/skeleton/MemorySkeleton";
import { MemoryDetails } from "./components/MemoryDetails";
import UpdateMemory from "@/components/shared/update-memory";
import { useUI } from "@/hooks/useUI";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
import NotFound from "@/app/not-found";
function MemoryContent({ id }: { id: string }) {
const { fetchMemoryById, isLoading, error } = useMemoriesApi();
const memory = useSelector(
(state: RootState) => state.memories.selectedMemory
);
useEffect(() => {
const loadMemory = async () => {
try {
await fetchMemoryById(id);
} catch (err) {
console.error("Failed to load memory:", err);
}
};
loadMemory();
}, []);
if (isLoading) {
return <MemorySkeleton />;
}
if (error) {
return <NotFound message={error} />;
}
if (!memory) {
return <NotFound message="Memory not found" statusCode={404} />;
}
return <MemoryDetails memory_id={memory.id} />;
}
export default function MemoryPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const resolvedParams = use(params);
const { updateMemoryDialog, handleCloseUpdateMemoryDialog } = useUI();
return (
<div>
<div className="animate-fade-slide-down delay-1">
<UpdateMemory
memoryId={updateMemoryDialog.memoryId || ""}
memoryContent={updateMemoryDialog.memoryContent || ""}
open={updateMemoryDialog.isOpen}
onOpenChange={handleCloseUpdateMemoryDialog}
/>
</div>
<div className="animate-fade-slide-down delay-2">
<MemoryContent id={resolvedParams.id} />
</div>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import "@/styles/notfound.scss";
import Link from "next/link";
import { Button } from "@/components/ui/button";
interface NotFoundProps {
statusCode?: number;
message?: string;
title?: string;
}
const getStatusCode = (message: string) => {
const possibleStatusCodes = ["404", "403", "500", "422"];
const potentialStatusCode = possibleStatusCodes.find((code) =>
message.includes(code)
);
return potentialStatusCode ? parseInt(potentialStatusCode) : undefined;
};
export default function NotFound({
statusCode,
message = "Page Not Found",
title,
}: NotFoundProps) {
const potentialStatusCode = getStatusCode(message);
return (
<div className="flex flex-col items-center justify-center h-[calc(100vh-100px)]">
<div className="site">
<div className="sketch">
<div className="bee-sketch red"></div>
<div className="bee-sketch blue"></div>
</div>
<h1>
{statusCode
? `${statusCode}:`
: potentialStatusCode
? `${potentialStatusCode}:`
: "404"}
<small>{title || message || "Page Not Found"}</small>
</h1>
</div>
<div className="">
<Button
variant="outline"
className="bg-primary text-white hover:bg-primary/80"
>
<Link href="/">Go Home</Link>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import { Install } from "@/components/dashboard/Install";
import Stats from "@/components/dashboard/Stats";
import { MemoryFilters } from "@/app/memories/components/MemoryFilters";
import { MemoriesSection } from "@/app/memories/components/MemoriesSection";
import "@/styles/animation.css";
export default function DashboardPage() {
return (
<div className="text-white py-6">
<div className="container">
<div className="w-full mx-auto space-y-6">
<div className="grid grid-cols-3 gap-6">
{/* Memory Category Breakdown */}
<div className="col-span-2 animate-fade-slide-down">
<Install />
</div>
{/* Memories Stats */}
<div className="col-span-1 animate-fade-slide-down delay-1">
<Stats />
</div>
</div>
<div>
<div className="animate-fade-slide-down delay-2">
<MemoryFilters />
</div>
<div className="animate-fade-slide-down delay-3">
<MemoriesSection />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,8 @@
"use client";
import { Provider } from "react-redux";
import { store } from "../store/store";
export function Providers({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}