Initial commit: Kit landing page
- Next.js 16 with Turbopack - React 19 - Tailwind CSS 4 with CSS-first config - Framer Motion animations - Animated logo and hero section - Tool cards for Vert and Paint - Glassmorphism effects and gradient animations - Fully responsive design - Docker support with Nginx - Static export ready 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env*.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
*.pem
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# Install pnpm and dependencies
|
||||
RUN corepack enable pnpm && pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy built static files from builder
|
||||
COPY --from=builder /app/out /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
129
README.md
Normal file
129
README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Kit Landing Page
|
||||
|
||||
A stylish, animated landing page for [kit.pivoine.art](https://kit.pivoine.art) - your creative toolkit.
|
||||
|
||||
## Features
|
||||
|
||||
- ✨ **Animated UI** - Smooth animations with Framer Motion
|
||||
- 🎨 **Modern Design** - Glassmorphism effects, gradients, and animated backgrounds
|
||||
- 📱 **Responsive** - Mobile-first design that works on all devices
|
||||
- ⚡ **Fast** - Static export with Next.js 16 and Turbopack for optimal performance
|
||||
- 🎯 **SEO Optimized** - Proper meta tags and semantic HTML
|
||||
- 🚀 **Production Ready** - Docker support with Nginx
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Next.js 16** - React framework with App Router and Turbopack
|
||||
- **React 19** - Latest React with modern features
|
||||
- **Tailwind CSS 4** - Utility-first CSS with CSS-first configuration
|
||||
- **Framer Motion** - Professional animation library
|
||||
- **TypeScript 5** - Type-safe development
|
||||
- **ESLint 9** - Latest linting with flat config
|
||||
- **pnpm** - Fast, efficient package manager
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm (via corepack)
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run development server
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Preview production build locally
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Visit [http://localhost:3000](http://localhost:3000) to see the site.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
Build and run with Docker:
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t kit-landing .
|
||||
|
||||
# Run the container
|
||||
docker run -p 80:80 kit-landing
|
||||
```
|
||||
|
||||
Or with docker-compose (see `/home/valknar/Projects/docker-compose/kit/compose.yaml`).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── app/
|
||||
│ ├── layout.tsx # Root layout with metadata
|
||||
│ ├── page.tsx # Home page
|
||||
│ └── globals.css # Global styles and utilities
|
||||
├── components/
|
||||
│ ├── AnimatedBackground.tsx # Animated gradient background
|
||||
│ ├── Hero.tsx # Hero section with logo
|
||||
│ ├── Logo.tsx # Animated SVG logo
|
||||
│ ├── ToolCard.tsx # Tool card component
|
||||
│ ├── ToolsGrid.tsx # Grid of available tools
|
||||
│ └── Footer.tsx # Footer component
|
||||
├── public/ # Static assets
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── nginx.conf # Nginx configuration
|
||||
└── next.config.ts # Next.js configuration
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
Edit `components/ToolsGrid.tsx` and add a new tool object to the `tools` array:
|
||||
|
||||
```typescript
|
||||
{
|
||||
title: 'Tool Name',
|
||||
description: 'Tool description',
|
||||
url: 'https://tool.kit.pivoine.art',
|
||||
gradient: 'gradient-purple-blue', // or 'gradient-cyan-purple'
|
||||
icon: (
|
||||
// Your SVG icon here
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
Tailwind CSS 4 uses a new CSS-first configuration approach:
|
||||
|
||||
- **Theme customization**: Edit the `@theme` block in `app/globals.css`
|
||||
- **Custom utilities**: Add `@utility` blocks in `app/globals.css`
|
||||
- **Animations**: Define keyframes directly in the `@theme` block
|
||||
- **Colors & fonts**: Configure via CSS custom properties in `@theme`
|
||||
|
||||
## Available Tools
|
||||
|
||||
- **Vert** - Minimalist pastebin for code snippets
|
||||
- **Paint** - Browser-based image editor
|
||||
|
||||
## Performance
|
||||
|
||||
- Static export for fast loading
|
||||
- Optimized images and assets
|
||||
- Gzip compression via Nginx
|
||||
- Proper caching headers
|
||||
|
||||
## License
|
||||
|
||||
Private project - All rights reserved.
|
||||
|
||||
## Author
|
||||
|
||||
Created for [pivoine.art](https://pivoine.art)
|
||||
70
app/globals.css
Normal file
70
app/globals.css
Normal file
@@ -0,0 +1,70 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "../components/*.{js,ts,jsx,tsx}";
|
||||
@source "../components/ui/*.{js,ts,jsx,tsx}";
|
||||
@source "*.{js,ts,jsx,tsx}";
|
||||
|
||||
@theme {
|
||||
--color-background: #0a0a0f;
|
||||
--color-foreground: #ffffff;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Custom animations */
|
||||
--animate-gradient: gradient 8s linear infinite;
|
||||
--animate-float: float 3s ease-in-out infinite;
|
||||
--animate-glow: glow 2s ease-in-out infinite alternate;
|
||||
|
||||
@keyframes gradient {
|
||||
0%, 100% {
|
||||
background-size: 200% 200%;
|
||||
background-position: left center;
|
||||
}
|
||||
50% {
|
||||
background-size: 200% 200%;
|
||||
background-position: right center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from {
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 30px rgba(139, 92, 246, 0.6), 0 0 40px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--color-foreground);
|
||||
background: var(--color-background);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@utility text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@utility glass {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@utility gradient-purple-blue {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
@utility gradient-cyan-purple {
|
||||
background: linear-gradient(135deg, #2dd4bf 0%, #8b5cf6 100%);
|
||||
}
|
||||
22
app/layout.tsx
Normal file
22
app/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Kit - Your Creative Toolkit',
|
||||
description: 'A curated collection of creative and utility tools for developers and creators',
|
||||
keywords: ['tools', 'utilities', 'pastebin', 'paint', 'creative toolkit'],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
15
app/page.tsx
Normal file
15
app/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import AnimatedBackground from '@/components/AnimatedBackground';
|
||||
import Hero from '@/components/Hero';
|
||||
import ToolsGrid from '@/components/ToolsGrid';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="relative min-h-screen">
|
||||
<AnimatedBackground />
|
||||
<Hero />
|
||||
<ToolsGrid />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
components/AnimatedBackground.tsx
Normal file
34
components/AnimatedBackground.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
export default function AnimatedBackground() {
|
||||
return (
|
||||
<div className="fixed inset-0 -z-10 overflow-hidden">
|
||||
{/* Animated gradient background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-50"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #667eea 100%)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'gradient 15s ease infinite',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Grid pattern overlay */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '50px 50px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Floating orbs */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-float" />
|
||||
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-cyan-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-float" style={{ animationDelay: '2s' }} />
|
||||
<div className="absolute bottom-1/4 left-1/3 w-96 h-96 bg-pink-500 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-float" style={{ animationDelay: '4s' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
components/Footer.tsx
Normal file
43
components/Footer.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="relative py-12 px-4 border-t border-gray-800">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
className="text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-cyan-400">
|
||||
Kit
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex justify-center gap-8 mb-6 text-sm">
|
||||
<a
|
||||
href="https://pivoine.art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-purple-400 transition-colors"
|
||||
>
|
||||
pivoine.art
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<p className="text-gray-500 text-sm">
|
||||
© {new Date().getFullYear()} Kit. Built with Next.js & Tailwind CSS.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
70
components/Hero.tsx
Normal file
70
components/Hero.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import Logo from './Logo';
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 py-20">
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
{/* Logo */}
|
||||
<motion.div
|
||||
className="mb-8 flex justify-center"
|
||||
initial={{ opacity: 0, y: -50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<Logo size={160} />
|
||||
</motion.div>
|
||||
|
||||
{/* Main heading */}
|
||||
<motion.h1
|
||||
className="text-6xl md:text-8xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
Kit
|
||||
</motion.h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<motion.p
|
||||
className="text-xl md:text-2xl text-gray-300 mb-4 max-w-2xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
>
|
||||
Your Creative Toolkit
|
||||
</motion.p>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
className="text-base md:text-lg text-gray-400 mb-12 max-w-xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
>
|
||||
A curated collection of creative and utility tools for developers and creators.
|
||||
Simple, powerful, and always at your fingertips.
|
||||
</motion.p>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 1 }}
|
||||
>
|
||||
<span className="text-sm text-gray-500">Explore Tools</span>
|
||||
<motion.div
|
||||
className="w-6 h-10 border-2 border-gray-600 rounded-full p-1"
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
<div className="w-1 h-2 bg-gradient-to-b from-purple-400 to-cyan-400 rounded-full mx-auto" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
119
components/Logo.tsx
Normal file
119
components/Logo.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function Logo({ className = '', size = 120 }: { className?: string; size?: number }) {
|
||||
return (
|
||||
<motion.svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 200 200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
initial={{ opacity: 0, scale: 0.5, rotate: -180 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
transition={{ duration: 1, ease: 'easeOut' }}
|
||||
>
|
||||
{/* Toolbox base */}
|
||||
<motion.rect
|
||||
x="40"
|
||||
y="80"
|
||||
width="120"
|
||||
height="80"
|
||||
rx="8"
|
||||
stroke="url(#gradient1)"
|
||||
strokeWidth="4"
|
||||
fill="rgba(139, 92, 246, 0.1)"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.5, ease: 'easeInOut' }}
|
||||
/>
|
||||
|
||||
{/* Toolbox handle */}
|
||||
<motion.path
|
||||
d="M 80 80 Q 100 40, 120 80"
|
||||
stroke="url(#gradient1)"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.2, delay: 0.3, ease: 'easeInOut' }}
|
||||
/>
|
||||
|
||||
{/* Wrench */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
>
|
||||
<path
|
||||
d="M 70 110 L 70 130"
|
||||
stroke="url(#gradient2)"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="70" cy="105" r="6" fill="url(#gradient2)" />
|
||||
</motion.g>
|
||||
|
||||
{/* Pencil */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 1 }}
|
||||
>
|
||||
<path
|
||||
d="M 100 110 L 100 135"
|
||||
stroke="url(#gradient3)"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M 95 110 L 100 105 L 105 110"
|
||||
fill="url(#gradient3)"
|
||||
/>
|
||||
</motion.g>
|
||||
|
||||
{/* Brush */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 1.2 }}
|
||||
>
|
||||
<rect
|
||||
x="127"
|
||||
y="110"
|
||||
width="6"
|
||||
height="20"
|
||||
fill="url(#gradient4)"
|
||||
rx="1"
|
||||
/>
|
||||
<path
|
||||
d="M 125 110 L 130 105 L 135 110"
|
||||
fill="url(#gradient4)"
|
||||
/>
|
||||
</motion.g>
|
||||
|
||||
{/* Gradient definitions */}
|
||||
<defs>
|
||||
<linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#8b5cf6" />
|
||||
<stop offset="100%" stopColor="#6366f1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradient2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#2dd4bf" />
|
||||
<stop offset="100%" stopColor="#06b6d4" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradient3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#f59e0b" />
|
||||
<stop offset="100%" stopColor="#ef4444" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradient4" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#10b981" />
|
||||
<stop offset="100%" stopColor="#14b8a6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
83
components/ToolCard.tsx
Normal file
83
components/ToolCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ToolCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
url: string;
|
||||
gradient: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export default function ToolCard({ title, description, icon, url, gradient, index }: ToolCardProps) {
|
||||
return (
|
||||
<motion.a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative block"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -10 }}
|
||||
>
|
||||
<div className="glass relative overflow-hidden rounded-2xl p-8 h-full transition-all duration-300 hover:shadow-2xl">
|
||||
{/* Gradient overlay on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-300 ${gradient}`}
|
||||
/>
|
||||
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl -z-10">
|
||||
<div className={`w-full h-full ${gradient}`} />
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="mb-6 flex justify-center"
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
>
|
||||
<div className={`p-4 rounded-xl ${gradient}`}>
|
||||
{icon}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-2xl font-bold mb-3 text-white group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-purple-400 group-hover:to-cyan-400 transition-all duration-300">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-400 group-hover:text-gray-300 transition-colors duration-300">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Arrow icon */}
|
||||
<motion.div
|
||||
className="absolute bottom-8 right-8 text-gray-600 group-hover:text-purple-400"
|
||||
initial={{ x: 0 }}
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.a>
|
||||
);
|
||||
}
|
||||
81
components/ToolsGrid.tsx
Normal file
81
components/ToolsGrid.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import ToolCard from './ToolCard';
|
||||
|
||||
const tools = [
|
||||
{
|
||||
title: 'Vert',
|
||||
description: 'Privacy-focused file converter that processes images, audio, and documents locally on your device. No file size limits, completely open source.',
|
||||
url: 'https://vert.kit.pivoine.art',
|
||||
gradient: 'gradient-purple-blue',
|
||||
icon: (
|
||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Paint',
|
||||
description: 'An advanced image editor running in your browser. Edit photos, create graphics, and more.',
|
||||
url: 'https://paint.kit.pivoine.art',
|
||||
gradient: 'gradient-cyan-purple',
|
||||
icon: (
|
||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ToolsGrid() {
|
||||
return (
|
||||
<section className="relative py-20 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Section heading */}
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-cyan-400">
|
||||
Available Tools
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
Explore our collection of carefully crafted tools designed to boost your productivity and creativity.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Tools grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{tools.map((tool, index) => (
|
||||
<ToolCard
|
||||
key={tool.title}
|
||||
title={tool.title}
|
||||
description={tool.description}
|
||||
icon={tool.icon}
|
||||
url={tool.url}
|
||||
gradient={tool.gradient}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Coming soon hint */}
|
||||
<motion.div
|
||||
className="mt-16 text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
<p className="text-gray-500 text-sm">
|
||||
More tools coming soon...
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
16
eslint.config.mjs
Normal file
16
eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
10
next.config.ts
Normal file
10
next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'export',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
56
nginx.conf
Normal file
56
nginx.conf
Normal file
@@ -0,0 +1,56 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# Performance
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "kit.pivoine.art",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.11.17",
|
||||
"next": "^16.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
4067
pnpm-lock.yaml
generated
Normal file
4067
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
9
tailwind.config.ts
Normal file
9
tailwind.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Tailwind CSS 4 configuration
|
||||
// Most configuration is now done in CSS using @theme
|
||||
export default {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
};
|
||||
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user