Update
This commit is contained in:
@@ -1,152 +1,201 @@
|
||||
import { useState, useEffect, useLayoutEffect } from "react"
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import EditSidebar from "./partials/edit-sidebar"
|
||||
import EditorCanvas from "./partials/editor-canvas"
|
||||
import EditorHeader from "./partials/editor-header"
|
||||
import EditorControls from "./partials/editor-controls"
|
||||
import { calculateOptimalMaxWidth, calculateResponsiveWidth } from "./utils/layout-constants"
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import { Volume2Icon, VolumeOffIcon } from 'lucide-react';
|
||||
import EditNavSidebar from './partials/edit-nav-sidebar';
|
||||
import EditSidebar from './partials/edit-sidebar';
|
||||
import EditorCanvas from './partials/editor-canvas';
|
||||
import EditorControls from './partials/editor-controls';
|
||||
import EditorHeader from './partials/editor-header';
|
||||
import { calculateOptimalMaxWidth, calculateResponsiveWidth } from './utils/layout-constants';
|
||||
|
||||
// Hook to detect if viewport is below minimum width
|
||||
const useViewportDetection = (minWidth = 320) => {
|
||||
const [isBelowMinWidth, setIsBelowMinWidth] = useState(false);
|
||||
const [isBelowMinWidth, setIsBelowMinWidth] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setIsBelowMinWidth(window.innerWidth < minWidth);
|
||||
}, [minWidth]);
|
||||
useLayoutEffect(() => {
|
||||
setIsBelowMinWidth(window.innerWidth < minWidth);
|
||||
}, [minWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkViewport = () => {
|
||||
setIsBelowMinWidth(window.innerWidth < minWidth);
|
||||
};
|
||||
useEffect(() => {
|
||||
const checkViewport = () => {
|
||||
setIsBelowMinWidth(window.innerWidth < minWidth);
|
||||
};
|
||||
|
||||
checkViewport();
|
||||
window.addEventListener('resize', checkViewport);
|
||||
window.addEventListener('orientationchange', checkViewport);
|
||||
checkViewport();
|
||||
window.addEventListener('resize', checkViewport);
|
||||
window.addEventListener('orientationchange', checkViewport);
|
||||
|
||||
let resizeObserver;
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(checkViewport);
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
let resizeObserver;
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(checkViewport);
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkViewport);
|
||||
window.removeEventListener('orientationchange', checkViewport);
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
};
|
||||
}, [minWidth]);
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkViewport);
|
||||
window.removeEventListener('orientationchange', checkViewport);
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
};
|
||||
}, [minWidth]);
|
||||
|
||||
return isBelowMinWidth;
|
||||
return isBelowMinWidth;
|
||||
};
|
||||
|
||||
// Hook for responsive dimensions
|
||||
const useResponsiveDimensions = () => {
|
||||
const [dimensions, setDimensions] = useState(() => ({
|
||||
maxWidth: calculateOptimalMaxWidth(),
|
||||
responsiveWidth: calculateResponsiveWidth()
|
||||
}));
|
||||
const [dimensions, setDimensions] = useState(() => ({
|
||||
maxWidth: calculateOptimalMaxWidth(),
|
||||
responsiveWidth: calculateResponsiveWidth(),
|
||||
}));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const newMaxWidth = calculateOptimalMaxWidth();
|
||||
const newResponsiveWidth = calculateResponsiveWidth();
|
||||
setDimensions({
|
||||
maxWidth: newMaxWidth,
|
||||
responsiveWidth: newResponsiveWidth
|
||||
});
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
const newMaxWidth = calculateOptimalMaxWidth();
|
||||
const newResponsiveWidth = calculateResponsiveWidth();
|
||||
setDimensions({
|
||||
maxWidth: newMaxWidth,
|
||||
responsiveWidth: newResponsiveWidth,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const newMaxWidth = calculateOptimalMaxWidth();
|
||||
const newResponsiveWidth = calculateResponsiveWidth();
|
||||
setDimensions({
|
||||
maxWidth: newMaxWidth,
|
||||
responsiveWidth: newResponsiveWidth
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const newMaxWidth = calculateOptimalMaxWidth();
|
||||
const newResponsiveWidth = calculateResponsiveWidth();
|
||||
setDimensions({
|
||||
maxWidth: newMaxWidth,
|
||||
responsiveWidth: newResponsiveWidth,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
|
||||
let resizeObserver;
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
let resizeObserver;
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
|
||||
let mutationObserver;
|
||||
if (window.MutationObserver) {
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
setTimeout(handleResize, 50);
|
||||
});
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style']
|
||||
});
|
||||
}
|
||||
let mutationObserver;
|
||||
if (window.MutationObserver) {
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
setTimeout(handleResize, 50);
|
||||
});
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
if (mutationObserver) mutationObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
if (mutationObserver) mutationObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return dimensions;
|
||||
return dimensions;
|
||||
};
|
||||
|
||||
const Editor = () => {
|
||||
const [isEditSidebarOpen, setIsEditSidebarOpen] = useState(false)
|
||||
const { maxWidth, responsiveWidth } = useResponsiveDimensions();
|
||||
const isBelowMinWidth = useViewportDetection(320);
|
||||
const { getSetting } = useLocalSettingsStore();
|
||||
|
||||
const handleEditClick = () => {
|
||||
setIsEditSidebarOpen(!isEditSidebarOpen)
|
||||
}
|
||||
const [isEditNavSidebarOpen, setIsEditNavSidebarOpen] = useState(false);
|
||||
const [isEditSidebarOpen, setIsEditSidebarOpen] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(true); // Video starts muted by default
|
||||
const { maxWidth, responsiveWidth } = useResponsiveDimensions();
|
||||
const isBelowMinWidth = useViewportDetection(320);
|
||||
|
||||
const handleCloseSidebar = () => {
|
||||
setIsEditSidebarOpen(false)
|
||||
}
|
||||
const handleEditNavClick = () => {
|
||||
setIsEditNavSidebarOpen(!isEditNavSidebarOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx-auto min-h-screen flex flex-col relative space-y-2 py-4"
|
||||
style={{ width: `${responsiveWidth}px` }}
|
||||
>
|
||||
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleCloseSidebar} />
|
||||
const handleEditNavClose = () => {
|
||||
setIsEditNavSidebarOpen(false);
|
||||
};
|
||||
|
||||
<EditorHeader className="mx-auto" style={{ width: `${responsiveWidth}px` }} />
|
||||
const handleEditClick = () => {
|
||||
setIsEditSidebarOpen(!isEditSidebarOpen);
|
||||
};
|
||||
|
||||
{isBelowMinWidth ? (
|
||||
<div className="aspect-[9/16]">
|
||||
<div className="flex-1 flex items-center justify-center p-6 bg-white h-full rounded-lg border shadow-lg ">
|
||||
const handleEditClose = () => {
|
||||
setIsEditSidebarOpen(false);
|
||||
};
|
||||
|
||||
<div>
|
||||
<video className="mx-auto" width="100" height="100%" src="https://cdn.memeaigen.com/videos/cat%20asking%20for%20food.webm" autoPlay muted loop />
|
||||
<div className="text-center">
|
||||
<div className="text-xl
|
||||
font-bold">YIKES 🥔</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
You seem to be using a potato-sized screen. Please continue with desktop for a more refined experience!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<EditorCanvas maxWidth={maxWidth} />
|
||||
<EditorControls
|
||||
className="mx-auto"
|
||||
style={{ width: `${responsiveWidth}px` }}
|
||||
onEditClick={handleEditClick}
|
||||
isEditActive={isEditSidebarOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Toggle mute functionality
|
||||
const handleToggleMute = () => {
|
||||
setIsMuted(!isMuted);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-screen flex-col space-y-2 py-4" style={{ width: `${responsiveWidth}px` }}>
|
||||
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleEditClose} />
|
||||
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
|
||||
|
||||
<EditorHeader
|
||||
className="mx-auto"
|
||||
style={{ width: `${responsiveWidth}px` }}
|
||||
onNavClick={handleEditNavClick}
|
||||
isNavActive={isEditNavSidebarOpen}
|
||||
/>
|
||||
|
||||
{isBelowMinWidth ? (
|
||||
<div className="aspect-[9/16]">
|
||||
<div className="flex h-full flex-1 items-center justify-center rounded-lg border bg-white p-6 shadow-lg">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="relative">
|
||||
<video
|
||||
className="mx-auto"
|
||||
width="100"
|
||||
height="100%"
|
||||
src="https://cdn.memeaigen.com/videos/cat%20asking%20for%20food.webm"
|
||||
autoPlay
|
||||
muted={isMuted}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2 text-center">
|
||||
<div className="text-xl font-bold">{getSetting('genAlphaSlang') ? 'SHEESH' : 'YIKES'} 🥔</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
{getSetting('genAlphaSlang')
|
||||
? 'Not gonna lie, using on a potato screen is giving L vibes. Desktop hits different - that experience is straight fire, bet!'
|
||||
: 'You seem to be using a potato-sized screen. Please continue with desktop for a more refined experience!'}
|
||||
</p>
|
||||
<div class="text-muted-foreground text-xs italic">
|
||||
Note: You can turn {getSetting('genAlphaSlang') ? 'off' : 'on'} gen alpha slang in Settings.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-center">
|
||||
<button
|
||||
onClick={handleToggleMute}
|
||||
className="bg-opacity-50 hover:bg-opacity-70 mx-auto rounded-full bg-black p-2 text-white transition-opacity"
|
||||
title={isMuted ? 'Unmute video' : 'Mute video'}
|
||||
>
|
||||
{isMuted ? <VolumeOffIcon className="h-4 w-4" /> : <Volume2Icon className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<EditorCanvas maxWidth={maxWidth} />
|
||||
<EditorControls
|
||||
className="mx-auto"
|
||||
style={{ width: `${responsiveWidth}px` }}
|
||||
onEditClick={handleEditClick}
|
||||
isEditActive={isEditSidebarOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
59
resources/js/modules/editor/partials/edit-nav-sidebar.tsx
Normal file
59
resources/js/modules/editor/partials/edit-nav-sidebar.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
|
||||
interface EditNavSidebarProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EditNavSidebar({ isOpen, onClose }: EditNavSidebarProps) {
|
||||
const { getSetting, setSetting } = useLocalSettingsStore();
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent side="left" className="w-50 overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-3">
|
||||
<div className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">MEMEAIGEN</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="link">
|
||||
<SettingsIcon className="h-6 w-6" /> Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>Change your settings here.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="genAlphaSlang"
|
||||
checked={getSetting('genAlphaSlang')}
|
||||
onCheckedChange={() => setSetting('genAlphaSlang', !getSetting('genAlphaSlang'))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="genAlphaSlang"
|
||||
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Use gen alpha slang
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +1,133 @@
|
||||
import { Edit3, Plus, Coins } from "lucide-react"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Coins, Edit3, Plus } from 'lucide-react';
|
||||
|
||||
interface EditSidebarProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EditSidebar({ isOpen, onClose }: EditSidebarProps) {
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-3">
|
||||
<Edit3 className="h-6 w-6" />
|
||||
Edit Media
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-3">
|
||||
<Edit3 className="h-6 w-6" />
|
||||
Edit Media
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Background and Meme Selection */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border-2 border-gray-300 rounded-lg p-3 text-center">
|
||||
<div className="w-full h-16 bg-blue-600 rounded mb-2 overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=64&width=120"
|
||||
alt="Gaming background"
|
||||
width={120}
|
||||
height={64}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Background</span>
|
||||
</div>
|
||||
<div className="border-2 border-gray-300 rounded-lg p-3 text-center">
|
||||
<div className="w-full h-16 bg-gray-200 rounded mb-2 overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=64&width=120"
|
||||
alt="Meme character"
|
||||
width={120}
|
||||
height={64}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Meme</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Background and Meme Selection */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-lg border-2 border-gray-300 p-3 text-center">
|
||||
<div className="mb-2 h-16 w-full overflow-hidden rounded bg-blue-600">
|
||||
<img
|
||||
src="/placeholder.svg?height=64&width=120"
|
||||
alt="Gaming background"
|
||||
width={120}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Background</span>
|
||||
</div>
|
||||
<div className="rounded-lg border-2 border-gray-300 p-3 text-center">
|
||||
<div className="mb-2 h-16 w-full overflow-hidden rounded bg-gray-200">
|
||||
<img
|
||||
src="/placeholder.svg?height=64&width=120"
|
||||
alt="Meme character"
|
||||
width={120}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Meme</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Background Search */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Search for backgrounds using AI</h3>
|
||||
{/* AI Background Search */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Search for backgrounds using AI</h3>
|
||||
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center mb-4">
|
||||
<div className="w-12 h-12 border-2 border-gray-400 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Plus className="h-6 w-6" />
|
||||
</div>
|
||||
<p className="text-sm font-medium mb-2">Generate a background with AI</p>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-lg font-bold">1</span>
|
||||
<div className="w-5 h-5 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<Coins className="h-3 w-3 text-yellow-800" />
|
||||
<div className="mb-4 rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
|
||||
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full border-2 border-gray-400">
|
||||
<Plus className="h-6 w-6" />
|
||||
</div>
|
||||
<p className="mb-2 text-sm font-medium">Generate a background with AI</p>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-lg font-bold">1</span>
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-yellow-400">
|
||||
<Coins className="h-3 w-3 text-yellow-800" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meme Templates Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="aspect-square overflow-hidden rounded-lg bg-gray-100">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Creepy face meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative aspect-square overflow-hidden rounded-lg bg-gray-100">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Confused person meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 right-2 text-sm font-bold text-black">???</div>
|
||||
<div className="absolute bottom-2 left-2 text-sm font-bold text-black">???</div>
|
||||
</div>
|
||||
<div className="aspect-square overflow-hidden rounded-lg bg-gray-100">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Woody meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square overflow-hidden rounded-lg bg-gray-100">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Doge meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative aspect-square overflow-hidden rounded-lg bg-blue-900">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Stock market meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-900/50">
|
||||
<span className="text-xs text-white">📈 28%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="aspect-square overflow-hidden rounded-lg bg-yellow-100">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Room meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meme Templates Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Creepy face meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square bg-gray-100 rounded-lg overflow-hidden relative">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Confused person meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 right-2 text-black font-bold text-sm">???</div>
|
||||
<div className="absolute bottom-2 left-2 text-black font-bold text-sm">???</div>
|
||||
</div>
|
||||
<div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Woody meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Doge meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square bg-blue-900 rounded-lg overflow-hidden relative">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Stock market meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-blue-900/50 flex items-center justify-center">
|
||||
<span className="text-white text-xs">📈 28%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="aspect-square bg-yellow-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="/placeholder.svg?height=150&width=150"
|
||||
alt="Room meme"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,53 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import CoinIcon from "@/reusables/coin-icon"
|
||||
import { Menu, Coins } from "lucide-react"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import CoinIcon from '@/reusables/coin-icon';
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const EditorHeader = (
|
||||
{className = ''}
|
||||
) => {
|
||||
return (
|
||||
<div className={cn("bg-white rounded-xl p-2 flex items-center justify-between shadow-sm w-full", className)}>
|
||||
<Button variant="outline" size="icon" className="rounded">
|
||||
<Menu className="h-8 w-8" />
|
||||
</Button>
|
||||
const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = false }) => {
|
||||
const { getSetting } = useLocalSettingsStore();
|
||||
|
||||
<h1 className="text-lg md:text-xl font-display tracking-wide ml-0 md:ml-3">MEMEAIGEN</h1>
|
||||
const [openCoinDialog, setOpenCoinDialog] = useState(false);
|
||||
|
||||
<Button variant="outline" className="rounded inline-flex gap-1">
|
||||
<span className="text-sm font-semibold">100</span>
|
||||
<CoinIcon className="w-8 h-8" />
|
||||
</Button>
|
||||
return (
|
||||
<div className={cn('flex w-full items-center justify-between rounded-xl bg-white p-2 shadow-sm', className)}>
|
||||
<Button onClick={onNavClick} variant="outline" size="icon" className="rounded">
|
||||
<Menu className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<h1 className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">MEMEAIGEN</h1>
|
||||
|
||||
<Button variant="outline" className="inline-flex gap-1 rounded" onClick={() => setOpenCoinDialog(true)}>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
<CoinIcon className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
<Dialog open={openCoinDialog} onOpenChange={(open) => setOpenCoinDialog(open)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{getSetting('genAlphaSlang') ? 'Bruh' : 'Chill'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{getSetting('genAlphaSlang')
|
||||
? "No cap, soon you'll be able to get AI cooking memes that absolutely slay! But lowkey fam, we gotta focus on making these core features bussin' first."
|
||||
: "Soon you'll be able to prompt AI to generate memes! Let us focus on nailing the core features first."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between gap-1">
|
||||
<span class="text-muted-foreground text-xs italic">
|
||||
Note: You can turn {getSetting('genAlphaSlang') ? 'off' : 'on'} gen alpha slang in Settings.
|
||||
</span>
|
||||
<Button variant="outline" onClick={() => setOpenCoinDialog(false)}>
|
||||
{getSetting('genAlphaSlang') ? 'Bet' : 'Okay'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorHeader;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Editor from "@/modules/editor/editor";
|
||||
import Editor from '@/modules/editor/editor';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div class="bg-neutral-50 min-h-screen">
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-50">
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CoinIconProps {
|
||||
className?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CoinIcon: React.FC<CoinIconProps> = ({ className }) => {
|
||||
return (
|
||||
<svg className={className} width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_23_2)">
|
||||
<path d="M100 200C44.8582 200 0 155.139 0 100C0 44.8605 44.8582 0 100 0C155.142 0 200 44.8605 200 100C200 155.139 155.142 200 100 200Z" fill="#FFE14D"/>
|
||||
<path d="M200 100C200 44.8605 155.142 0 100 0V200C155.142 200 200 155.139 200 100Z" fill="#FFCC33"/>
|
||||
<path d="M100 13.0435C52.0509 13.0435 13.0435 52.0533 13.0435 100C13.0435 147.947 52.0509 186.957 100 186.957C147.949 186.957 186.957 147.947 186.957 100C186.957 52.0533 147.949 13.0435 100 13.0435ZM100 173.913C59.2435 173.913 26.0872 140.756 26.0872 100C26.0872 59.2439 59.2435 26.0872 100 26.0872C140.757 26.0872 173.913 59.2435 173.913 100C173.913 140.757 140.757 173.913 100 173.913Z" fill="#FF9F19"/>
|
||||
<path d="M173.913 100C173.913 140.757 140.756 173.913 100 173.913V186.956C147.949 186.956 186.957 147.947 186.957 99.9997C186.957 52.0525 147.949 13.0435 100 13.0435V26.0868C140.757 26.0872 173.913 59.2435 173.913 100Z" fill="#F28618"/>
|
||||
<path d="M81.8546 60.7827C83.0432 60.7827 84.0092 61.037 84.7521 61.5454C85.5691 62.0537 86.3121 62.8885 86.9806 64.0503L97.0099 81.8071C97.3813 82.4607 97.6786 82.9336 97.9015 83.2241C98.1986 83.5143 98.5702 83.6596 99.0157 83.6596H99.5167V112.31H97.0099C95.5984 112.31 94.4092 112.056 93.4435 111.547C92.5522 110.966 91.7722 110.058 91.1036 108.824L84.5294 96.9497V135.296C84.5293 136.676 84.195 137.693 83.5265 138.346C82.9322 138.927 81.9292 139.217 80.5177 139.217H63.9142C62.5029 139.217 61.4626 138.927 60.7941 138.346C60.2 137.693 59.9035 136.676 59.9034 135.296V64.7046C59.9034 63.3249 60.2 62.3442 60.7941 61.7632C61.4626 61.1096 62.5029 60.7827 63.9142 60.7827H81.8546Z" fill="#FF9F19"/>
|
||||
<path d="M135.119 60.7827C136.53 60.7827 137.533 61.1096 138.127 61.7632C138.796 62.3442 139.13 63.3247 139.13 64.7046V135.296C139.13 136.676 138.796 137.693 138.127 138.346C137.533 138.927 136.53 139.217 135.119 139.217H117.958C116.547 139.217 115.507 138.927 114.838 138.346C114.244 137.693 113.947 136.676 113.947 135.296V96.9497L107.372 108.824C106.704 110.058 105.887 110.966 104.921 111.547C104.03 112.056 102.878 112.31 101.467 112.31H99.517V83.6596H100.018C100.463 83.6596 100.798 83.5143 101.021 83.2241C101.318 82.9336 101.652 82.4607 102.024 81.8071L111.942 64.0503C112.61 62.8885 113.316 62.0537 114.059 61.5454C114.876 61.0371 115.878 60.7827 117.067 60.7827H135.119Z" fill="#F28618"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_23_2">
|
||||
<rect width="200" height="200" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg className={className} width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_23_2)">
|
||||
<path
|
||||
d="M100 200C44.8582 200 0 155.139 0 100C0 44.8605 44.8582 0 100 0C155.142 0 200 44.8605 200 100C200 155.139 155.142 200 100 200Z"
|
||||
fill="#FFE14D"
|
||||
/>
|
||||
<path d="M200 100C200 44.8605 155.142 0 100 0V200C155.142 200 200 155.139 200 100Z" fill="#FFCC33" />
|
||||
<path
|
||||
d="M100 13.0435C52.0509 13.0435 13.0435 52.0533 13.0435 100C13.0435 147.947 52.0509 186.957 100 186.957C147.949 186.957 186.957 147.947 186.957 100C186.957 52.0533 147.949 13.0435 100 13.0435ZM100 173.913C59.2435 173.913 26.0872 140.756 26.0872 100C26.0872 59.2439 59.2435 26.0872 100 26.0872C140.757 26.0872 173.913 59.2435 173.913 100C173.913 140.757 140.757 173.913 100 173.913Z"
|
||||
fill="#FF9F19"
|
||||
/>
|
||||
<path
|
||||
d="M173.913 100C173.913 140.757 140.756 173.913 100 173.913V186.956C147.949 186.956 186.957 147.947 186.957 99.9997C186.957 52.0525 147.949 13.0435 100 13.0435V26.0868C140.757 26.0872 173.913 59.2435 173.913 100Z"
|
||||
fill="#F28618"
|
||||
/>
|
||||
<path
|
||||
d="M81.8546 60.7827C83.0432 60.7827 84.0092 61.037 84.7521 61.5454C85.5691 62.0537 86.3121 62.8885 86.9806 64.0503L97.0099 81.8071C97.3813 82.4607 97.6786 82.9336 97.9015 83.2241C98.1986 83.5143 98.5702 83.6596 99.0157 83.6596H99.5167V112.31H97.0099C95.5984 112.31 94.4092 112.056 93.4435 111.547C92.5522 110.966 91.7722 110.058 91.1036 108.824L84.5294 96.9497V135.296C84.5293 136.676 84.195 137.693 83.5265 138.346C82.9322 138.927 81.9292 139.217 80.5177 139.217H63.9142C62.5029 139.217 61.4626 138.927 60.7941 138.346C60.2 137.693 59.9035 136.676 59.9034 135.296V64.7046C59.9034 63.3249 60.2 62.3442 60.7941 61.7632C61.4626 61.1096 62.5029 60.7827 63.9142 60.7827H81.8546Z"
|
||||
fill="#FF9F19"
|
||||
/>
|
||||
<path
|
||||
d="M135.119 60.7827C136.53 60.7827 137.533 61.1096 138.127 61.7632C138.796 62.3442 139.13 63.3247 139.13 64.7046V135.296C139.13 136.676 138.796 137.693 138.127 138.346C137.533 138.927 136.53 139.217 135.119 139.217H117.958C116.547 139.217 115.507 138.927 114.838 138.346C114.244 137.693 113.947 136.676 113.947 135.296V96.9497L107.372 108.824C106.704 110.058 105.887 110.966 104.921 111.547C104.03 112.056 102.878 112.31 101.467 112.31H99.517V83.6596H100.018C100.463 83.6596 100.798 83.5143 101.021 83.2241C101.318 82.9336 101.652 82.4607 102.024 81.8071L111.942 64.0503C112.61 62.8885 113.316 62.0537 114.059 61.5454C114.876 61.0371 115.878 60.7827 117.067 60.7827H135.119Z"
|
||||
fill="#F28618"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_23_2">
|
||||
<rect width="200" height="200" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoinIcon;
|
||||
|
||||
80
resources/js/stores/localSettingsStore.ts
Normal file
80
resources/js/stores/localSettingsStore.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// Immutable default settings
|
||||
const defaultSettings = {
|
||||
genAlphaSlang: true,
|
||||
// Add more settings here
|
||||
};
|
||||
|
||||
const useLocalSettingsStore = create(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
settings: { ...defaultSettings }, // clone to avoid shared reference
|
||||
|
||||
// Get a setting by key
|
||||
getSetting: (key) => {
|
||||
const currentSettings = get().settings;
|
||||
return key in currentSettings ? currentSettings[key] : defaultSettings[key] ?? null;
|
||||
},
|
||||
|
||||
// Set or update a specific setting
|
||||
setSetting: (key, value) => {
|
||||
console.log(`Updating setting ${key} to`, value); // <-- Debug log
|
||||
set((state) => ({
|
||||
settings: {
|
||||
...state.settings,
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
// Reset all settings to default
|
||||
resetSettings: () => {
|
||||
set({ settings: { ...defaultSettings } }); // create new object reference
|
||||
},
|
||||
|
||||
// Reset a specific setting to its default
|
||||
resetSetting: (key) => {
|
||||
set((state) => ({
|
||||
settings: {
|
||||
...state.settings,
|
||||
[key]: defaultSettings[key],
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
// Merge default settings with current ones (useful on load)
|
||||
initializeSettings: () => {
|
||||
set((state) => ({
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
...state.settings,
|
||||
},
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'local-settings-storage',
|
||||
// Ensure only the 'settings' key is stored
|
||||
partialize: (state) => ({ settings: state.settings }),
|
||||
// Explicit localStorage usage (for compatibility)
|
||||
storage: {
|
||||
getItem: (name) => {
|
||||
const stored = localStorage.getItem(name);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
},
|
||||
setItem: (name, value) => {
|
||||
localStorage.setItem(name, JSON.stringify(value));
|
||||
},
|
||||
removeItem: (name) => {
|
||||
localStorage.removeItem(name);
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useLocalSettingsStore;
|
||||
Reference in New Issue
Block a user