mirror of
https://github.com/HugeFrog24/shakethefrog.git
synced 2026-03-02 00:14:33 +00:00
initil
This commit is contained in:
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
.eslintrc*
|
||||||
|
.prettier*
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:18-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files from builder
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Set correct permissions
|
||||||
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
95
app/components/FloatingHearts.tsx
Normal file
95
app/components/FloatingHearts.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { HeartIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
|
interface Heart {
|
||||||
|
id: number;
|
||||||
|
angle: number;
|
||||||
|
speed: number;
|
||||||
|
startPosition: { x: number; y: number };
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FloatingHeartsProps {
|
||||||
|
intensity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloatingHearts({ intensity }: FloatingHeartsProps) {
|
||||||
|
const [hearts, setHearts] = useState<Heart[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (intensity <= 0) return;
|
||||||
|
|
||||||
|
// Number of hearts based on intensity
|
||||||
|
const numHearts = Math.min(Math.floor(intensity * 2), 50);
|
||||||
|
|
||||||
|
// Create waves of hearts
|
||||||
|
const waves = 4; // Number of waves
|
||||||
|
const heartsPerWave = Math.ceil(numHearts / waves);
|
||||||
|
const waveDelay = 200; // Delay between waves in ms
|
||||||
|
const timers: NodeJS.Timeout[] = [];
|
||||||
|
|
||||||
|
// Generate hearts in waves
|
||||||
|
for (let wave = 0; wave < waves; wave++) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const newHearts = Array.from({ length: heartsPerWave }, (_, i) => {
|
||||||
|
const totalIndex = wave * heartsPerWave + i;
|
||||||
|
return {
|
||||||
|
id: Date.now() + totalIndex,
|
||||||
|
// Distribute angles evenly within each wave
|
||||||
|
angle: Math.random() * 360, // Random angle for full radial distribution
|
||||||
|
speed: 0.8 + Math.random() * 0.4,
|
||||||
|
startPosition: {
|
||||||
|
x: Math.random() * 40 - 20,
|
||||||
|
y: Math.random() * 40 - 20,
|
||||||
|
},
|
||||||
|
scale: 0.8 + Math.random() * 0.4,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setHearts(prev => [...prev, ...newHearts]);
|
||||||
|
}, wave * waveDelay);
|
||||||
|
|
||||||
|
timers.push(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove hearts after animation completes
|
||||||
|
const cleanupTimer = setTimeout(() => {
|
||||||
|
setHearts(prev => prev.filter(heart => heart.id > Date.now() - 3500));
|
||||||
|
}, waves * waveDelay + 3500);
|
||||||
|
|
||||||
|
timers.push(cleanupTimer);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timers.forEach(timer => clearTimeout(timer));
|
||||||
|
};
|
||||||
|
}, [intensity]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 pointer-events-none -z-10">
|
||||||
|
{hearts.map((heart) => {
|
||||||
|
const style = {
|
||||||
|
'--angle': `${heart.angle}deg`,
|
||||||
|
'--speed': `${heart.speed}`,
|
||||||
|
'--start-x': `${heart.startPosition.x}px`,
|
||||||
|
'--start-y': `${heart.startPosition.y}px`,
|
||||||
|
'--scale': heart.scale,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={heart.id}
|
||||||
|
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 animate-float-heart"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<HeartIcon
|
||||||
|
className="w-16 h-16 text-pink-500 opacity-80 animate-fade-out"
|
||||||
|
style={{ transform: `scale(var(--scale))` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
app/components/ThemeToggle.tsx
Normal file
22
app/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from '../providers/ThemeProvider';
|
||||||
|
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { darkMode, toggleDarkMode } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleDarkMode}
|
||||||
|
className="fixed top-4 right-4 p-2 rounded-full bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
aria-label="Toggle dark mode"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<SunIcon className="w-6 h-6 text-yellow-500" />
|
||||||
|
) : (
|
||||||
|
<MoonIcon className="w-6 h-6 text-gray-900" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
app/globals.css
Normal file
52
app/globals.css
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode styles */
|
||||||
|
body {
|
||||||
|
background-color: rgb(240, 253, 244);
|
||||||
|
color: rgb(15, 23, 42);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
:is(.dark body) {
|
||||||
|
background-color: rgb(15, 23, 42);
|
||||||
|
color: rgb(248, 250, 252);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-5px) rotate(-5deg); }
|
||||||
|
75% { transform: translateX(5px) rotate(5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shake {
|
||||||
|
animation: shake 0.3s cubic-bezier(.36,.07,.19,.97) both 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-heart {
|
||||||
|
to {
|
||||||
|
transform: translate(
|
||||||
|
calc(var(--start-x) + (50vw * cos(var(--angle)))),
|
||||||
|
calc(var(--start-y) + (50vh * sin(var(--angle))))
|
||||||
|
) scale(var(--scale));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float-heart {
|
||||||
|
animation: float-heart calc(2s * var(--speed)) ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-out {
|
||||||
|
animation: fade-out 2s ease-out forwards;
|
||||||
|
}
|
||||||
34
app/hooks/useDarkMode.ts
Normal file
34
app/hooks/useDarkMode.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if user has a dark mode preference in localStorage
|
||||||
|
const isDark = localStorage.getItem('darkMode') === 'true';
|
||||||
|
// Check system preference if no localStorage value
|
||||||
|
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
setDarkMode(isDark ?? systemPrefersDark);
|
||||||
|
|
||||||
|
// Add listener for system theme changes
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
if (localStorage.getItem('darkMode') === null) {
|
||||||
|
setDarkMode(e.matches);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
localStorage.setItem('darkMode', (!darkMode).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
return { darkMode, toggleDarkMode };
|
||||||
|
}
|
||||||
32
app/layout.tsx
Normal file
32
app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import { ThemeProvider } from './providers/ThemeProvider'
|
||||||
|
import { ThemeToggle } from './components/ThemeToggle'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Shake the Frog',
|
||||||
|
description: 'A fun interactive frog that reacts to shaking!',
|
||||||
|
icons: {
|
||||||
|
icon: '/images/frog.svg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body className={`${inter.className} transition-colors`}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeToggle />
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
app/page.tsx
Normal file
141
app/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { FloatingHearts } from './components/FloatingHearts';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [isShaken, setIsShaken] = useState(false);
|
||||||
|
const [shakeIntensity, setShakeIntensity] = useState(0);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState(0);
|
||||||
|
const [motionPermission, setMotionPermission] = useState<PermissionState>('prompt');
|
||||||
|
const shakeThreshold = 15;
|
||||||
|
|
||||||
|
// Check if device motion is available and handle permissions
|
||||||
|
const requestMotionPermission = async () => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Check if device motion is available
|
||||||
|
if (!('DeviceMotionEvent' in window)) {
|
||||||
|
setMotionPermission('denied');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permission on iOS devices
|
||||||
|
if ('requestPermission' in DeviceMotionEvent) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - TypeScript doesn't know about requestPermission
|
||||||
|
const permission = await DeviceMotionEvent.requestPermission();
|
||||||
|
setMotionPermission(permission);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error requesting motion permission:', err);
|
||||||
|
setMotionPermission('denied');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Android or desktop - no permission needed
|
||||||
|
setMotionPermission('granted');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerShake = (intensity: number) => {
|
||||||
|
// Start shake animation
|
||||||
|
setIsShaken(true);
|
||||||
|
|
||||||
|
// Always reset shake after 500ms
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsShaken(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Trigger hearts with a shorter duration
|
||||||
|
setShakeIntensity(intensity);
|
||||||
|
setTimeout(() => setShakeIntensity(0), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
triggerShake(25);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMotion = (event: DeviceMotionEvent) => {
|
||||||
|
const acceleration = event.accelerationIncludingGravity;
|
||||||
|
if (!acceleration) return;
|
||||||
|
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const timeDiff = currentTime - lastUpdate;
|
||||||
|
|
||||||
|
if (timeDiff > 100) {
|
||||||
|
setLastUpdate(currentTime);
|
||||||
|
|
||||||
|
const speed = Math.abs(acceleration.x || 0) +
|
||||||
|
Math.abs(acceleration.y || 0) +
|
||||||
|
Math.abs(acceleration.z || 0);
|
||||||
|
|
||||||
|
if (speed > shakeThreshold) {
|
||||||
|
triggerShake(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add motion listener if permission is granted
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (motionPermission === 'granted' && 'DeviceMotionEvent' in window) {
|
||||||
|
window.addEventListener('devicemotion', handleMotion);
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (motionPermission === 'granted') {
|
||||||
|
window.removeEventListener('devicemotion', handleMotion);
|
||||||
|
}
|
||||||
|
window.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [lastUpdate, motionPermission, triggerShake]);
|
||||||
|
|
||||||
|
// Initial permission check
|
||||||
|
useEffect(() => {
|
||||||
|
requestMotionPermission();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
triggerShake(25);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex h-[100dvh] flex-col items-center justify-center p-4 bg-green-50 dark:bg-slate-900">
|
||||||
|
<div
|
||||||
|
className={`relative ${isShaken ? 'animate-shake' : ''} z-10`}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<FloatingHearts intensity={shakeIntensity} />
|
||||||
|
<Image
|
||||||
|
src={isShaken ? '/images/frog-shaken.svg' : '/images/frog.svg'}
|
||||||
|
alt="Frog"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-2">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-center max-w-[240px]">
|
||||||
|
{motionPermission === 'prompt' ? (
|
||||||
|
<button
|
||||||
|
onClick={requestMotionPermission}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Enable device shake
|
||||||
|
</button>
|
||||||
|
) : motionPermission === 'granted' ? (
|
||||||
|
"Shake your device, press spacebar, or click/tap frog!"
|
||||||
|
) : (
|
||||||
|
"Press spacebar or click/tap frog!"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
app/providers/ThemeProvider.tsx
Normal file
26
app/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { useDarkMode } from '../hooks/useDarkMode';
|
||||||
|
|
||||||
|
const ThemeContext = createContext({ darkMode: false, toggleDarkMode: () => {} });
|
||||||
|
|
||||||
|
export const useTheme = () => useContext(ThemeContext);
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { darkMode, toggleDarkMode } = useDarkMode();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: 'standalone'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "shakethefrog",
|
"name": "shakethefrog",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
@@ -175,6 +176,15 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -9,19 +9,20 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0"
|
||||||
"next": "15.1.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"@eslint/eslintrc": "^3"
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
315
public/images/frog-ai.ai
Normal file
315
public/images/frog-ai.ai
Normal file
File diff suppressed because one or more lines are too long
376
public/images/frog-shaken-ai.ai
Normal file
376
public/images/frog-shaken-ai.ai
Normal file
File diff suppressed because one or more lines are too long
45
public/images/frog-shaken.svg
Normal file
45
public/images/frog-shaken.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 397.22 397.63">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.1.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 142) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #020202;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: #010201;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st3 {
|
||||||
|
fill: #f6c3cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st4 {
|
||||||
|
fill: #8bc86e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st5 {
|
||||||
|
fill: #ff8bc5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="st4" d="M90.23,353.87C25.05,309.81-9.76,239.48,2.2,174.53c7.34-39.86,38.35-82.44,52.62-96.63C50.83,24.54,78.86.39,113.08,0c34-.39,56.61,37.64,57.52,39.22,5.94-1.71,17.26-4.44,25.67-5.06,14.36-1.06,22.38,2.17,30.81,4.36C240.68,13.79,266.1-1.21,291.41.72c33.53,2.56,62.71,34.48,62.19,74.6.61,6.45-1.18,12.64-5.37,18.57,9.32,9.34,49.63,51.8,48.75,114.53-.9,64.57-30,121.63-96.72,147.95-4,23.4-22.97,42.46-43.26,41.34-15.32-.84-27.84-13.99-34.52-29.34-7.82,1.17-50.16,3.59-57.23-.07-5.31,18.42-22.24,30.45-39.45,29.34-20.34-1.32-37.46-20.75-35.56-43.76Z"/>
|
||||||
|
<path class="st4" d="M286.71,365.14"/>
|
||||||
|
<path class="st1" d="M273.48,154.72c3.81-4.6,10.99-3.15,12.19,2.52.57,2.67-6.83,17.2-8.04,22.15-1.37,5.6-4.44,18.72,3.27,20.91,15.09,4.29,22.28-22.69,27.23-31.71,4.62-8.43,13.51-5.56,12.57,3.16-.91,8.47-13.54,29.58-20.33,35.13-18.08,14.78-39.03,3.63-37.3-19.81.5-6.74,6.33-27.41,10.41-32.35Z"/>
|
||||||
|
<path class="st1" d="M86.55,209.83c-4.41-3.1-20.83-18.8-21.96-23.3-1.26-4.99,2.76-9.6,7.87-8.12,2.94.85,12.97,13.54,16.66,16.72,6.18,5.32,13.89,11.75,22.31,7.79,8.19-3.85-.62-18.04-3.97-22.96-2.76-4.06-12.16-12.86-12.91-15.9-1.21-4.97,1.81-8.98,6.99-8.2s19.38,19.97,21.9,25.12c14.44,29.46-11.76,46.5-36.9,28.87Z"/>
|
||||||
|
<path class="st2" d="M108.1,37.45c34.78-11.3,33.1,44.08,3.28,44.78-24.51.58-24.56-37.87-3.28-44.78Z"/>
|
||||||
|
<path class="st2" d="M270.04,38.93c25.85-5.36,40.97,35.07,18.03,43.37-29.1,10.53-45.03-37.77-18.03-43.37Z"/>
|
||||||
|
<path class="st0" d="M224.44,85.16c11.21-.25,13.82-1.7,16.78-3.19,2.35.67,1.43,7.49.03,11.65-4.54,13.47-22.56,23.2-39.49,23.94-24.86,1.1-41.33-17.44-45.18-24.78-.97-1.85-3.07-5.85-1.37-8.65,1.34-2.2,4.6-2.94,7.08-2.62,4.98.65,11.24,2.83,14.52,3.13,7.83.71,10.66-4.2,23.35-3.8,9.87.32,17.29,3.96,24.29,4.32Z"/>
|
||||||
|
<path class="st3" d="M92.81,91.3c8.08-2.11,19.05-1.24,26.28,3.09,14.76,8.85,7.14,19.81-6.56,23.01-10.45,2.44-34-1.23-32.91-15.65.4-5.36,8.66-9.27,13.19-10.46Z"/>
|
||||||
|
<path class="st3" d="M307.58,106.42c1.45,18.18-49.88,20.63-47.5-.82.59-5.31,8.9-8.62,19.7-9.82,15.8-1.76,27.04,4.61,27.79,10.64Z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M106.35,36.3c-4.91,1.58-8.56,8.77-6.9,13.51,2.53,7.22,17.33,8.59,17.25,8.91-.09.36-17-6.74-22.71-.86-2.95,3.03-2.78,9.37,0,12.93,7.16,9.16,35.7,5.1,39.38-6.9,3.61-11.78-16.89-30.84-27.02-27.59Z"/>
|
||||||
|
<path d="M284.23,36.3c4.91,1.58,8.56,8.77,6.9,13.51-2.53,7.22-17.33,8.59-17.25,8.91.09.36,17-6.74,22.71-.86,2.95,3.03,2.78,9.37,0,12.93-7.16,9.16-35.7,5.1-39.38-6.9-3.61-11.78,16.89-30.84,27.02-27.59Z"/>
|
||||||
|
<path class="st5" d="M216.53,105.68c0,6.67-33.84,6.67-33.84,0s7.58-12.07,16.92-12.07,16.92,5.4,16.92,12.07Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
32
public/images/frog.svg
Normal file
32
public/images/frog.svg
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 397.22 397.63">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.1.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 142) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #020202;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: #010201;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st2 {
|
||||||
|
fill: #f6c3cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st3 {
|
||||||
|
fill: #8bc86e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="st3" d="M90.23,353.87C25.05,309.81-9.76,239.48,2.2,174.53c7.34-39.86,38.35-82.44,52.62-96.63C50.83,24.54,78.86.39,113.08,0c34-.39,56.61,37.64,57.52,39.22,5.94-1.71,17.26-4.44,25.67-5.06,14.36-1.06,22.38,2.17,30.81,4.36C240.68,13.79,266.1-1.21,291.41.72c33.53,2.56,62.71,34.48,62.19,74.6.61,6.45-1.18,12.64-5.37,18.57,9.32,9.34,49.63,51.8,48.75,114.53-.9,64.57-30,121.63-96.72,147.95-4,23.4-22.97,42.46-43.26,41.34-15.32-.84-27.84-13.99-34.52-29.34-7.82,1.17-50.16,3.59-57.23-.07-5.31,18.42-22.24,30.45-39.45,29.34-20.34-1.32-37.46-20.75-35.56-43.76Z"/>
|
||||||
|
<path class="st3" d="M286.71,365.14"/>
|
||||||
|
<path class="st1" d="M273.48,154.72c3.81-4.6,10.99-3.15,12.19,2.52.57,2.67-6.83,17.2-8.04,22.15-1.37,5.6-4.44,18.72,3.27,20.91,15.09,4.29,22.28-22.69,27.23-31.71,4.62-8.43,13.51-5.56,12.57,3.16-.91,8.47-13.54,29.58-20.33,35.13-18.08,14.78-39.03,3.63-37.3-19.81.5-6.74,6.33-27.41,10.41-32.35Z"/>
|
||||||
|
<path class="st1" d="M86.55,209.83c-4.41-3.1-20.83-18.8-21.96-23.3-1.26-4.99,2.76-9.6,7.87-8.12,2.94.85,12.97,13.54,16.66,16.72,6.18,5.32,13.89,11.75,22.31,7.79,8.19-3.85-.62-18.04-3.97-22.96-2.76-4.06-12.16-12.86-12.91-15.9-1.21-4.97,1.81-8.98,6.99-8.2s19.38,19.97,21.9,25.12c14.44,29.46-11.76,46.5-36.9,28.87Z"/>
|
||||||
|
<path d="M108.1,37.45c34.78-11.3,33.1,44.08,3.28,44.78-24.51.58-24.56-37.87-3.28-44.78Z"/>
|
||||||
|
<path d="M270.04,38.93c25.85-5.36,40.97,35.07,18.03,43.37-29.1,10.53-45.03-37.77-18.03-43.37Z"/>
|
||||||
|
<path class="st0" d="M187.14,80.8c.34-.24.23-1.74,1.91-2.49,7.35-3.26,11.41,4.6,18.66,7.11,4.63,1.6,11.46,2.66,16.05,1.08,4.92-1.69,9.48-9.25,14.68-2.09,4.95,6.81-5.49,12.78-11.38,14.2-8.65,2.09-15.66,1.93-23.96-1.05-2.37-.85-8.99-4.89-10.27-4.72-1.39.19-7.33,4.27-10.48,5.11-10.62,2.85-24.63,3.09-32.57-5.71-4.91-5.44-1.53-12.33,5.71-11.27,2.02.3,4.69,3.69,7.12,4.67,7.93,3.22,17.92-.21,24.54-4.85Z"/>
|
||||||
|
<path class="st2" d="M92.81,91.3c8.08-2.11,19.05-1.24,26.28,3.09,14.76,8.85,7.14,19.81-6.56,23.01-10.45,2.44-34-1.23-32.91-15.65.4-5.36,8.66-9.27,13.19-10.46Z"/>
|
||||||
|
<path class="st2" d="M307.58,106.42c1.45,18.18-49.88,20.63-47.5-.82.59-5.31,8.9-8.62,19.7-9.82,15.8-1.76,27.04,4.61,27.79,10.64Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,18 +1,42 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
export default {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
keyframes: {
|
||||||
background: "var(--background)",
|
shake: {
|
||||||
foreground: "var(--foreground)",
|
'0%, 100%': { transform: 'rotate(0deg)' },
|
||||||
|
'25%': { transform: 'rotate(-5deg)' },
|
||||||
|
'75%': { transform: 'rotate(5deg)' }
|
||||||
},
|
},
|
||||||
|
float: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translate(calc(-50% + var(--start-x)), calc(-50% + var(--start-y))) scale(var(--scale))',
|
||||||
|
opacity: '1'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translate(calc(-50% + var(--start-x) + (cos(var(--angle)) * 500px)), calc(-50% + var(--start-y) + (sin(var(--angle)) * 500px))) scale(var(--scale))',
|
||||||
|
opacity: '0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fadeOut: {
|
||||||
|
'0%': { opacity: '1' },
|
||||||
|
'100%': { opacity: '0' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'shake': 'shake 0.2s ease-in-out infinite',
|
||||||
|
'float-heart': 'float 2s cubic-bezier(0.2, 0, 0.8, 1) forwards',
|
||||||
|
'fade-out': 'fadeOut 2s ease-out forwards'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
} satisfies Config;
|
}
|
||||||
|
export default config
|
||||||
|
|||||||
Reference in New Issue
Block a user