Skip to content

Commit

Permalink
Add support of animations, add move sticker functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Mayurifag committed Sep 12, 2024
1 parent 523f7c6 commit 3a77bdc
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 182 deletions.
18 changes: 18 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"axios": "^1.7.7",
"lottie-react": "^2.4.0",
"next": "14.2.10",
"react": "^18",
"react-dom": "^18",
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/app/components/RenderSticker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import Lottie from 'lottie-react';

interface Sticker {
file_id: string;
is_animated: boolean;
is_video: boolean;
}

interface RenderStickerProps {
sticker: Sticker;
}

export function RenderSticker({ sticker }: RenderStickerProps) {
const [animationData, setAnimationData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
const fetchAnimationData = async () => {
if (sticker.is_animated && !animationData && !isLoading) {
setIsLoading(true);
try {
const response = await axios.get(`http://localhost:8000/api/sticker/${sticker.file_id}`);
setAnimationData(response.data);
} catch (error) {
console.error('Error fetching animation data:', error);
} finally {
setIsLoading(false);
}
}
};

fetchAnimationData();
}, [sticker, animationData, isLoading]);

const stickerUrl = `http://localhost:8000/api/sticker/${sticker.file_id}`;

if (sticker.is_animated) {
return animationData ? (
<Lottie
animationData={animationData}
loop
autoplay
className="w-full h-auto max-w-[300px] object-contain p-1"
/>
) : (
<div>...</div>
);
} else if (sticker.is_video) {
return (
<video autoPlay loop muted playsInline className="w-full h-auto max-w-[300px] object-contain p-1">
<source src={stickerUrl} type="video/webm" />
Your browser does not support the video tag.
</video>
);
} else {
return <img src={stickerUrl} className="w-full h-auto max-w-[300px] object-contain p-1" />;
}
}
2 changes: 2 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
:root {
--background: #ffffff;
--foreground: #171717;
--modal: #f5f5f5;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--modal: #171717;
}
}

Expand Down
14 changes: 7 additions & 7 deletions frontend/src/app/manage-stickers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import axios from 'axios';
import Link from 'next/link';

interface StickerSet {
set_name: string;
name: string;
title: string;
user_id: number;
}
Expand All @@ -26,10 +26,10 @@ export default function ManageStickers() {
}
};

const deleteStickerSet = async (setName: string) => {
if (confirm(`Are you sure you want to delete the sticker set "${setName}"?`)) {
const deleteStickerSet = async (name: string) => {
if (confirm(`Are you sure you want to delete the sticker set "${name}"?`)) {
try {
await axios.post('http://localhost:8000/api/delete_sticker_set', { set_name: setName });
await axios.post('http://localhost:8000/api/delete_sticker_set', { name: name });
fetchStickerSets(); // Refresh the list after deletion
} catch (error) {
console.error('Error deleting sticker set:', error);
Expand All @@ -55,13 +55,13 @@ export default function ManageStickers() {
<div key={userId}>
<h2 className="text-xl font-bold">User_id: {userId}</h2>
{sets.map((set) => (
<div key={set.set_name} className="border rounded-lg p-4 flex justify-between items-center">
<div key={set.name} className="border rounded-lg p-4 flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">{set.title}</h3>
<p className="text-sm text-gray-500">{set.set_name}</p>
<p className="text-sm text-gray-500">{set.name}</p>
</div>
<button
onClick={() => deleteStickerSet(set.set_name)}
onClick={() => deleteStickerSet(set.name)}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Delete
Expand Down
29 changes: 5 additions & 24 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import axios from 'axios';
import Link from 'next/link';

interface StickerPack {
set_name: string;
name: string;
title: string;
user_id: number;
}

export default function Home() {
const [stickerPacks, setStickerPacks] = useState<StickerPack[]>([]);
const [loadedImages, setLoadedImages] = useState<string[]>([]);

useEffect(() => {
const fetchStickerPacks = async () => {
Expand All @@ -21,35 +21,16 @@ export default function Home() {
fetchStickerPacks();
}, []);

useEffect(() => {
const cachedImages = JSON.parse(localStorage.getItem('loadedImages') || '[]');
setLoadedImages(cachedImages);
}, []);

const handleImageLoad = (src: string) => {
if (!loadedImages.includes(src)) {
setLoadedImages((prev) => [...prev, src]);
localStorage.setItem('loadedImages', JSON.stringify([...loadedImages, src]));
}
};

return (
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold my-4">Sticker Packs</h1>
<Link href="/manage-stickers" className="text-blue-500 hover:underline mb-4 inline-block">
Manage Sticker Sets
</Link>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{stickerPacks.map((pack) => (
<Link href={`/stickerpack/${pack.set_name}`} key={pack.set_name}>
<div className="border rounded-lg p-4 cursor-pointer hover:shadow-lg transition-shadow">
<img
src={`http://localhost:8000/api/stickerpack/${pack.set_name}/preview`}
alt={pack.title}
className="w-full h-40 object-cover mb-2"
onLoad={() => handleImageLoad(`http://localhost:8000/api/stickerpack/${pack.set_name}/preview`)}
style={{ display: loadedImages.includes(`http://localhost:8000/api/stickerpack/${pack.set_name}/preview`) ? 'block' : 'none' }}
/>
<Link href={`/stickerpack/${pack.name}`} key={pack.name}>
<div className="border rounded-lg p-6 cursor-pointer hover:shadow-2xl transition-shadow transform hover:scale-105 bg-modal">
<h2 className="text-lg font-semibold">{pack.title}</h2>
</div>
</Link>
Expand Down
175 changes: 175 additions & 0 deletions frontend/src/app/stickerpack/[name]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";

import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import axios from 'axios';
import { RenderSticker } from '@/app/components/RenderSticker';

interface Sticker {
file_id: string;
emoji: string;
is_animated: boolean;
is_video: boolean;
}

interface StickerPack {
name: string;
title: string;
user_id: number;
stickers: Sticker[];
}

export default function StickerPackDetail() {
const params = useParams();
const name = params.name as string;
const [stickerPack, setStickerPack] = useState<StickerPack | null>(null);
const [selectedStickers, setSelectedStickers] = useState<string[]>([]);
const [selectedStickerForMove, setSelectedStickerForMove] = useState<string | null>(null);
const [stickerPacksForMove, setStickerPacksForMove] = useState<StickerPack[]>([]);

useEffect(() => {
const fetchStickerPack = async () => {
if (name) {
try {
const response = await axios.get(`http://localhost:8000/api/stickerpack/${name}`);
setStickerPack(response.data);
} catch (error) {
console.error('Error fetching sticker pack:', error);
}
}
};
fetchStickerPack();
}, [name]);

const toggleSticker = (file_id: string) => {
setSelectedStickers((prev) =>
prev.includes(file_id)
? prev.filter((id) => id !== file_id)
: [...prev, file_id]
);
};

const deleteSelectedStickers = async () => {
try {
await axios.post('http://localhost:8000/api/delete_stickers', { file_ids: selectedStickers });
const response = await axios.get(`http://localhost:8000/api/stickerpack/${name}`);
setStickerPack(response.data);
setSelectedStickers([]);
} catch (error) {
console.error('Error deleting stickers:', error);
}
};

const cancelSelection = () => {
setSelectedStickers([]);
};

const handleContextMenu = async (event: React.MouseEvent, sticker: Sticker) => {
event.preventDefault();
setSelectedStickerForMove(sticker.file_id);
const response = await axios.get('http://localhost:8000/api/stickerpacks');
setStickerPacksForMove(response.data.filter((pack: StickerPack) => pack.name !== stickerPack?.name));
};

const handleMoveSticker = async (destinationPack: StickerPack) => {
if (selectedStickerForMove && stickerPack) {
try {
const response = await axios.post('http://localhost:8000/api/move_sticker', {
source_pack: stickerPack.name,
destination_pack: destinationPack.name,
file_id: selectedStickerForMove,
user_id: stickerPack.user_id
});
if (response.data.success) {
setSelectedStickerForMove(null);
setStickerPacksForMove([]);
const updatedPack = await axios.get(`http://localhost:8000/api/stickerpack/${stickerPack.name}`);
setStickerPack(updatedPack.data);
} else {
console.error(`Error moving sticker to pack: ${destinationPack.name}`, response.data.error);
}
} catch (error) {
console.error(`Error moving sticker to pack: ${destinationPack.name}`, error);
}
}
};

return (
<div className="container mx-auto px-4">
<div className="fixed top-0 left-0 right-0 bg-background shadow-md z-10 p-4 flex justify-between items-center" style={{ height: '60px' }}>
<div>
<Link href="/" className="text-blue-500 underline mb-4">Home</Link>
{stickerPack && (
<>
<span className="font-bold"> {stickerPack.title}</span>
<span> ({stickerPack.stickers.length} stickers) (User ID: {stickerPack.user_id ?? 'N/A'})</span>
</>
)}
</div>
<div>
{selectedStickers.length > 0 && (
<>
<button
className="bg-red-500 text-white px-4 py-2 rounded mr-2"
onClick={deleteSelectedStickers}
>
Delete Selected Stickers ({selectedStickers.length})
</button>
<button
className="bg-gray-300 text-black px-4 py-2 rounded"
onClick={cancelSelection}
>
Cancel Selection
</button>
</>
)}
</div>
</div>
<div className={`mt-16 ${selectedStickerForMove ? 'opacity-50' : ''}`}>
<div className="flex flex-wrap">
{stickerPack?.stickers.map((sticker) => (
<div key={sticker.file_id} className="flex flex-col items-center w-1/2 sm:w-1/4 md:w-1/5 lg:w-1/6 xl:w-1/7">
<div
className={`relative cursor-pointer`}
onClick={() => toggleSticker(sticker.file_id)}
onContextMenu={(event) => handleContextMenu(event, sticker)}
style={{
border: selectedStickers.includes(sticker.file_id) ? '3px solid red' : '3px solid transparent',
boxSizing: 'border-box'
}}
>
<RenderSticker sticker={sticker} />
</div>
<span className="mt-2">
{sticker.emoji}
</span>
</div>
))}
</div>
</div>
{selectedStickerForMove && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-modal p-4 rounded shadow">
<h2 className="text-xl font-bold mb-4">Move Sticker To:</h2>
{stickerPacksForMove.map((pack) => (
<button
key={pack.name}
className="block w-full bg-blue-400 text-white px-4 py-2 rounded mb-2"
onClick={() => handleMoveSticker(pack)}
>
{pack.title}
</button>
))}
<button
className="block w-full bg-gray-500 text-black mt-6 px-4 py-2 rounded"
onClick={() => setSelectedStickerForMove(null)}
>
Cancel
</button>
</div>
</div>
)}
</div>
);
}
Loading

0 comments on commit 3a77bdc

Please sign in to comment.