Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 48 additions & 66 deletions app/notification-generator/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"use client";

import { Play } from "lucide-react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useState } from "react";
import type { NotificationConfig } from "@/components/notification-generator/types";
import { Button } from "@/components/ui/button";
import { FadeIn, MotionSection, SlideIn } from "@/components/ui/motion/motion-components";

const NotificationGeneratedCode = dynamic(
Expand All @@ -21,14 +19,6 @@ const NotificationGeneratorForm = dynamic(
),
{ ssr: false }
);
const MinecraftPreview = dynamic(
() =>
import("@/components/notification-generator/preview/minecraft-preview").then(
(mod) => mod.MinecraftPreview
),
{ ssr: false }
);

export default function NotificationGeneratorPage() {
const [notification, setNotification] = useState<NotificationConfig>({
chat: "",
Expand All @@ -46,74 +36,66 @@ export default function NotificationGeneratorPage() {
});

const [yamlCode, setYamlCode] = useState("");
const [previewKey, setPreviewKey] = useState(0);

const generateYaml = useCallback(() => generateYamlString(notification), [notification]);

useEffect(() => {
setYamlCode(generateYaml());
}, [generateYaml]);

const handlePlayPreview = useCallback(() => {
setPreviewKey((prev) => prev + 1);
}, []);

return (
<MotionSection className="w-full">
<div className="mb-12 text-center">
<SlideIn delay={0.1} direction="down">
<h1 className="bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text font-extrabold text-4xl text-transparent tracking-tight md:text-6xl dark:from-white dark:to-gray-300">
Notification Generator
</h1>
</SlideIn>
<FadeIn delay={0.2}>
<p className="mt-4 text-gray-600 text-lg dark:text-gray-400">
Design and preview your EternalCode notifications in real-time.
</p>
</FadeIn>
<MotionSection className="relative w-full overflow-hidden">
<div className="absolute inset-0">
<div className="absolute top-32 left-[-10%] h-[420px] w-[420px] rounded-full bg-blue-500/15 blur-[120px]" />
<div className="absolute right-[-5%] bottom-0 h-[480px] w-[480px] rounded-full bg-emerald-500/10 blur-[140px]" />
<div className="absolute top-0 right-[20%] h-[260px] w-[260px] rounded-full bg-orange-400/10 blur-[110px]" />
</div>

<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<SlideIn className="h-full" delay={0.3} direction="left">
<NotificationGeneratorForm
notification={notification}
setNotification={setNotification}
/>
</SlideIn>

<SlideIn className="h-full" delay={0.4} direction="right">
<div className="h-full rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900/40 dark:ring-gray-800">
<h2 className="mb-4 font-semibold text-gray-900 text-xl dark:text-white">
Generated Configuration
</h2>
<NotificationGeneratedCode yamlCode={yamlCode} />
</div>
</SlideIn>
</div>
<div className="relative z-10 mx-auto max-w-[90rem] px-4 py-16 sm:px-6 lg:px-8">
<div className="mb-12 text-center">
<SlideIn delay={0.1} direction="down">
<p className="font-semibold text-gray-500 text-sm uppercase tracking-[0.3em] dark:text-gray-400">
Minecraft Notification Studio
</p>
</SlideIn>
<SlideIn delay={0.18} direction="down">
<h1 className="mt-4 bg-gradient-to-r from-gray-900 via-gray-700 to-gray-500 bg-clip-text font-extrabold text-4xl text-transparent tracking-tight md:text-6xl dark:from-white dark:via-gray-200 dark:to-gray-400">
Notification Generator
</h1>
</SlideIn>
<FadeIn delay={0.26}>
<p className="mx-auto mt-4 max-w-2xl text-gray-600 text-lg dark:text-gray-400">
Build, preview, and ship polished in-game notifications with modern formatting and
legacy-friendly color support.
</p>
</FadeIn>
</div>

<SlideIn className="mt-8" delay={0.5} direction="up">
<div className="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900/40 dark:ring-gray-800">
<div className="mb-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
<div>
<h2 className="font-semibold text-gray-900 text-xl dark:text-white">Live Preview</h2>
<p className="mt-1 text-gray-500 text-sm dark:text-gray-400">
See how your notification looks in-game.
</p>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<SlideIn className="h-full" delay={0.3} direction="left">
<NotificationGeneratorForm
notification={notification}
setNotification={setNotification}
/>
</SlideIn>

<SlideIn className="h-full" delay={0.4} direction="right">
<div className="h-full rounded-3xl border border-white/10 bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:border-gray-800 dark:bg-gray-950/60">
<div className="mb-6">
<p className="font-semibold text-gray-500 text-xs uppercase tracking-[0.25em] dark:text-gray-400">
Output
</p>
<h2 className="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">
Generated Configuration
</h2>
<p className="mt-2 text-gray-500 text-sm dark:text-gray-400">
Copy the YAML and paste it into the EternalCore notification config.
</p>
</div>
<NotificationGeneratedCode yamlCode={yamlCode} />
</div>
<Button
leftIcon={<Play className="h-4 w-4" />}
onClick={handlePlayPreview}
variant="primary"
>
Replay Animation
</Button>
</div>

<div className="overflow-hidden rounded-xl border border-gray-200 bg-black shadow-lg dark:border-gray-800">
<MinecraftPreview key={previewKey} notification={notification} />
</div>
</SlideIn>
</div>
</SlideIn>
</div>
</MotionSection>
);
}
Expand Down
36 changes: 20 additions & 16 deletions components/notification-generator/form/color-constants.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { MinecraftColor, TagCategory } from "./types";

export const minecraftColors: MinecraftColor[] = [
{ name: "Black", hex: "#000000" },
{ name: "Dark Blue", hex: "#0000AA" },
{ name: "Dark Green", hex: "#00AA00" },
{ name: "Dark Aqua", hex: "#00AAAA" },
{ name: "Dark Red", hex: "#AA0000" },
{ name: "Dark Purple", hex: "#AA00AA" },
{ name: "Gold", hex: "#FFAA00" },
{ name: "Gray", hex: "#AAAAAA" },
{ name: "Dark Gray", hex: "#555555" },
{ name: "Blue", hex: "#5555FF" },
{ name: "Green", hex: "#55FF55" },
{ name: "Aqua", hex: "#55FFFF" },
{ name: "Red", hex: "#FF5555" },
{ name: "Light Purple", hex: "#FF55FF" },
{ name: "Yellow", hex: "#FFFF55" },
{ name: "White", hex: "#FFFFFF" },
{ name: "Black", hex: "#000000", legacyCode: "&0" },
{ name: "Dark Blue", hex: "#0000AA", legacyCode: "&1" },
{ name: "Dark Green", hex: "#00AA00", legacyCode: "&2" },
{ name: "Dark Aqua", hex: "#00AAAA", legacyCode: "&3" },
{ name: "Dark Red", hex: "#AA0000", legacyCode: "&4" },
{ name: "Dark Purple", hex: "#AA00AA", legacyCode: "&5" },
{ name: "Gold", hex: "#FFAA00", legacyCode: "&6" },
{ name: "Gray", hex: "#AAAAAA", legacyCode: "&7" },
{ name: "Dark Gray", hex: "#555555", legacyCode: "&8" },
{ name: "Blue", hex: "#5555FF", legacyCode: "&9" },
{ name: "Green", hex: "#55FF55", legacyCode: "&a" },
{ name: "Aqua", hex: "#55FFFF", legacyCode: "&b" },
{ name: "Red", hex: "#FF5555", legacyCode: "&c" },
{ name: "Light Purple", hex: "#FF55FF", legacyCode: "&d" },
{ name: "Yellow", hex: "#FFFF55", legacyCode: "&e" },
{ name: "White", hex: "#FFFFFF", legacyCode: "&f" },
];

export const gradientPresets = [
Expand Down Expand Up @@ -48,6 +48,10 @@ export const allowedTags: Record<string, TagCategory> = {
pattern: /<[a-z_]+>/,
tags: minecraftColors.map((color) => `<${color.name.toLowerCase().replace(/ /g, "_")}>`),
},
legacy: {
pattern: /(?:§|&)[0-9a-fk-or]/i,
tags: [...minecraftColors.map((color) => color.legacyCode), "&k", "&l", "&m", "&n", "&o", "&r"],
},
clickUrl: {
pattern: /<click:open_url:'[^']*'><\/click>/,
tags: ["<click:open_url:'url'></click>"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { gradientPresets, minecraftColors } from "../color-constants";
import type { ColorPickerProps, MinecraftColor } from "../types";

export const ColorPicker = ({ onApplyAction, onCloseAction }: ColorPickerProps) => {
const [activeTab, setActiveTab] = useState<"solid" | "gradient" | "presets">("solid");
const [activeTab, setActiveTab] = useState<"solid" | "gradient" | "presets" | "legacy">("solid");
const [solidColor, setSolidColor] = useState("#FFFFFF");
const [gradientColors, setGradientColors] = useState<string[]>(["#55FFFF", "#FF55FF"]);
const [legacyColor, setLegacyColor] = useState<MinecraftColor>(minecraftColors[0]);
const [_copied, _setCopied] = useState(false);

// Sync solid color with first gradient color for smoother transitions
Expand All @@ -25,10 +26,16 @@ export const ColorPicker = ({ onApplyAction, onCloseAction }: ColorPickerProps)
if (activeTab === "gradient") {
const result = `<gradient:${gradientColors.join(":")}></gradient>`;
onApplyAction(result, true, gradientColors);
} else {
const result = `<color:${solidColor}></color>`;
onApplyAction(result, false, [solidColor]);
return;
}

if (activeTab === "legacy") {
onApplyAction(legacyColor.legacyCode, false, [legacyColor.hex]);
return;
}

const result = `<color:${solidColor}></color>`;
onApplyAction(result, false, [solidColor]);
};

const handlePresetClick = (color: MinecraftColor) => {
Expand Down Expand Up @@ -89,7 +96,7 @@ export const ColorPicker = ({ onApplyAction, onCloseAction }: ColorPickerProps)

{/* Tabs */}
<div className="flex border-gray-100 border-b dark:border-gray-800">
{(["solid", "gradient", "presets"] as const).map((tab) => (
{(["solid", "gradient", "presets", "legacy"] as const).map((tab) => (
<button
className={`flex-1 cursor-pointer px-3 py-2 font-medium text-sm transition-colors ${
activeTab === tab
Expand Down Expand Up @@ -257,6 +264,48 @@ export const ColorPicker = ({ onApplyAction, onCloseAction }: ColorPickerProps)
</div>
</motion.div>
)}

{activeTab === "legacy" && (
<motion.div
animate={{ opacity: 1, x: 0 }}
className="space-y-4"
exit={{ opacity: 0, x: 10 }}
initial={{ opacity: 0, x: -10 }}
key="legacy"
transition={{ duration: 0.2 }}
>
<div>
<h4 className="mb-2 font-semibold text-gray-500 text-xs uppercase dark:text-gray-400">
Legacy Colors (with & codes)
</h4>
<div className="grid grid-cols-4 gap-2">
{minecraftColors.map((mcColor) => (
<button
className={`group relative flex aspect-square cursor-pointer flex-col items-center justify-center rounded-md border transition-all hover:scale-105 hover:shadow-md ${
legacyColor.name === mcColor.name
? "border-blue-500 ring-2 ring-blue-500/40"
: "border-gray-200 dark:border-gray-700"
}`}
key={mcColor.hex}
onClick={() => setLegacyColor(mcColor)}
style={{ backgroundColor: mcColor.hex }}
title={`${mcColor.name} (${mcColor.legacyCode})`}
type="button"
>
<span className="sr-only">{mcColor.name}</span>
<span className="rounded bg-black/50 px-1.5 py-0.5 font-mono text-[10px] text-white">
{mcColor.legacyCode}
</span>
</button>
))}
</div>
</div>

<div className="rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-600 text-xs dark:border-gray-800 dark:bg-gray-900/60 dark:text-gray-300">
Legacy codes are classic Minecraft formatting codes that work in older configs.
</div>
</motion.div>
)}
</AnimatePresence>

{activeTab !== "presets" && (
Expand All @@ -265,7 +314,15 @@ export const ColorPicker = ({ onApplyAction, onCloseAction }: ColorPickerProps)
onClick={handleApply}
type="button"
>
Apply {activeTab === "gradient" ? "Gradient" : "Color"}
{(() => {
if (activeTab === "gradient") {
return "Apply Gradient";
}
if (activeTab === "legacy") {
return "Insert Legacy Code";
}
return "Apply Color";
})()}
</button>
)}
</div>
Expand Down
19 changes: 19 additions & 0 deletions components/notification-generator/form/formatting/tag-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { allowedTags } from "../color-constants";

const LEGACY_CODE_PATTERN = /^(?:&|§)[0-9a-fk-or]$/i;

export const isValidTag = (tag: string): boolean =>
Object.values(allowedTags).some(
(category) => category.pattern.test(tag) || category.tags.includes(tag)
Expand Down Expand Up @@ -46,6 +48,23 @@ export const insertTag = (
console.warn(`Invalid tag format: ${tag}`);
return { newValue: text, newCursorPosition: end };
}

if (LEGACY_CODE_PATTERN.test(tag)) {
if (start !== end) {
const selectedText = text.substring(start, end);
const resetCode = tag.startsWith("§") ? "§r" : "&r";
const wrappedText = `${tag}${selectedText}${resetCode}`;
return {
newValue: text.substring(0, start) + wrappedText + text.substring(end),
newCursorPosition: start + wrappedText.length,
};
}
return {
newValue: text.substring(0, start) + tag + text.substring(end),
newCursorPosition: start + tag.length,
};
}

if (start !== end) {
const selectedText = text.substring(start, end);
const tagWithContent = tag.replace("></", `>${selectedText}</`);
Expand Down
1 change: 1 addition & 0 deletions components/notification-generator/form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface TagCategory {
export interface MinecraftColor {
name: string;
hex: string;
legacyCode: string;
}

export interface FormFieldProps {
Expand Down
Loading