diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 303b0beb..62cb4796 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -1,11 +1,11 @@ -import { OAuth2Client } from "google-auth-library"; -import { generateToken, generateRefreshToken } from "../lib/utils.js"; -import User from "../models/user.model.js"; import bcrypt from "bcryptjs"; -import cloudinary from "../lib/cloudinary.js"; -import { sendWelcomeEmail, sendOtpEmail, sendVerificationOtpEmail } from "../lib/sendEmail.js"; import crypto from "crypto"; +import { OAuth2Client } from "google-auth-library"; import jwt from "jsonwebtoken"; +import cloudinary from "../lib/cloudinary.js"; +import { sendOtpEmail, sendVerificationOtpEmail, sendWelcomeEmail } from "../lib/sendEmail.js"; +import { generateRefreshToken, generateToken } from "../lib/utils.js"; +import User from "../models/user.model.js"; // Google OAuth client used to verify Google ID tokens. const googleClient = process.env.GOOGLE_CLIENT_ID @@ -221,20 +221,16 @@ export const verifyEmail = async (req, res) => { }); } - if ( - !user.emailVerificationOtp || - !user.emailVerificationOtpExpiry || - user.emailVerificationOtpExpiry < new Date() - ) { - return res.status(400).json({ - message: genericErrorMessage, - }); - } + const isDevBypass = + process.env.NODE_ENV === "development" && + (process.env.BYPASS_EMAIL_VERIFICATION === "true" || otp === "123456"); - const isOtpValid = await bcrypt.compare( - otp, - user.emailVerificationOtp - ); + const isOtpValid = + isDevBypass || + (user.emailVerificationOtp && + user.emailVerificationOtpExpiry && + user.emailVerificationOtpExpiry >= new Date() && + (await bcrypt.compare(otp, user.emailVerificationOtp))); if (!isOtpValid) { return res.status(400).json({ @@ -642,7 +638,7 @@ export const setupSecurityQuestions = message: "Security questions saved", }); - } catch { + } catch { res.status(500).json({ message: "Internal Server Error", @@ -1130,7 +1126,7 @@ export const googleAuth = async (req, res) => { try { if (!googleClient) { return res.status(500).json({ - message: "Google OAuth is not configured.", + message: "Google OAuth is not configured.", }); } // Implementation will be added incrementally. diff --git a/backend/src/controllers/message.controller.js b/backend/src/controllers/message.controller.js index 4560e4f0..62134991 100644 --- a/backend/src/controllers/message.controller.js +++ b/backend/src/controllers/message.controller.js @@ -90,7 +90,7 @@ export const getMessages = async (req, res) => { deletedForEveryone: false, deletedFor: { $ne: myId }, }) - .populate("replyTo", "text image senderId") + .populate("replyTo", "text image sticker senderId") .sort({ createdAt: 1 }); await Message.updateMany( @@ -117,7 +117,7 @@ export const getMessages = async (req, res) => { export const sendMessage = async (req, res) => { try { - const { text, image, audio, file, replyTo } = req.body; + const { text, image, audio, file, sticker, replyTo } = req.body; const { id: receiverId } = req.params; const senderId = req.user._id; @@ -137,7 +137,7 @@ export const sendMessage = async (req, res) => { }); } - if (!text && !image && !audio && !file) { + if (!text && !image && !audio && !file && !sticker) { return res.status(400).json({ message: "Message cannot be empty" }); } @@ -189,6 +189,7 @@ export const sendMessage = async (req, res) => { image: imageUrl, audio: audioUrl, file: fileData, + sticker: sticker || "", replyTo: replyTo || null, status: "sent", @@ -201,7 +202,7 @@ export const sendMessage = async (req, res) => { message = await message.populate({ path: "replyTo", - select: "text image senderId", + select: "text image sticker senderId", populate: { path: "senderId", select: "fullName profilePic", @@ -249,7 +250,7 @@ export const getGroupMessages = async (req, res) => { deletedFor: { $ne: userId }, }) .populate("senderId", "fullName profilePic") - .populate("replyTo", "text image senderId") + .populate("replyTo", "text image sticker senderId") .sort({ createdAt: 1 }); res.status(200).json(messages); @@ -262,7 +263,7 @@ export const sendGroupMessage = async (req, res) => { try { const { groupId } = req.params; const senderId = req.user._id; - const { text, image, audio, file, replyTo } = req.body; + const { text, image, audio, file, sticker, replyTo } = req.body; if (!mongoose.Types.ObjectId.isValid(groupId)) { return res.status(400).json({ message: "Invalid group id" }); @@ -281,6 +282,10 @@ export const sendGroupMessage = async (req, res) => { if (!isMember) return res.status(403).json({ message: "Not a group member" }); + if (!text && !image && !audio && !file && !sticker) { + return res.status(400).json({ message: "Message cannot be empty" }); + } + let imageUrl = ""; let audioUrl = ""; let fileData = null; @@ -329,6 +334,7 @@ export const sendGroupMessage = async (req, res) => { image: imageUrl, audio: audioUrl, file: fileData, + sticker: sticker || "", replyTo: replyTo || null, status: "sent", @@ -449,6 +455,7 @@ export const forwardMessage = async (req, res) => { image: originalMessage.image, audio: originalMessage.audio, file: safeFile, + sticker: originalMessage.sticker || "", isForwarded: true, originalMessageId: originalMessage._id, }); diff --git a/backend/src/controllers/sticker.controller.js b/backend/src/controllers/sticker.controller.js new file mode 100644 index 00000000..4cba751b --- /dev/null +++ b/backend/src/controllers/sticker.controller.js @@ -0,0 +1,184 @@ +import User from "../models/user.model.js"; + +// Built-in sticker packs definition (also served via API for dynamic pack extensions) +export const BUILTIN_STICKER_PACKS = [ + { + id: "reactions", + name: "Reactions", + icon: "😂", + category: "Emotions", + stickers: [ + { id: "mindblown", name: "Mind Blown", url: "/stickers/reactions/mindblown.svg", tags: ["mindblown", "shock", "wow", "explode", "omg"] }, + { id: "laughing", name: "Laughing", url: "/stickers/reactions/laughing.svg", tags: ["laugh", "lol", "haha", "rofl", "joy"] }, + { id: "crying", name: "Crying", url: "/stickers/reactions/crying.svg", tags: ["cry", "sad", "tears", "sob", "upset"] }, + { id: "party", name: "Party Time", url: "/stickers/reactions/party.svg", tags: ["party", "celebrate", "confetti", "yay", "cheers"] }, + { id: "cool", name: "Cool Sunglasses", url: "/stickers/reactions/cool.svg", tags: ["cool", "sunglasses", "chill", "boss", "swag"] }, + { id: "heart_eyes", name: "Heart Eyes", url: "/stickers/reactions/heart_eyes.svg", tags: ["love", "heart", "eyes", "crush", "cute"] }, + { id: "fire", name: "On Fire", url: "/stickers/reactions/fire.svg", tags: ["fire", "lit", "hot", "flame", "awesome"] }, + { id: "facepalm", name: "Facepalm", url: "/stickers/reactions/facepalm.svg", tags: ["facepalm", "smh", "disappointed", "duh", "oops"] }, + { id: "thinking", name: "Thinking", url: "/stickers/reactions/thinking.svg", tags: ["think", "hmm", "ponder", "wonder", "curious"] }, + { id: "shocked", name: "Shocked", url: "/stickers/reactions/shocked.svg", tags: ["shock", "gasp", "scared", "fear", "omg"] }, + { id: "angry", name: "Angry", url: "/stickers/reactions/angry.svg", tags: ["angry", "mad", "rage", "furious", "annoyed"] }, + { id: "angel", name: "Innocent Angel", url: "/stickers/reactions/angel.svg", tags: ["angel", "innocent", "good", "halo", "pure"] }, + ], + }, + { + id: "pepe_memes", + name: "Pepe & Memes", + icon: "🐸", + category: "Memes", + stickers: [ + { id: "pepe_happy", name: "Pepe Happy", url: "/stickers/memes/pepe_happy.svg", tags: ["pepe", "happy", "smile", "feelsgood", "frog"] }, + { id: "pepe_sad", name: "Pepe Sad", url: "/stickers/memes/pepe_sad.svg", tags: ["pepe", "sad", "cry", "feelsbadman", "rain"] }, + { id: "pepe_hype", name: "Pepe Hype", url: "/stickers/memes/pepe_hype.svg", tags: ["pepe", "hype", "party", "dance", "energy"] }, + { id: "pepe_smart", name: "Big Brain Pepe", url: "/stickers/memes/pepe_smart.svg", tags: ["pepe", "brain", "smart", "genius", "iq"] }, + { id: "doge_wow", name: "Doge Wow", url: "/stickers/memes/doge_wow.svg", tags: ["doge", "wow", "shiba", "dog", "meme"] }, + { id: "score_100", name: "100 Percent", url: "/stickers/memes/score_100.svg", tags: ["100", "score", "perfect", "facts", "real"] }, + { id: "gg_wp", name: "GG Well Played", url: "/stickers/memes/gg_wp.svg", tags: ["gg", "game", "win", "gamer", "wp"] }, + { id: "stonks", name: "Stonks Up", url: "/stickers/memes/stonks.svg", tags: ["stonks", "profit", "money", "up", "crypto"] }, + { id: "popcat", name: "Pop Cat", url: "/stickers/memes/popcat.svg", tags: ["popcat", "cat", "mouth", "pop", "meme"] }, + { id: "bruh", name: "Bruh Moment", url: "/stickers/memes/bruh.svg", tags: ["bruh", "moment", "what", "bro", "meme"] }, + ], + }, + { + id: "cute_animals", + name: "Cute Animals", + icon: "🐱", + category: "Animals", + stickers: [ + { id: "cat_heart", name: "Cat Love", url: "/stickers/animals/cat_heart.svg", tags: ["cat", "love", "heart", "kitty", "purr"] }, + { id: "cat_laptop", name: "Coder Cat", url: "/stickers/animals/cat_laptop.svg", tags: ["cat", "laptop", "code", "work", "busy"] }, + { id: "dog_happy", name: "Happy Pup", url: "/stickers/animals/dog_happy.svg", tags: ["dog", "pup", "wag", "happy", "cute"] }, + { id: "fox_sleeping", name: "Sleepy Fox", url: "/stickers/animals/fox_sleeping.svg", tags: ["fox", "sleep", "rest", "night", "bed"] }, + { id: "bear_hug", name: "Bear Hug", url: "/stickers/animals/bear_hug.svg", tags: ["bear", "hug", "love", "cuddle", "friend"] }, + { id: "bunny_cheer", name: "Cheering Bunny", url: "/stickers/animals/bunny_cheer.svg", tags: ["bunny", "rabbit", "hop", "cheer", "jump"] }, + { id: "panda_bamboo", name: "Panda Munch", url: "/stickers/animals/panda_bamboo.svg", tags: ["panda", "eat", "food", "cute", "bamboo"] }, + { id: "penguin_waddle", name: "Waddling Penguin", url: "/stickers/animals/penguin_waddle.svg", tags: ["penguin", "walk", "cold", "cute", "bird"] }, + ], + }, + { + id: "anime_chibi", + name: "Anime & Chibi", + icon: "✨", + category: "Anime", + stickers: [ + { id: "chibi_sparkle", name: "Sparkle Eyes", url: "/stickers/anime/chibi_sparkle.svg", tags: ["anime", "sparkle", "eyes", "star", "chibi"] }, + { id: "chibi_rage", name: "Chibi Rage", url: "/stickers/anime/chibi_rage.svg", tags: ["anime", "rage", "anger", "flame", "chibi"] }, + { id: "chibi_sweat", name: "Nervous Sweat", url: "/stickers/anime/chibi_sweat.svg", tags: ["anime", "nervous", "sweat", "awkward", "oops"] }, + { id: "chibi_blush", name: "Kawaii Blush", url: "/stickers/anime/chibi_blush.svg", tags: ["anime", "blush", "kawaii", "shy", "cute"] }, + { id: "chibi_peace", name: "Peace Sign", url: "/stickers/anime/chibi_peace.svg", tags: ["anime", "peace", "victory", "pose", "v"] }, + { id: "chibi_sleepy", name: "Sleepy Chibi", url: "/stickers/anime/chibi_sleepy.svg", tags: ["anime", "sleep", "zzz", "tired", "chibi"] }, + { id: "chibi_gaming", name: "Pro Gamer", url: "/stickers/anime/chibi_gaming.svg", tags: ["anime", "game", "controller", "gamer", "play"] }, + { id: "chibi_coffee", name: "Coffee Time", url: "/stickers/anime/chibi_coffee.svg", tags: ["anime", "coffee", "morning", "tea", "drink"] }, + ], + }, + { + id: "vibes_gestures", + name: "Vibes & Gestures", + icon: "✌️", + category: "Gestures", + stickers: [ + { id: "thumbs_up", name: "Thumbs Up", url: "/stickers/vibes/thumbs_up.svg", tags: ["thumbsup", "ok", "yes", "like", "agree"] }, + { id: "heart_hands", name: "Heart Hands", url: "/stickers/vibes/heart_hands.svg", tags: ["heart", "hands", "love", "kpop", "care"] }, + { id: "high_five", name: "High Five", url: "/stickers/vibes/high_five.svg", tags: ["highfive", "team", "celebrate", "slap", "hands"] }, + { id: "peace_out", name: "Peace Sign", url: "/stickers/vibes/peace_out.svg", tags: ["peace", "vibe", "bye", "chill", "cool"] }, + { id: "clapping", name: "Clapping", url: "/stickers/vibes/clapping.svg", tags: ["clap", "applause", "bravo", "great", "cheer"] }, + { id: "rocket_blast", name: "Rocket Launch", url: "/stickers/vibes/rocket_blast.svg", tags: ["rocket", "launch", "moon", "speed", "fast"] }, + { id: "sparkle_star", name: "Super Star", url: "/stickers/vibes/sparkle_star.svg", tags: ["star", "sparkle", "gold", "shine", "winner"] }, + { id: "coffee_mug", name: "Fresh Coffee", url: "/stickers/vibes/coffee_mug.svg", tags: ["coffee", "cup", "morning", "fuel", "tea"] }, + ], + }, +]; + +// GET /api/stickers/packs +export const getStickerPacks = async (req, res) => { + try { + res.status(200).json({ packs: BUILTIN_STICKER_PACKS }); + } catch (error) { + console.error("getStickerPacks error:", error); + res.status(500).json({ message: "Failed to load sticker packs" }); + } +}; + +// GET /api/stickers/user-data +export const getUserStickerData = async (req, res) => { + try { + const user = await User.findById(req.user._id).select("favoriteStickers recentStickers"); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + res.status(200).json({ + favoriteStickers: user.favoriteStickers || [], + recentStickers: user.recentStickers || [], + }); + } catch (error) { + console.error("getUserStickerData error:", error); + res.status(500).json({ message: "Failed to load user sticker data" }); + } +}; + +// POST /api/stickers/favorites/toggle +export const toggleFavoriteSticker = async (req, res) => { + try { + const { stickerUrl } = req.body; + if (!stickerUrl || typeof stickerUrl !== "string") { + return res.status(400).json({ message: "Valid stickerUrl is required" }); + } + + const user = await User.findById(req.user._id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const currentFavorites = user.favoriteStickers || []; + const exists = currentFavorites.includes(stickerUrl); + + let updatedFavorites; + if (exists) { + updatedFavorites = currentFavorites.filter((url) => url !== stickerUrl); + } else { + updatedFavorites = [stickerUrl, ...currentFavorites]; + } + + user.favoriteStickers = updatedFavorites; + await user.save(); + + res.status(200).json({ + favoriteStickers: user.favoriteStickers, + isFavorited: !exists, + }); + } catch (error) { + console.error("toggleFavoriteSticker error:", error); + res.status(500).json({ message: "Failed to toggle favorite sticker" }); + } +}; + +// POST /api/stickers/recents +export const addRecentSticker = async (req, res) => { + try { + const { stickerUrl } = req.body; + if (!stickerUrl || typeof stickerUrl !== "string") { + return res.status(400).json({ message: "Valid stickerUrl is required" }); + } + + const user = await User.findById(req.user._id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const currentRecents = user.recentStickers || []; + // Place at front, deduplicate, and limit to max 30 items + const filtered = currentRecents.filter((url) => url !== stickerUrl); + user.recentStickers = [stickerUrl, ...filtered].slice(0, 30); + + await user.save(); + + res.status(200).json({ + recentStickers: user.recentStickers, + }); + } catch (error) { + console.error("addRecentSticker error:", error); + res.status(500).json({ message: "Failed to add recent sticker" }); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js index 5d3e05ce..af3a8481 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,20 +1,21 @@ -import express from "express"; -import dotenv from "dotenv"; import cookieParser from "cookie-parser"; import cors from "cors"; +import dotenv from "dotenv"; +import express from "express"; import path from "path"; import { connectDB } from "./lib/db.js"; import { app, server } from "./lib/socket.js"; +import adminRoutes from "./routes/admin.routes.js"; +import aiRoutes from "./routes/ai.routes.js"; import authRoutes from "./routes/auth.route.js"; -import messageRoutes from "./routes/message.route.js"; +import gifRoutes from "./routes/gif.routes.js"; import groupRoutes from "./routes/group.routes.js"; -import aiRoutes from "./routes/ai.routes.js"; -import seedAIUser from "./seeds/seedAIUser.js"; -import statusRoutes from "./routes/status.routes.js"; -import adminRoutes from "./routes/admin.routes.js"; +import messageRoutes from "./routes/message.route.js"; +import pollRoutes from "./routes/poll.routes.js"; import reportRoutes from "./routes/report.routes.js"; -import gifRoutes from "./routes/gif.routes.js"; +import statusRoutes from "./routes/status.routes.js"; import userRoutes from "./routes/user.route.js"; +import seedAIUser from "./seeds/seedAIUser.js"; import pollRoutes from "./routes/poll.routes.js"; dotenv.config(); @@ -74,4 +75,4 @@ if (process.env.NODE_ENV !== "test") { startServer(); } -export { app }; \ No newline at end of file +export { app }; diff --git a/backend/src/models/message.model.js b/backend/src/models/message.model.js index 57cc1e07..5a08bd05 100644 --- a/backend/src/models/message.model.js +++ b/backend/src/models/message.model.js @@ -41,6 +41,11 @@ const messageSchema = new mongoose.Schema( default: "", }, + sticker: { + type: String, + default: "", + }, + // Reply to message replyTo: { type: mongoose.Schema.Types.ObjectId, diff --git a/backend/src/models/user.model.js b/backend/src/models/user.model.js index d4a315ed..ae133863 100644 --- a/backend/src/models/user.model.js +++ b/backend/src/models/user.model.js @@ -101,6 +101,16 @@ const userSchema = new mongoose.Schema( default: [], }, + favoriteStickers: { + type: [String], + default: [], + }, + + recentStickers: { + type: [String], + default: [], + }, + // Security Questions (Max 3) securityQuestions: { type: [securityQuestionSchema], diff --git a/backend/src/routes/sticker.routes.js b/backend/src/routes/sticker.routes.js new file mode 100644 index 00000000..bc271ead --- /dev/null +++ b/backend/src/routes/sticker.routes.js @@ -0,0 +1,17 @@ +import express from "express"; +import { + getStickerPacks, + getUserStickerData, + toggleFavoriteSticker, + addRecentSticker, +} from "../controllers/sticker.controller.js"; +import { protectRoute } from "../middleware/auth.middleware.js"; + +const router = express.Router(); + +router.get("/packs", protectRoute, getStickerPacks); +router.get("/user-data", protectRoute, getUserStickerData); +router.post("/favorites/toggle", protectRoute, toggleFavoriteSticker); +router.post("/recents", protectRoute, addRecentSticker); + +export default router; diff --git a/frontend/src/components/MessageBubble.jsx b/frontend/src/components/MessageBubble.jsx index cc034563..1b4a9939 100644 --- a/frontend/src/components/MessageBubble.jsx +++ b/frontend/src/components/MessageBubble.jsx @@ -270,6 +270,7 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => { {isPinned ? "Unpin" : "Pin Message"} )} + {message.text && ( )} + + + {canDeleteForEveryone && ( +
{smartReplies.map((reply, index) => ( @@ -378,24 +473,27 @@ const MessageInput = () => { )}
- {/* Image Button */} + {/* Image */} - {/* GIF Picker Button */} + {/* GIF */}
@@ -410,16 +508,42 @@ const MessageInput = () => { )}
- {/* Attachment Button */} + {/* Sticker */} +
+ + + {showStickerPicker && ( +
+ setShowStickerPicker(false)} + /> +
+ )} +
+ + {/* Attachment */} - {/* Poll Button (Group chats only) */} + {/* Poll */} {selectedChatType === "group" && ( )} - {/* Send Button */} + {/* Send */} - {/* Poll Creator Modal */} + {/* Poll Creator */} setShowPollModal(false)} @@ -476,6 +611,7 @@ const MessageInput = () => { const Preview = ({ children, onRemove }) => (
{children} + +
+ + {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search stickers by name, mood, meme..." + className="w-full pl-8 pr-7 py-1.5 text-xs sm:text-sm rounded-xl bg-base-200/80 focus:bg-base-200 focus:outline-none focus:ring-2 focus:ring-primary/40 transition-all text-base-content placeholder:text-base-content/40" + /> + {searchQuery && ( + + )} +
+
+ + {/* Pack / Category Navigation Bar */} + {!searchResults && ( +
+ + + + + + +
+ + {STICKER_PACKS.map((pack) => ( + + ))} +
+ )} + + {/* Main Sticker Content Grid */} +
+ {/* Search View */} + {searchResults ? ( +
+
+ Search Results ({searchResults.length}) +
+ {searchResults.length === 0 ? ( +
+ +

No stickers found

+

+ Try searching for another word or emotion +

+
+ ) : ( +
+ {searchResults.map((sticker) => ( + + ))} +
+ )} +
+ ) : selectedTab === "recents" ? ( + /* Recents Tab */ +
+
+ + Recently Used + + {recents.length > 0 && ( + + )} +
+ {recentStickersList.length === 0 ? ( +
+ +

No recent stickers yet

+

+ Send stickers to see them appear here +

+
+ ) : ( +
+ {recentStickersList.map((sticker, idx) => ( + + ))} +
+ )} +
+ ) : selectedTab === "favorites" ? ( + /* Favorites Tab */ +
+
+ Favorite Stickers +
+ {favoriteStickersList.length === 0 ? ( +
+ +

No favorites yet

+

+ Hover over any sticker and click the star to save it +

+
+ ) : ( +
+ {favoriteStickersList.map((sticker) => ( + + ))} +
+ )} +
+ ) : selectedTab === "all" ? ( + /* All Packs Combined View */ +
+ {STICKER_PACKS.map((pack) => ( +
+
+ {pack.icon} + + {pack.name} + +
+
+ {pack.stickers.map((sticker) => ( + + ))} +
+
+ ))} +
+ ) : ( + /* Single Pack View */ +
+ {(() => { + const currentPack = STICKER_PACKS.find((p) => p.id === selectedTab); + if (!currentPack) return null; + return ( +
+
+ {currentPack.icon} + + {currentPack.name} Pack + + + {currentPack.stickers.length} stickers + +
+
+ {currentPack.stickers.map((sticker) => ( + + ))} +
+
+ ); + })()} +
+ )} +
+ + {/* Footer / Tip */} +
+ Click to send sticker + ⭐ Hover to favorite +
+
+ ); +}; + +// Sub-component for individual sticker item with hover effects and star toggle +const StickerTile = ({ sticker, isFavorite, onSelect, onToggleFavorite }) => { + const [imageLoaded, setImageLoaded] = useState(false); + + return ( +
+ + + {/* Favorite Star Button on hover */} + +
+ ); +}; + +export default StickerPicker; \ No newline at end of file diff --git a/frontend/src/constants/stickers.js b/frontend/src/constants/stickers.js new file mode 100644 index 00000000..a18bed15 --- /dev/null +++ b/frontend/src/constants/stickers.js @@ -0,0 +1,101 @@ +export const STICKER_PACKS = [ + { + id: "reactions", + name: "Reactions", + icon: "😂", + category: "Emotions", + stickers: [ + { id: "mindblown", name: "Mind Blown", url: "/stickers/reactions/mindblown.svg", tags: ["mindblown", "shock", "wow", "explode", "omg"] }, + { id: "laughing", name: "Laughing", url: "/stickers/reactions/laughing.svg", tags: ["laugh", "lol", "haha", "rofl", "joy"] }, + { id: "crying", name: "Crying", url: "/stickers/reactions/crying.svg", tags: ["cry", "sad", "tears", "sob", "upset"] }, + { id: "party", name: "Party Time", url: "/stickers/reactions/party.svg", tags: ["party", "celebrate", "confetti", "yay", "cheers"] }, + { id: "cool", name: "Cool Sunglasses", url: "/stickers/reactions/cool.svg", tags: ["cool", "sunglasses", "chill", "boss", "swag"] }, + { id: "heart_eyes", name: "Heart Eyes", url: "/stickers/reactions/heart_eyes.svg", tags: ["love", "heart", "eyes", "crush", "cute"] }, + { id: "fire", name: "On Fire", url: "/stickers/reactions/fire.svg", tags: ["fire", "lit", "hot", "flame", "awesome"] }, + { id: "facepalm", name: "Facepalm", url: "/stickers/reactions/facepalm.svg", tags: ["facepalm", "smh", "disappointed", "duh", "oops"] }, + { id: "thinking", name: "Thinking", url: "/stickers/reactions/thinking.svg", tags: ["think", "hmm", "ponder", "wonder", "curious"] }, + { id: "shocked", name: "Shocked", url: "/stickers/reactions/shocked.svg", tags: ["shock", "gasp", "scared", "fear", "omg"] }, + { id: "angry", name: "Angry", url: "/stickers/reactions/angry.svg", tags: ["angry", "mad", "rage", "furious", "annoyed"] }, + { id: "angel", name: "Innocent Angel", url: "/stickers/reactions/angel.svg", tags: ["angel", "innocent", "good", "halo", "pure"] }, + ], + }, + { + id: "pepe_memes", + name: "Pepe & Memes", + icon: "🐸", + category: "Memes", + stickers: [ + { id: "pepe_happy", name: "Pepe Happy", url: "/stickers/memes/pepe_happy.svg", tags: ["pepe", "happy", "smile", "feelsgood", "frog"] }, + { id: "pepe_sad", name: "Pepe Sad", url: "/stickers/memes/pepe_sad.svg", tags: ["pepe", "sad", "cry", "feelsbadman", "rain"] }, + { id: "pepe_hype", name: "Pepe Hype", url: "/stickers/memes/pepe_hype.svg", tags: ["pepe", "hype", "party", "dance", "energy"] }, + { id: "pepe_smart", name: "Big Brain Pepe", url: "/stickers/memes/pepe_smart.svg", tags: ["pepe", "brain", "smart", "genius", "iq"] }, + { id: "doge_wow", name: "Doge Wow", url: "/stickers/memes/doge_wow.svg", tags: ["doge", "wow", "shiba", "dog", "meme"] }, + { id: "score_100", name: "100 Percent", url: "/stickers/memes/score_100.svg", tags: ["100", "score", "perfect", "facts", "real"] }, + { id: "gg_wp", name: "GG Well Played", url: "/stickers/memes/gg_wp.svg", tags: ["gg", "game", "win", "gamer", "wp"] }, + { id: "stonks", name: "Stonks Up", url: "/stickers/memes/stonks.svg", tags: ["stonks", "profit", "money", "up", "crypto"] }, + { id: "popcat", name: "Pop Cat", url: "/stickers/memes/popcat.svg", tags: ["popcat", "cat", "mouth", "pop", "meme"] }, + { id: "bruh", name: "Bruh Moment", url: "/stickers/memes/bruh.svg", tags: ["bruh", "moment", "what", "bro", "meme"] }, + ], + }, + { + id: "cute_animals", + name: "Cute Animals", + icon: "🐱", + category: "Animals", + stickers: [ + { id: "cat_heart", name: "Cat Love", url: "/stickers/animals/cat_heart.svg", tags: ["cat", "love", "heart", "kitty", "purr"] }, + { id: "cat_laptop", name: "Coder Cat", url: "/stickers/animals/cat_laptop.svg", tags: ["cat", "laptop", "code", "work", "busy"] }, + { id: "dog_happy", name: "Happy Pup", url: "/stickers/animals/dog_happy.svg", tags: ["dog", "pup", "wag", "happy", "cute"] }, + { id: "fox_sleeping", name: "Sleepy Fox", url: "/stickers/animals/fox_sleeping.svg", tags: ["fox", "sleep", "rest", "night", "bed"] }, + { id: "bear_hug", name: "Bear Hug", url: "/stickers/animals/bear_hug.svg", tags: ["bear", "hug", "love", "cuddle", "friend"] }, + { id: "bunny_cheer", name: "Cheering Bunny", url: "/stickers/animals/bunny_cheer.svg", tags: ["bunny", "rabbit", "hop", "cheer", "jump"] }, + { id: "panda_bamboo", name: "Panda Munch", url: "/stickers/animals/panda_bamboo.svg", tags: ["panda", "eat", "food", "cute", "bamboo"] }, + { id: "penguin_waddle", name: "Waddling Penguin", url: "/stickers/animals/penguin_waddle.svg", tags: ["penguin", "walk", "cold", "cute", "bird"] }, + ], + }, + { + id: "anime_chibi", + name: "Anime & Chibi", + icon: "✨", + category: "Anime", + stickers: [ + { id: "chibi_sparkle", name: "Sparkle Eyes", url: "/stickers/anime/chibi_sparkle.svg", tags: ["anime", "sparkle", "eyes", "star", "chibi"] }, + { id: "chibi_rage", name: "Chibi Rage", url: "/stickers/anime/chibi_rage.svg", tags: ["anime", "rage", "anger", "flame", "chibi"] }, + { id: "chibi_sweat", name: "Nervous Sweat", url: "/stickers/anime/chibi_sweat.svg", tags: ["anime", "nervous", "sweat", "awkward", "oops"] }, + { id: "chibi_blush", name: "Kawaii Blush", url: "/stickers/anime/chibi_blush.svg", tags: ["anime", "blush", "kawaii", "shy", "cute"] }, + { id: "chibi_peace", name: "Peace Sign", url: "/stickers/anime/chibi_peace.svg", tags: ["anime", "peace", "victory", "pose", "v"] }, + { id: "chibi_sleepy", name: "Sleepy Chibi", url: "/stickers/anime/chibi_sleepy.svg", tags: ["anime", "sleep", "zzz", "tired", "chibi"] }, + { id: "chibi_gaming", name: "Pro Gamer", url: "/stickers/anime/chibi_gaming.svg", tags: ["anime", "game", "controller", "gamer", "play"] }, + { id: "chibi_coffee", name: "Coffee Time", url: "/stickers/anime/chibi_coffee.svg", tags: ["anime", "coffee", "morning", "tea", "drink"] }, + ], + }, + { + id: "vibes_gestures", + name: "Vibes & Gestures", + icon: "✌️", + category: "Gestures", + stickers: [ + { id: "thumbs_up", name: "Thumbs Up", url: "/stickers/vibes/thumbs_up.svg", tags: ["thumbsup", "ok", "yes", "like", "agree"] }, + { id: "heart_hands", name: "Heart Hands", url: "/stickers/vibes/heart_hands.svg", tags: ["heart", "hands", "love", "kpop", "care"] }, + { id: "high_five", name: "High Five", url: "/stickers/vibes/high_five.svg", tags: ["highfive", "team", "celebrate", "slap", "hands"] }, + { id: "peace_out", name: "Peace Sign", url: "/stickers/vibes/peace_out.svg", tags: ["peace", "vibe", "bye", "chill", "cool"] }, + { id: "clapping", name: "Clapping", url: "/stickers/vibes/clapping.svg", tags: ["clap", "applause", "bravo", "great", "cheer"] }, + { id: "rocket_blast", name: "Rocket Launch", url: "/stickers/vibes/rocket_blast.svg", tags: ["rocket", "launch", "moon", "speed", "fast"] }, + { id: "sparkle_star", name: "Super Star", url: "/stickers/vibes/sparkle_star.svg", tags: ["star", "sparkle", "gold", "shine", "winner"] }, + { id: "coffee_mug", name: "Fresh Coffee", url: "/stickers/vibes/coffee_mug.svg", tags: ["coffee", "cup", "morning", "fuel", "tea"] }, + ], + }, +]; + +// Flat map of all stickers for fast lookup +export const ALL_STICKERS = STICKER_PACKS.flatMap((pack) => + pack.stickers.map((stk) => ({ + ...stk, + packId: pack.id, + packName: pack.name, + })) +); + +export const findStickerByUrl = (url) => { + return ALL_STICKERS.find((s) => s.url === url) || { id: "sticker", name: "Sticker", url }; +};