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";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'standalone'
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "shakethefrog",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"next": "15.1.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
@@ -175,6 +176,15 @@
|
||||
"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": {
|
||||
"version": "0.19.1",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"next": "15.1.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.1.4"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^9",
|
||||
"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: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
keyframes: {
|
||||
shake: {
|
||||
'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: [],
|
||||
} satisfies Config;
|
||||
}
|
||||
export default config
|
||||
|
||||
Reference in New Issue
Block a user