This commit is contained in:
HugeFrog24
2025-01-15 00:11:35 +01:00
parent ee25430eee
commit 128a06be30
18 changed files with 1312 additions and 16 deletions

35
.dockerignore Normal file
View 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
View 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"]

View 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>
);
}

View 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
View 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
View 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
View 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
View 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>
);
}

View 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
View 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

View File

@@ -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
View File

@@ -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",

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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
View 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

View File

@@ -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