initial commit

This commit is contained in:
SpyC0der77
2025-01-19 10:58:43 -05:00
parent 59d10a6b8b
commit b5e39fa011
16 changed files with 801 additions and 271 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+6 -1
View File
@@ -6,11 +6,16 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: __dirname
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-unused-vars": "off"
}
}
];
export default eslintConfig;
+199 -117
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -9,19 +9,27 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.473.0",
"next": "15.1.5",
"react": "^19.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.0.0",
"next": "15.1.5"
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"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.5",
"@eslint/eslintrc": "^3"
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
import { Roboto } from 'next/font/google'
export const roboto = Roboto({
weight: ['400', '700'],
subsets: ['latin'],
display: 'swap',
})
+63 -10
View File
@@ -1,21 +1,74 @@
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
body {
font-family: Arial, Helvetica, sans-serif;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+8 -27
View File
@@ -1,34 +1,15 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ReactNode } from 'react';
import { roboto } from './fonts';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
interface AppProps {
children: ReactNode;
}
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default function App({ children }: AppProps) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<head />
<body className={roboto.className}>{children}</body>
</html>
);
}
+6 -97
View File
@@ -1,101 +1,10 @@
import Image from "next/image";
import { DragAndDrop } from '../components/DragAndDrop'
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
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={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
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"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
<main className="w-full h-screen">
<DragAndDrop />
</main>
)
}
+27
View File
@@ -0,0 +1,27 @@
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { roboto } from '../app/fonts'
import { Sidebar } from './Sidebar'
import { ElementsProvider } from '../context/ElementsContext'
const DynamicDropArea = dynamic(() => import('./DropArea'), { ssr: false })
export const DragAndDrop: React.FC = () => {
return (
<DndProvider backend={HTML5Backend}>
<ElementsProvider>
<div className={`w-full h-screen flex ${roboto.className}`}>
<Sidebar />
<div className="flex-1">
<DynamicDropArea />
</div>
</div>
</ElementsProvider>
</DndProvider>
)
}
+72
View File
@@ -0,0 +1,72 @@
'use client'
import React, { useRef, useState, useEffect } from 'react'
import { useDrag } from 'react-dnd'
import { Button } from "@/components/ui/button"
import { roboto } from '../app/fonts'
import { useElements } from '../context/ElementsContext'
interface DraggableItemProps {
id: string
type: string
left: number
top: number
zIndex: number
moveItem: (id: string, left: number, top: number) => void
}
export const DraggableItem: React.FC<DraggableItemProps> = ({ id, type, left, top, zIndex, moveItem }) => {
const ref = useRef<HTMLDivElement>(null)
const { getElementContent } = useElements()
const [shouldHide, setShouldHide] = useState(false)
const [{ isDragging }, drag] = useDrag(() => ({
type: 'item',
item: (monitor) => {
const initialOffset = monitor.getInitialClientOffset()
const initialSourceClientOffset = monitor.getInitialSourceClientOffset()
return {
id,
left,
top,
initialOffsetX: initialOffset ? initialOffset.x - initialSourceClientOffset!.x : 0,
initialOffsetY: initialOffset ? initialOffset.y - initialSourceClientOffset!.y : 0
}
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}), [id, left, top])
useEffect(() => {
let timer: NodeJS.Timeout
if (isDragging) {
timer = setTimeout(() => setShouldHide(true), 80) // 100ms delay
} else {
setShouldHide(false)
}
return () => clearTimeout(timer)
}, [isDragging])
drag(ref)
return (
<div
ref={ref}
style={{
position: 'absolute',
left,
top,
zIndex,
cursor: 'move',
opacity: shouldHide ? 0 : 1,
touchAction: 'none',
}}
>
<Button variant="outline" className={`px-3 ${roboto.className} transition-none`}>
<span dangerouslySetInnerHTML={{ __html: getElementContent(type) }} />
</Button>
</div>
)
}
+163
View File
@@ -0,0 +1,163 @@
'use client'
import React, { useState, useRef, useCallback } from 'react'
import { useDrop } from 'react-dnd'
import { DraggableItem } from './DraggableItem'
import { useElements } from '../context/ElementsContext'
interface Item {
id: string
type: string
left: number
top: number
zIndex: number
}
interface DragItem extends Item {
initialOffsetX: number
initialOffsetY: number
}
const mergeElements = async (type1: string, type2: string) => {
try {
const response = await fetch(
`https://corsproxy.io/?https://infiniteback.org/pair?first=${type1}&second=${type2}`
)
const data = await response.json()
return {
type: data.result.toLowerCase(),
emoji: data.emoji
}
} catch (error) {
console.error('Error merging elements:', error)
return null
}
}
const DropArea: React.FC = () => {
const [items, setItems] = useState<Item[]>([])
const [maxZIndex, setMaxZIndex] = useState(0)
const dropAreaRef = useRef<HTMLDivElement>(null)
const { addElement } = useElements()
const moveItem = useCallback((id: string, left: number, top: number) => {
setItems((prevItems) => {
const newZIndex = maxZIndex + 1
setMaxZIndex(newZIndex)
return prevItems.map((item) =>
item.id === id ? { ...item, left, top, zIndex: newZIndex } : item
)
})
}, [maxZIndex])
const mergeItems = useCallback(async (item1: Item, item2: Item) => {
const result = await mergeElements(item1.type, item2.type)
if (result) {
// Add the new element type to the sidebar
addElement(result.type, result.emoji)
return {
...item1,
type: result.type
}
}
return item1
}, [addElement])
const [, drop] = useDrop(() => ({
accept: ['item', 'new-item'],
drop: async (item: DragItem | { type: string; initialOffsetX: number; initialOffsetY: number }, monitor) => {
const dropAreaRect = dropAreaRef.current?.getBoundingClientRect()
if (!dropAreaRect) return
const clientOffset = monitor.getClientOffset()
if (!clientOffset) return
const left = clientOffset.x - dropAreaRect.left - item.initialOffsetX
const top = clientOffset.y - dropAreaRect.top - item.initialOffsetY
if ('id' in item) {
const targetItem = items.find(i =>
i.id !== item.id &&
Math.abs(i.left - left) < 50 &&
Math.abs(i.top - top) < 50
)
if (targetItem) {
const mergedItem = await mergeItems(item, targetItem)
setItems(prevItems => {
// Remove both original items and add the merged item
const filteredItems = prevItems.filter(i => i.id !== item.id && i.id !== targetItem.id)
return [...filteredItems, {
...mergedItem,
id: Date.now().toString(),
left,
top,
zIndex: maxZIndex + 1
}]
})
setMaxZIndex(prev => prev + 1)
} else {
moveItem(item.id, left, top)
}
} else {
const newZIndex = maxZIndex + 1
setMaxZIndex(newZIndex)
const newItem: Item = {
id: Date.now().toString(),
type: item.type,
left,
top,
zIndex: newZIndex,
}
const targetItem = items.find(i =>
Math.abs(i.left - left) < 50 &&
Math.abs(i.top - top) < 50
)
if (targetItem) {
const mergedItem = await mergeItems(newItem, targetItem)
setItems(prevItems => {
// Remove the original item and add the merged item
const filteredItems = prevItems.filter(i => i.id !== targetItem.id)
return [...filteredItems, {
...mergedItem,
id: Date.now().toString(),
left,
top,
zIndex: maxZIndex + 1
}]
})
setMaxZIndex(prev => prev + 1)
} else {
setItems((prevItems) => [...prevItems, newItem])
}
}
},
}), [moveItem, maxZIndex, items, mergeItems])
return (
<div
ref={(node) => {
drop(node)
dropAreaRef.current = node
}}
className="w-full h-full relative bg-white"
>
{items.map((item) => (
<DraggableItem
key={item.id}
id={item.id}
type={item.type}
left={item.left}
top={item.top}
zIndex={item.zIndex}
moveItem={moveItem}
/>
))}
</div>
)
}
export default DropArea
+59
View File
@@ -0,0 +1,59 @@
'use client'
import React, { useRef } from 'react'
import { useDrag } from 'react-dnd'
import { Button } from "@/components/ui/button"
import { roboto } from '../app/fonts'
import { useElements } from '../context/ElementsContext'
interface DraggableElementProps {
type: string
content: string
}
const DraggableElement: React.FC<DraggableElementProps> = ({ type, content }) => {
const ref = useRef<HTMLDivElement>(null)
const [, drag] = useDrag(() => ({
type: 'new-item',
item: (monitor) => {
const initialOffset = monitor.getInitialClientOffset()
const initialSourceClientOffset = monitor.getInitialSourceClientOffset()
return {
type,
initialOffsetX: initialOffset ? initialOffset.x - initialSourceClientOffset!.x : 0,
initialOffsetY: initialOffset ? initialOffset.y - initialSourceClientOffset!.y : 0
}
}
}))
drag(ref)
return (
<div ref={ref}>
<Button variant="outline" className={`px-3 ${roboto.className} transition-none`}>
<span dangerouslySetInnerHTML={{ __html: content }} />
</Button>
</div>
)
}
export const Sidebar: React.FC = () => {
const { elements, getElementContent } = useElements()
return (
<div className="w-64 h-full bg-gray-100 p-4 overflow-y-auto">
<h2 className="text-xl font-bold mb-4">Elements</h2>
<div className="flex flex-wrap gap-2">
{Array.from(elements.values()).map((element) => (
<DraggableElement
key={element.type}
type={element.type}
content={getElementContent(element.type)}
/>
))}
</div>
</div>
)
}
+57
View File
@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+60
View File
@@ -0,0 +1,60 @@
'use client'
import React, { createContext, useContext, useState, useCallback } from 'react'
interface Element {
type: string
emoji: string
text: string
}
interface ElementsContextType {
elements: Map<string, Element>
addElement: (type: string, emoji: string) => void
getElementContent: (type: string) => string
}
const ElementsContext = createContext<ElementsContextType | undefined>(undefined)
export function ElementsProvider({ children }: { children: React.ReactNode }) {
const [elements, setElements] = useState<Map<string, Element>>(new Map([
['water', { type: 'water', emoji: '💧', text: 'Water' }],
['fire', { type: 'fire', emoji: '🔥', text: 'Fire' }],
['earth', { type: 'earth', emoji: '🌿', text: 'Earth' }],
['air', { type: 'air', emoji: '💨', text: 'Air' }],
]))
const addElement = useCallback((type: string, emoji: string) => {
setElements(prev => {
const newElements = new Map(prev)
if (!newElements.has(type)) {
newElements.set(type, {
type,
emoji,
text: type.charAt(0).toUpperCase() + type.slice(1)
})
}
return newElements
})
}, [])
const getElementContent = useCallback((type: string) => {
const element = Array.from(elements.values()).find(el => el.type === type)
return element ? `${element.emoji} ${element.text}` : type
}, [elements])
return (
<ElementsContext.Provider value={{ elements, addElement, getElementContent }}>
{children}
</ElementsContext.Provider>
)
}
export function useElements() {
const context = useContext(ElementsContext)
if (context === undefined) {
throw new Error('useElements must be used within a ElementsProvider')
}
return context
}
+6
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))
}
+33 -14
View File
@@ -1,18 +1,37 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
const config: Config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
}
}
}
},
},
plugins: [],
plugins: []
} satisfies Config;
export default config;