Proof of concept update

This commit is contained in:
2026-05-03 23:09:44 -06:00
parent 9b3a25dc84
commit ccecca22c3
16 changed files with 1977 additions and 185 deletions

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/public/svg" vcs="Git" />
</component>
</project>

18
app/Store.tsx Normal file
View File

@@ -0,0 +1,18 @@
'use client';
import React, {createContext, Dispatch} from "react";
export const defaultContext: ContextStore = {
base: "#FFBA00",
};
export const Context = createContext<ContextStore>(defaultContext);
export type ContextStore = {
base: string;
action?: Dispatch<ContextStore>;
};
export function reducer(state: ContextStore, action: ContextStore) {
return { ...state, ...action };
}

19
app/api/emojis/route.ts Normal file
View File

@@ -0,0 +1,19 @@
import * as fs from "node:fs";
import path from "node:path";
import {NextResponse} from "next/server";
export async function GET() {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
const dirpath = path.join(process.cwd() + '/public/svg');
const files = fs.readdirSync(dirpath)
const fileObject: Record<string, string>[] = [];
files.filter(el => path.extname(el) === '.svg').forEach((filePath) => {
fileObject.push({ filePath: filePath, data: fs.readFileSync(path.join(dirpath, filePath)).toString() })
})
// do something with your files, by the way they are just filenames...
return NextResponse.json(fileObject, { status: 200 });
}

19
app/api/recolor/route.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from 'node:path';
import AdmZip from 'adm-zip';
export async function GET() {
const headers = new Headers();
headers.append('Content-Disposition', 'attachment; filename=archive.zip');
headers.append('Content-Type', 'application/zip');
const zip = new AdmZip();
zip.addLocalFile(path.join(process.cwd(), 'assets', 'image.png'));
zip.addLocalFile(path.join(process.cwd(), 'assets', 'document.pdf'));
zip.addFile('your-filename', Buffer.from('text file content', 'utf8'));
const zipBuffer = zip.toBuffer();
return new Response(zipBuffer, {
headers,
});
}

51
app/app.tsx Normal file
View File

@@ -0,0 +1,51 @@
'use client';
import {useContext, useEffect, useMemo, useState} from "react";
import { Context } from "./Store";
import { InfiniteSlider } from "@/components/motion-primitives/infinite-slider";
import {Container, Typography} from "@mui/material";
import {Colorful} from "@uiw/react-color";
export default function App() {
const {base, action} = useContext(Context);
const [images, setImages] = useState<Record<string, string>[]>([]);
useEffect(() => {
const response = fetch('/api/emojis');
response.then((response) => {
console.log(response);
response.json().then((body) => setImages(body));
});
}, []);
const renderImages = useMemo(() => {
return images.map((image) => {
const newImage = image.data.replace(/#FFBA00/g, base);
return btoa(newImage);
});
}, [images, base]);
return (
<>
<InfiniteSlider speedOnHover={20} gap={24}>
{renderImages.map((image, index) => {
const base64 = `data:image/svg+xml;base64,${image}`;
return <img key={index} src={base64} alt="" width={200} height={200}/>
})}
</InfiniteSlider>
<Container>
<Typography variant="h2">
Dragon Emoji Generator
</Typography>
<Colorful
color={base}
onChange={(color) => action?.({base: color.hex})}
disableAlpha={true}
/>
</Container>
</>
);
}

24
app/colors.ts Normal file
View File

@@ -0,0 +1,24 @@
export interface Colors {
"base": string,
"shadow": string,
"lines": string,
"mouth": string,
"blush": string,
"tongue": string,
"clothes": string,
"clothes_light": string,
"clothes_shadow": string,
}
export const DefaultColors: Colors = {
base: "#FFBA00",
shadow: "#E28940",
lines: "#161616",
mouth: "#EA663D",
blush: "#F76E1D",
tongue: "#D7598B",
clothes: "#8335A7",
clothes_light: "#B44FC8",
clothes_shadow: "#5C2F54",
}

View File

@@ -0,0 +1,22 @@
'use client';
import {Dispatch, FunctionComponent, SetStateAction, useState} from "react";
import {Colors} from "@/app/colors";
import {Colorful} from "@uiw/react-color";
import {Container, TextField} from "@mui/material";
export type ColorPickerProps = {
identifier: keyof Colors;
color: string;
action: Dispatch<SetStateAction<Colors>>;
}
export const ColorPickerWrapper: FunctionComponent<ColorPickerProps> = ({identifier, color, action}) => {
const [showPicker, setShowPicker] = useState(false);
return <Container>
<TextField id="outlined-basic" label="Outlined" variant="outlined" />
</Container>;
}

View File

@@ -0,0 +1,23 @@
'use client';
import {FunctionComponent} from "react";
export type GenerateEmojiProps = {
updatedColors: any;
};
export const GenerateEmoji: FunctionComponent<GenerateEmojiProps> = ({ updatedColors }) => {
async function handleDownload() {
const response = await fetch('/api/zip');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'archive.zip';
link.click();
window.URL.revokeObjectURL(url);
}
return <button onClick={handleDownload}>Download</button>;
};

View File

@@ -1,26 +1 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,17 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
@@ -25,7 +14,6 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>

View File

@@ -1,65 +1,28 @@
import Image from "next/image";
'use client'
import { useReducer } from "react";
import {Context, defaultContext, reducer} from "@/app/Store";
import {createTheme, CssBaseline, ThemeProvider} from "@mui/material";
import App from "@/app/app";
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
const [state, dispatch] = useReducer(reducer, defaultContext);
return (
<Context.Provider value={{ ...state, action: dispatch }}>
<ThemeProvider theme={darkTheme}>
<CssBaseline/>
<main>
<App/>
</main>
</ThemeProvider>
</Context.Provider>
);
}

View File

@@ -0,0 +1,113 @@
'use client';
import { cn } from '@/lib/utils';
import { useMotionValue, animate, motion } from 'motion/react';
import { useState, useEffect } from 'react';
import useMeasure from 'react-use-measure';
export type InfiniteSliderProps = {
children: React.ReactNode;
gap?: number;
speed?: number;
speedOnHover?: number;
direction?: 'horizontal' | 'vertical';
reverse?: boolean;
className?: string;
};
export function InfiniteSlider({
children,
gap = 16,
speed = 100,
speedOnHover,
direction = 'horizontal',
reverse = false,
className,
}: InfiniteSliderProps) {
const [isHovering, setIsHovering] = useState(false);
const currentSpeed = isHovering && speedOnHover ? speedOnHover : speed;
const [ref, { width, height }] = useMeasure();
const translation = useMotionValue(0);
const [isTransitioning, setIsTransitioning] = useState(false);
const [key, setKey] = useState(0);
useEffect(() => {
let controls;
const size = direction === 'horizontal' ? width : height;
const contentSize = size + gap;
const from = reverse ? -contentSize / 2 : 0;
const to = reverse ? 0 : -contentSize / 2;
const distanceToTravel = Math.abs(to - from);
const duration = distanceToTravel / currentSpeed;
if (isTransitioning) {
const remainingDistance = Math.abs(translation.get() - to);
const transitionDuration = remainingDistance / currentSpeed;
controls = animate(translation, [translation.get(), to], {
ease: 'linear',
duration: transitionDuration,
onComplete: () => {
setIsTransitioning(false);
setKey((prevKey) => prevKey + 1);
},
});
} else {
controls = animate(translation, [from, to], {
ease: 'linear',
duration: duration,
repeat: Infinity,
repeatType: 'loop',
repeatDelay: 0,
onRepeat: () => {
translation.set(from);
},
});
}
return controls?.stop;
}, [
key,
translation,
currentSpeed,
width,
height,
gap,
isTransitioning,
direction,
reverse,
]);
const hoverProps = speedOnHover
? {
onHoverStart: () => {
setIsTransitioning(true);
setIsHovering(true);
},
onHoverEnd: () => {
setIsTransitioning(true);
setIsHovering(false);
},
}
: {};
return (
<div className={cn('overflow-hidden', className)}>
<motion.div
className='flex w-max'
style={{
...(direction === 'horizontal'
? { x: translation }
: { y: translation }),
gap: `${gap}px`,
flexDirection: direction === 'horizontal' ? 'row' : 'column',
}}
ref={ref}
{...hoverProps}
>
{children}
{children}
</motion.div>
</div>
);
}

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

1721
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,18 +9,31 @@
"lint": "eslint"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.10",
"@mui/material": "^9.0.0",
"@tailwindcss/vite": "^4.2.4",
"@types/adm-zip": "^0.5.8",
"@uiw/react-color": "^2.10.1",
"adm-zip": "^0.5.17",
"lucide-react": "^1.14.0",
"motion": "^12.38.0",
"next": "16.2.4",
"postcss": "^8.5.13",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"react-use-measure": "^2.1.7",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@tailwindcss/postcss": "^4.2.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"tailwindcss": "^4",
"tailwindcss": "^4.2.4",
"typescript": "^5"
}
}