# FILE: package.json
{
"name": "wallpaper-generator",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.462.0",
"next": "14.2.5",
"postcss": "^8.4.47",
"react": "18.3.1",
"react-dom": "18.3.1",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.4"
}
}
# FILE: next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: { appDir: true }
};
export default nextConfig;
# FILE: tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es2021"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": { "@/components/*": ["components/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
# FILE: postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
# FILE: tailwind.config.ts
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
background: "hsl(0 0% 100%)",
foreground: "hsl(240 10% 3.9%)",
muted: "hsl(240 4.8% 95.9%)",
},
borderRadius: {
xl: "1rem",
"2xl": "1.25rem",
},
},
},
plugins: [],
} satisfies Config;
# FILE: app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root { color-scheme: light; }
html, body, #__next { height: 100%; }
body { @apply bg-white text-neutral-900; }
# FILE: app/layout.tsx
export const metadata = {
title: "Wallpaper Generator",
description: "Create custom wallpapers with affirmations and effects.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
# FILE: app/page.tsx
"use client";
import React, { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import BeforeAfterPreview from "@/components/BeforeAfterPreview";
export default function Page() {
const [orig, setOrig] = useState(null);
const [edit, setEdit] = useState(null);
const [affirm, setAffirm] = useState("I am grounded and capable.");
return (
);
}
# FILE: components/BeforeAfterPreview.tsx
"use client";
/**
* Wallpaper Generator Before/After Preview
* Created by Alex Campbell, 2025
* Companionish AI Solutions
*/
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { RefreshCcw, Columns2, Rows } from "lucide-react";
function srcFrom(input?: string | File | Blob | null) {
if (!input) return "";
if (typeof input === "string") return input;
return URL.createObjectURL(input);
}
export default function BeforeAfterPreview({
original,
edited,
height = 520,
checker = true,
defaultLayout = "stack",
enableCompareSlider = true,
}: {
original?: string | File | Blob | null,
edited?: string | File | Blob | null,
height?: number,
checker?: boolean,
defaultLayout?: "stack" | "side";
enableCompareSlider?: boolean;
}) {
const [layout, setLayout] = useState<"stack" | "side">(defaultLayout);
const [fitMode, setFitMode] = useState<"contain" | "cover">("contain");
const [zoom, setZoom] = useState(1);
const [compare, setCompare] = useState(50);
const [showCompare, setShowCompare] = useState(enableCompareSlider);
const [spacePanning, setSpacePanning] = useState(false);
const [pan, setPan] = useState({ x: 0, y: 0 });
const wrapRef = useRef(null);
const isStack = layout === "stack";
const originalSrc = useMemo(() => srcFrom(original), [original]);
const editedSrc = useMemo(() => srcFrom(edited), [edited]);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.code === "Space") { e.preventDefault(); setSpacePanning(true); }
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "0") { setZoom(1); setPan({ x: 0, y: 0 }); }
if ((e.ctrlKey || e.metaKey) && e.key === "=") { setZoom((z) => Math.min(5, +(z + 0.1).toFixed(2))); }
if ((e.ctrlKey || e.metaKey) && e.key === "-") { setZoom((z) => Math.max(0.2, +(z - 0.1).toFixed(2))); }
if (e.key.toLowerCase() === "s") setShowCompare((v) => !v);
if (e.key.toLowerCase() === "l") setLayout((p) => (p === "stack" ? "side" : "stack"));
};
const onKeyUp = (e: KeyboardEvent) => { if (e.code === "Space") setSpacePanning(false); };
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
return () => { window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keyup", onKeyUp); };
}, []);
useEffect(() => {
const el = wrapRef.current; if (!el) return;
const onWheel = (e: WheelEvent) => {
if (e.ctrlKey) {
e.preventDefault();
const rect = el.getBoundingClientRect();
const delta = -e.deltaY * 0.0015;
setZoom((z) => {
const nz = Math.min(5, Math.max(0.2, +(z + delta).toFixed(3)));
const cx = e.clientX - rect.left - rect.width / 2;
const cy = e.clientY - rect.top - rect.height / 2;
setPan((p) => ({ x: p.x - cx * (nz - z) * 0.08, y: p.y - cy * (nz - z) * 0.08 }));
return nz;
});
}
};
el.addEventListener("wheel", onWheel, { passive: false });
return () => el.removeEventListener("wheel", onWheel as any);
}, []);
const onPointerDown: React.PointerEventHandler = (e) => {
if (!spacePanning) return;
const start = { x: e.clientX, y: e.clientY };
const startPan = { ...pan };
const onMove = (ev: PointerEvent) => setPan({ x: startPan.x + (ev.clientX - start.x), y: startPan.y + (ev.clientY - start.y) });
const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp, { once: true });
};
const frameBase = "relative overflow-hidden rounded-2xl shadow-sm border select-none";
const checkerBg = checker
? "bg-[linear-gradient(45deg,_#f0f0f0_25%,_transparent_25%),linear-gradient(-45deg,_#f0f0f0_25%,_transparent_25%),linear-gradient(45deg,_transparent_75%,_#f0f0f0_75%),linear-gradient(-45deg,_transparent_75%,_#f0f0f0_75%)] bg-[length:20px_20px] bg-[position:0_0,0_10px,10px_-10px,-10px_0px]"
: "bg-muted";
const Img = ({ src, alt }: { src?: string; alt: string }) => (
);
const Frame = ({ label, src }: { label: string; src?: string }) => (
{label}
);
return (
{/* Hidden proof of authorship */}
{/* Controls */}
setZoom(v[0])} />
Shortcuts: Ctrl/Cmd + Scroll to zoom • Space to pan • S toggle compare • L toggle layout • Ctrl/Cmd+0 reset.
{isStack ? (
) : (
)}
);
}
function CompareOverlay({
compare,
height,
original,
edited,
fitMode,
pan,
zoom,
checkerBg,
sideBySide = false,
}: {
compare: number;
height: number;
original?: string;
edited?: string;
fitMode: "contain" | "cover";
pan: { x: number; y: number };
zoom: number;
checkerBg: string;
sideBySide?: boolean;
}) {
const [pct, setPct] = useState(compare);
useEffect(() => setPct(compare), [compare]);
const overlayRef = useRef(null);
const onPointerDown: React.PointerEventHandler = (e) => {
const el = overlayRef.current; if (!el) return;
const rect = el.getBoundingClientRect();
const start = { x: e.clientX, y: e.clientY };
const getPct = (ev: PointerEvent) => {
const rel = sideBySide ? (ev.clientX - rect.left) / rect.width : (ev.clientY - rect.top) / rect.height;
return Math.min(Math.max(rel, 0), 1) * 100;
};
const onMove = (ev: PointerEvent) => setPct(getPct(ev));
const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp, { once: true });
};
const transform = `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`;
return (
{edited && (

)}
{original && (
)}
);
}
// Fancy console banner + Easter egg
if (typeof window !== "undefined") {
const banner = `\n%c██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗\n%c██╔══██╗██╔══██╗██╔═══██╗██╔═══██╗██║ ██║██╔════╝\n%c██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║█████╗ \n%c██╔═══╝ ██╔══██╗██║ ██║██║ ██║╚██╗ ██╔╝██╔══╝ \n%c██║ ██║ ██║╚██████╔╝╚██████╔╝ ╚████╔╝ ███████╗\n%c╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═══╝ ╚══════╝\n`;
console.log(banner,
"color:#4ade80;font-weight:bold;",
"color:#22d3ee;font-weight:bold;",
"color:#a855f7;font-weight:bold;",
"color:#facc15;font-weight:bold;",
"color:#f87171;font-weight:bold;",
"color:#94a3b8;font-weight:bold;",
);
console.log("%cCreated by Alex Campbell — Companionish AI Solutions (2025)", "color:#10b981; font-size:14px; font-weight:bold; background:#f0fdf4; padding:4px 8px; border-radius:4px;");
;(window as any).wallpaper = {
info: () => {
console.log("%c🎨 Wallpaper Generator Before/After Preview", "color:#3b82f6; font-weight:bold; font-size:13px;");
console.log("Created by Alex Campbell — Companionish AI Solutions (2025)");
console.log("Version: 1.0.0");
console.log("Tip: Use Ctrl+Scroll to zoom, Space to pan, S to toggle compare, L to toggle layout.");
console.log("💡 Have ideas or suggestions? %ccompanionishaisolutions@gmail.com", "color:#2563eb; text-decoration:underline; cursor:pointer;");
console.log("🌐 Live Demo: %chttps://wallpaper-generator-drab.vercel.app/", "color:#16a34a; text-decoration:underline; cursor:pointer;");
console.log("✨ Made with ❤️ by Companionish AI Solutions");
return "✔️ Info logged above.";
}
};
}
# FILE: components/ui/button.tsx
import * as React from "react";
import { cn } from "../utils";
type Props = React.ButtonHTMLAttributes & { variant?: "default" | "outline"; size?: "sm" | "md" };
export function Button({ className, variant = "default", size = "md", ...props }: Props) {
const base = "inline-flex items-center justify-center rounded-2xl border text-sm font-medium transition-all disabled:opacity-50 disabled:pointer-events-none gap-1";
const styles = variant === "outline" ? "border-gray-300 bg-white hover:bg-gray-50" : "border-transparent bg-black text-white hover:bg-gray-800";
const sizes = size === "sm" ? "h-8 px-3" : "h-10 px-4";
return ;
}
export default Button;
# FILE: components/ui/card.tsx
import * as React from "react";
import { cn } from "../utils";
export function Card({ className, ...props }: React.HTMLAttributes) { return ; }
export function CardHeader({ className, ...props }: React.HTMLAttributes) { return ; }
export function CardTitle({ className, ...props }: React.HTMLAttributes) { return ; }
export function CardContent({ className, ...props }: React.HTMLAttributes) { return ; }
export default Card;
# FILE: components/ui/input.tsx
import * as React from "react";
import { cn } from "../utils";
export const Input = React.forwardRef>(function Input({ className, ...props }, ref) {
return ;
});
export default Input;
# FILE: components/ui/label.tsx
import * as React from "react";
import * as RadixLabel from "@radix-ui/react-label";
import { cn } from "../utils";
export function Label({ className, ...props }: RadixLabel.LabelProps) { return ; }
export default Label;
# FILE: components/ui/slider.tsx
import * as React from "react";
import * as RadixSlider from "@radix-ui/react-slider";
import { cn } from "../utils";
export function Slider({ className, value, onValueChange, min = 0, max = 100, step = 1 }: { className?: string; value: number[]; onValueChange: (v: number[]) => void; min?: number; max?: number; step?: number; }) {
return (
);
}
export default Slider;
# FILE: components/ui/switch.tsx
import * as React from "react";
import * as RadixSwitch from "@radix-ui/react-switch";
export function Switch({ checked, onCheckedChange }: { checked?: boolean; onCheckedChange?: (v: boolean) => void }) {
return (
);
}
export default Switch;
# FILE: components/ui/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
# FILE: public/favicon.ico
# (optional) Add any ico here.
# FILE: README.md
# Wallpaper Generator — Next.js + Tailwind (Ready for Vercel)
## Quick Start
```bash
npm install
npm run dev
```
Open http://localhost:3000
## Deploy to Vercel
- Push this repo to GitHub
- Go to Vercel → New Project → Import
- Framework preset: **Next.js**
- No special env vars needed
- Deploy
## Notes
- Before/After component includes authorship proof: code header, hidden DOM attribute, console banner, and `wallpaper.info()` Easter egg with email and live demo link.
- UI is minimal (Tailwind + tiny components) to avoid heavy setup.
- You can later add image effects, text overlays, preset saving, etc.