Skip to content

Commit

Permalink
Add image gallery page
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaapple committed Dec 30, 2024
1 parent 5318964 commit b99ecd7
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function page() {
const [urls, failedUrls, indexCount, searchCount] = await getUserStatistics(user.id);

return (
<ScrollArea className="group mx-auto overflow-auto peer-[[data-state=open]]:lg:pl-[300px] peer-[[data-state=open]]:xl:pl-[320px] my-10">
<ScrollArea className="group mx-auto overflow-auto">
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Search & Index Statistics</h2>
Expand Down
21 changes: 21 additions & 0 deletions frontend/app/[locale]/(dashboard)/images/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getCurrentUser } from '@/lib/session';
import { getUserImages } from '@/lib/store/image';
import { ImageList } from '@/components/dashboard/image-list';
import { redirect } from 'next/navigation';

export default async function MyPages() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
const images = await getUserImages(user.id);

return (
<div className="flex flex-col items-center justify-between space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">MemFree Image Gallery</h1>
</div>
<ImageList user={user} items={images} />
</div>
);
}
53 changes: 53 additions & 0 deletions frontend/app/[locale]/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Separator } from '@/components/ui/separator';
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/lib/session';
import SiteHeader from '@/components/layout/site-header';
import { mainNavConfig } from '@/config';
import { DashBoardSidebarNav } from '@/components/dashboard/sidebar-nav';

const sidebarNavItems = [
{
title: 'Images',
href: '/images',
},
{
title: 'Settings',
href: '/settings',
},
{
title: 'AI Search',
href: '/',
is_target_blank: true,
},
{
title: 'AI Image ',
href: '/generate-image',
is_target_blank: true,
},
{
title: 'AI Page',
href: 'https://pagegen.ai',
is_target_blank: true,
},
];

export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
if (!user?.id) {
redirect('/login');
}
return (
<>
<SiteHeader user={user} items={mainNavConfig.mainNav} />
<div className="container px-4 mx-auto max-w-8xl py-10">
<div className="space-y-6 md:flex md:space-y-0 md:space-x-6">
<aside className="md:w-1/6">
<DashBoardSidebarNav items={sidebarNavItems} />
</aside>
<Separator orientation="vertical" className="hidden md:block" />
<div className="flex-1">{children}</div>
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DashboardShell } from '@/components/dashboard/shell';

export default function DashboardSettingsLoading() {
return (
<div className="group w-5/6 mx-auto overflow-auto peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px] my-10">
<div className="group w-5/6 mx-auto overflow-auto">
<DashboardShell>
<DashboardHeader heading="MemFree Settings" text="Manage account and website settings." />
<div className="grid gap-10">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ export const metadata = {

export default async function SettingsPage() {
const user = await getCurrentUser();

if (!user) {
redirect('/login');
}
const userSubscriptionPlan = await getUserSubscriptionPlan(user.id);

return (
<div className="group w-5/6 mx-auto overflow-auto peer-[[data-state=open]]:lg:pl-[300px] peer-[[data-state=open]]:xl:pl-[320px] my-10">
<div className="group w-5/6 mx-auto overflow-auto">
<DashboardShell>
<DashboardHeader heading="MemFree Settings" text="Manage account and website settings." />
<div className="grid gap-10">
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/[locale]/(marketing)/generate-image/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const metadata: Metadata = {
export default async function Page() {
return (
<div className="min-h-screen flex flex-col w-full items-center justify-center bg-gradient-to-b from-primary/[0.04] to-transparen">
<h1 className="text-2xl font-bold text-center pt-24 md:pt-32">Generate Image With AI</h1>
<h1 className="text-2xl font-bold text-center">Generate Image With AI</h1>
<div className="container mx-auto px-4 py-12">
<AIImageGenerator />
</div>
Expand Down
5 changes: 1 addition & 4 deletions frontend/app/[locale]/(marketing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getCurrentUser } from '@/lib/session';
import { Suspense } from 'react';
import SiteHeader from '@/components/layout/site-header';
import { mainNavConfig } from '@/config';
import { unstable_setRequestLocale } from 'next-intl/server';
Expand All @@ -9,9 +8,7 @@ export default async function MarketingLayout({ children, params: { locale } })
const user = await getCurrentUser();
return (
<div className="flex min-h-screen flex-col">
<Suspense fallback="...">
<SiteHeader user={user} items={mainNavConfig.mainNav} />
</Suspense>
<SiteHeader user={user} items={mainNavConfig.mainNav} />
<main className="flex-1">{children}</main>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/app/api/generate-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ export async function POST(req: NextRequest) {
const image = result.data?.images?.[0]?.url;
await saveImage({
id: generateId(),
title: prompt,
userId,
isPublic: false,
prompt,
prompt: newPrompt.replace(/^```\n/, ''),
createdAt: new Date(),
imageUrl: image,
});
Expand Down
81 changes: 81 additions & 0 deletions frontend/components/dashboard/image-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import Image from 'next/image';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Fullscreen, Link as LinkIcon, Download, Loader2 } from 'lucide-react';
import { GenImage } from '@/lib/types';
import React from 'react';
import Link from 'next/link';
import useCopyToClipboard from '@/hooks/use-copy-clipboard';
import { useDownloadImage } from '@/hooks/use-download-image';
import { toast } from 'sonner';

const ImageCard = ({ item, isPriority }: { item: GenImage; isPriority: boolean }) => {
const { copyToClipboard } = useCopyToClipboard();
const { downloadImage, isDownloading } = useDownloadImage();

const handleCopyToClipboard = async () => {
await copyToClipboard(item.imageUrl);
toast.success('Image url copied to clipboard');
};

return (
<Card className="group relative overflow-hidden rounded-xl border border-border/40 hover:border-primary/40 hover:shadow-xl transition-all duration-500 flex flex-col h-full bg-background/50 backdrop-blur-sm">
<div className="relative aspect-[16/9] overflow-hidden rounded-t-xl">
<Image
src={item.imageUrl}
alt={item.title}
fill
className="object-cover transform group-hover:scale-110 transition-transform duration-700 ease-out"
priority={isPriority}
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-background/50 to-transparent opacity-40 group-hover:opacity-60 transition-opacity duration-500" />
</div>

<div className="p-6 space-y-3 flex-1">
<div className="space-y-2">
<h3 className="font-semibold text-lg text-foreground group-hover:text-primary transition-colors duration-300">{item.title}</h3>
<p className="text-sm text-muted-foreground/90 line-clamp-2 group-hover:text-muted-foreground transition-colors duration-300">
{item.prompt}
</p>
</div>
</div>

<div className="px-2 pb-4">
<div className="grid grid-cols-3 gap-1">
<Link href={item.imageUrl} target="_blank">
<Button variant="outline" size="icon" className="text-xs w-full">
<Fullscreen className="w-4 h-4 mr-1" />
Preview
</Button>
</Link>
<Button variant="outline" className="text-xs" onClick={handleCopyToClipboard}>
<LinkIcon className="w-4 h-4 mr-1" />
Copy
</Button>
<Button
variant="outline"
className="text-xs"
onClick={() => downloadImage(item.imageUrl, `memfree-${item.title.substring(0, 50)}.png`)}
disabled={isDownloading}
>
{isDownloading ? (
<>
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="w-4 h-4 mr-1" />
Download
</>
)}
</Button>
</div>
</div>
</Card>
);
};

export default ImageCard;
53 changes: 53 additions & 0 deletions frontend/components/dashboard/image-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { Loader2 } from 'lucide-react';

import { useState } from 'react';
import { GenImage } from '@/lib/types';
import React from 'react';
import { type User } from 'next-auth';
import { getUserImages } from '@/lib/store/image';
import InfiniteScroll from '@/components/ui/infinite-scroll';
import ImageCard from '@/components/dashboard/image-card';

const limit = 20;
export const ImageList = ({ items, user }: { items: GenImage[]; user: User }) => {
const [loading, setLoading] = useState(false);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);

const next = async () => {
if (!user) {
setHasMore(false);
return;
}
setLoading(true);
const newSearches = await getUserImages(user.id, offset);
if (newSearches.length < limit) {
setHasMore(false);
}
setOffset((prev) => prev + limit);
setLoading(false);
};

return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items?.length > 0 ? (
items.map((item, index) => <ImageCard key={item.id} item={item} isPriority={index <= 2} />)
) : (
<div className="text-center py-8 text-gray-500">No items in this category</div>
)}
</div>

<InfiniteScroll hasMore={hasMore} isLoading={loading} next={next} threshold={1}>
{hasMore && (
<div className="flex justify-center my-4">
<Loader2 className="size-6 text-primary animate-spin mr-2" />
<span>Loading More</span>
</div>
)}
</InfiniteScroll>
</>
);
};
38 changes: 38 additions & 0 deletions frontend/components/dashboard/sidebar-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';

import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
is_target_blank?: boolean;
}[];
}

export function DashBoardSidebarNav({ className, items, ...props }: SidebarNavProps) {
const pathname = usePathname();

return (
<nav className={cn('flex space-x-2 md:flex-col md:space-x-0 md:space-y-1', className)} {...props}>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
target={item.is_target_blank ? '_blank' : undefined}
className={cn(
buttonVariants({ variant: 'ghost' }),
pathname === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-transparent hover:underline',
'justify-start',
)}
>
{item.title}
</Link>
))}
</nav>
);
}
2 changes: 1 addition & 1 deletion frontend/components/layout/changelog-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function ChangelogBanner() {
<p>
<span>MemFree Supports DeepSeek-V3 and Google Gemini-2.0 AI Models Now 🎉 </span>
</p>
<Link href="/changelog" prefetch={false} className="underline">
<Link href="/changelog" target="_black" className="underline">
Learn More
</Link>
</div>
Expand Down
12 changes: 10 additions & 2 deletions frontend/lib/store/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export async function getUserImages(userId: string, offset: number = 0, limit: n
rev: true,
});

if (imageIds.length === 0) {
return [];
}

for (const id of imageIds) {
pipeline.hgetall(id);
pipeline.hgetall(IMAGE_KEY + id);
}

const results = await pipeline.exec();
Expand All @@ -41,9 +45,13 @@ export async function getLatestPublicImages(offset: number = 0, limit: number =
rev: true,
});

if (imageIds.length === 0) {
return [];
}

const pipeline = redisDB.pipeline();
for (const id of imageIds) {
pipeline.hgetall(id);
pipeline.hgetall(IMAGE_KEY + id);
}

const results = await pipeline.exec();
Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/tools/improve-image-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function generatePrompt(query: string, showText: boolean, useCase:
}
try {
const prompt = format(PROMPT, query, useCase, showTextInstructions);
// console.log("generatePrompt", prompt);
// console.log('generatePrompt', prompt);
const { text } = await generateText({
model: getLLM(GPT_4o_MIMI),
prompt: prompt,
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface Search extends Record<string, any> {
export interface GenImage extends Record<string, any> {
id: string;
userId: string;
title: string;
prompt: string;
createdAt: Date;
isPublic: boolean;
Expand Down

0 comments on commit b99ecd7

Please sign in to comment.