From 3e28d196be25771287ac401f8e62b69a22f60f00 Mon Sep 17 00:00:00 2001 From: ChaoticGood007 Date: Tue, 12 May 2026 21:06:04 -0500 Subject: [PATCH 1/5] feat: Add Custom Mold feature inline to AddDiscForm --- CHANGELOG.md | 5 ++ package-lock.json | 4 +- package.json | 2 +- src/app/actions/molds.ts | 41 +++++++++++ src/components/AddDiscForm.tsx | 126 +++++++++++++++++++++++++++++++-- 5 files changed, 171 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b4b6a..2b03d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - 2026-05-12 + +### Added +- **Custom Molds:** Added the ability to create Custom Molds directly inline from the "Add Disc" form. If a disc does not exist in the Discit API, users can now define the disc's name, brand, category, and flight numbers to add it to their vault. + ## [0.5.2] - 2026-05-07 ### Fixed diff --git a/package-lock.json b/package-lock.json index 911164b..0c67fba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "discvault", - "version": "0.1.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "discvault", - "version": "0.1.0", + "version": "0.6.0", "dependencies": { "@prisma/client": "^6.19.2", "@types/papaparse": "^5.5.2", diff --git a/package.json b/package.json index abcce67..73b8184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discvault", - "version": "0.5.0", + "version": "0.6.0", "private": true, "scripts": { "dev": "next dev -p 3001", diff --git a/src/app/actions/molds.ts b/src/app/actions/molds.ts index 06ae9e7..1e30437 100644 --- a/src/app/actions/molds.ts +++ b/src/app/actions/molds.ts @@ -96,3 +96,44 @@ export async function syncMolds() { revalidatePath('/') return { updatedCount, createdCount } } + +function getStability(turn: number, fade: number): string { + const score = turn + fade + if (score <= -2) return 'Understable' + if (score <= 0) return 'Stable' + if (score <= 2) return 'Overstable' + return 'Very Overstable' +} + +export async function createCustomMold(formData: FormData) { + const name = formData.get('name') as string + const brand = formData.get('brand') as string + const category = formData.get('category') as string + const speed = parseFloat(formData.get('speed') as string) + const glide = parseFloat(formData.get('glide') as string) + const turn = parseFloat(formData.get('turn') as string) + const fade = parseFloat(formData.get('fade') as string) + + if (!name || !brand || !category || isNaN(speed) || isNaN(glide) || isNaN(turn) || isNaN(fade)) { + throw new Error('All fields are required to create a custom mold.') + } + + const stability = getStability(turn, fade) + + const newMold = await prisma.mold.create({ + data: { + name, + brand, + category, + speed, + glide, + turn, + fade, + stability, + isCustom: true + } + }) + + revalidatePath('/settings') + return newMold +} diff --git a/src/components/AddDiscForm.tsx b/src/components/AddDiscForm.tsx index 1c9f4a4..35e4cb8 100644 --- a/src/components/AddDiscForm.tsx +++ b/src/components/AddDiscForm.tsx @@ -18,7 +18,8 @@ import { useState, useEffect, useCallback } from 'react' import { addDisc } from '@/app/actions/inventory' -import { Search, Loader2 } from 'lucide-react' +import { createCustomMold } from '@/app/actions/molds' +import { Search, Loader2, Plus } from 'lucide-react' import LocationPicker from '@/components/LocationPicker' import { type LocationNode } from '@/lib/locationTree' @@ -41,6 +42,49 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { const [loading, setLoading] = useState(false) const [isSearching, setIsSearching] = useState(false) const [selectedLocation, setSelectedLocation] = useState(null) + const [isCustomMode, setIsCustomMode] = useState(false) + const [isCreatingCustom, setIsCreatingCustom] = useState(false) + + const handleCreateCustomMold = async () => { + const brandInput = document.querySelector('input[name="customBrand"]') as HTMLInputElement + const nameInput = document.querySelector('input[name="customName"]') as HTMLInputElement + const categoryInput = document.querySelector('select[name="customCategory"]') as HTMLSelectElement + const speedInput = document.querySelector('input[name="customSpeed"]') as HTMLInputElement + const glideInput = document.querySelector('input[name="customGlide"]') as HTMLInputElement + const turnInput = document.querySelector('input[name="customTurn"]') as HTMLInputElement + const fadeInput = document.querySelector('input[name="customFade"]') as HTMLInputElement + + if (!brandInput.value || !nameInput.value || !speedInput.value || !glideInput.value || !turnInput.value || !fadeInput.value) { + alert("Please fill out all custom mold fields") + return + } + + setIsCreatingCustom(true) + const formData = new FormData() + formData.append('brand', brandInput.value) + formData.append('name', nameInput.value) + formData.append('category', categoryInput.value) + formData.append('speed', speedInput.value) + formData.append('glide', glideInput.value) + formData.append('turn', turnInput.value) + formData.append('fade', fadeInput.value) + + try { + const newMold = await createCustomMold(formData) + handleSelectMold({ + id: newMold.id, + name: newMold.name, + brand: newMold.brand, + category: newMold.category + }) + setIsCustomMode(false) + } catch (e) { + console.error(e) + alert("Failed to create custom mold") + } finally { + setIsCreatingCustom(false) + } + } const searchMolds = useCallback(async (q: string) => { if (q.length < 2) { @@ -98,7 +142,7 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { {loading && } - {isSearching && molds.length > 0 && ( + {isSearching && (
    {molds.map((mold) => (
  • {mold.brand} • {mold.category}
  • ))} +
  • { + setIsCustomMode(true) + setSelectedMold(null) + setIsSearching(false) + }} + className="px-5 py-4 hover:bg-slate-50 cursor-pointer flex items-center justify-center transition-all rounded-2xl border-2 border-dashed border-slate-200 mt-2 text-slate-500 font-bold gap-2" + > + Not finding your disc? Create Custom Mold +
)} - + + {isCustomMode && !selectedMold && ( +
+
+

+ Create Custom Mold +

+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ )} +
@@ -241,7 +359,7 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) {
diff --git a/src/components/DiscDetailView.tsx b/src/components/DiscDetailView.tsx index fd48390..70caf3b 100644 --- a/src/components/DiscDetailView.tsx +++ b/src/components/DiscDetailView.tsx @@ -4,6 +4,7 @@ import { Edit3, ArrowLeft, MapPin, Zap } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' import DiscPreview from './DiscPreview' +import { SECONDARY_PATTERN_LABELS } from '@/lib/constants' interface DiscDetailViewProps { disc: { @@ -148,7 +149,7 @@ export default function DiscDetailView({ disc, categoryColors, vaultId, bagPaths
Secondary Pattern - {disc.secondaryPattern || '—'} + {disc.secondaryPattern ? (SECONDARY_PATTERN_LABELS[disc.secondaryPattern] || disc.secondaryPattern) : '—'}
Condition diff --git a/src/components/EditDiscForm.tsx b/src/components/EditDiscForm.tsx index 6e67276..f05b228 100644 --- a/src/components/EditDiscForm.tsx +++ b/src/components/EditDiscForm.tsx @@ -22,6 +22,7 @@ import { Trash2, ArrowLeft, Inbox } from 'lucide-react' import { useRouter } from 'next/navigation' import LocationPicker from '@/components/LocationPicker' import { type LocationNode } from '@/lib/locationTree' +import { SECONDARY_PATTERN_LABELS } from '@/lib/constants' interface Collection { id: string @@ -191,11 +192,9 @@ export default function EditDiscForm({ disc, collections }: Omit - - - - - + {Object.entries(SECONDARY_PATTERN_LABELS).map(([value, label]) => ( + + ))}
diff --git a/src/components/PreviewModal.tsx b/src/components/PreviewModal.tsx index 1943331..7a56324 100644 --- a/src/components/PreviewModal.tsx +++ b/src/components/PreviewModal.tsx @@ -20,6 +20,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { X } from 'lucide-react' import DiscPreview from './DiscPreview' import { useEffect } from 'react' +import { SECONDARY_PATTERN_LABELS } from '@/lib/constants' interface PreviewModalProps { isOpen: boolean @@ -105,7 +106,7 @@ export default function PreviewModal({ isOpen, onClose, disc }: PreviewModalProp {disc.secondaryPattern && (
- {disc.secondaryPattern} Pattern + {SECONDARY_PATTERN_LABELS[disc.secondaryPattern] || disc.secondaryPattern} Pattern
)} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 679be62..ced58a0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -5,3 +5,11 @@ export const DEFAULT_CATEGORY_COLORS: Record = { 'Putter': '#10b981', // emerald 'Utility': '#8b5cf6', // violet } + +export const SECONDARY_PATTERN_LABELS: Record = { + 'Halo': 'Halo/Overmold', + 'Burst': 'Center Burst', + 'Split': 'Half \'n Half', + 'Swirl': 'Swirly', + 'Speckled': 'Speckled' +} From 37cf0338bb3653c3700f9fba8d63c76328afeade Mon Sep 17 00:00:00 2001 From: ChaoticGood007 Date: Tue, 12 May 2026 21:22:48 -0500 Subject: [PATCH 3/5] fix: Resolve SVG rendering issues with translucent secondary patterns --- CHANGELOG.md | 1 + src/components/DiscPreview.tsx | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a095d13..6c65b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Secondary Patterns:** Standardized Secondary Pattern names (e.g. "Halo/Overmold") to consistently display friendly labels across both the editor forms and the detail views instead of the raw database values. +- **Disc Visuals:** Fixed an SVG rendering bug where translucent or "Clear" secondary colors in Half 'n Half and Halo patterns incorrectly showed the base disc color underneath them. ## [0.5.2] - 2026-05-07 diff --git a/src/components/DiscPreview.tsx b/src/components/DiscPreview.tsx index 787cf5b..9960630 100644 --- a/src/components/DiscPreview.tsx +++ b/src/components/DiscPreview.tsx @@ -128,12 +128,26 @@ export default function DiscPreview({ {isTuned && showTunedOutline && isHovered && } {/* Base Disc */} - + {pattern === 'Split' && secondaryColor ? ( + <> + + + + ) : ( + + )} {/* Secondary Patterns - Clipped to disc bounds */} {pattern === 'Halo' && secondaryColor && ( - + )} {pattern === 'Burst' && secondaryColor && ( From cce07e3af7ea9007e39a271da697f07d188e867d Mon Sep 17 00:00:00 2001 From: ChaoticGood007 Date: Tue, 12 May 2026 21:33:51 -0500 Subject: [PATCH 4/5] fix: Wrap long text in location picker selection button --- CHANGELOG.md | 1 + src/components/LocationPicker.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c65b82..bc2c282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Secondary Patterns:** Standardized Secondary Pattern names (e.g. "Halo/Overmold") to consistently display friendly labels across both the editor forms and the detail views instead of the raw database values. - **Disc Visuals:** Fixed an SVG rendering bug where translucent or "Clear" secondary colors in Half 'n Half and Halo patterns incorrectly showed the base disc color underneath them. +- **Location Picker:** Fixed an issue where heavily nested location paths would be truncated after selection. The dropdown button now expands vertically to wrap long text. ## [0.5.2] - 2026-05-07 diff --git a/src/components/LocationPicker.tsx b/src/components/LocationPicker.tsx index 2369da7..8f7de50 100644 --- a/src/components/LocationPicker.tsx +++ b/src/components/LocationPicker.tsx @@ -74,14 +74,14 @@ export default function LocationPicker({ tree, value, onChange, className = '' } : 'border-slate-200 bg-slate-50 hover:border-indigo-200' }`} > - + {selected ? ( <> - {selected.inBag && } - {selected.path} + {selected.inBag && } + {selected.path} ) : ( - No location set + No location set )}
From cdba0e4512af7d04d10882ef6567fb5aad58895b Mon Sep 17 00:00:00 2001 From: ChaoticGood007 Date: Tue, 12 May 2026 22:30:23 -0500 Subject: [PATCH 5/5] feat: 0.6.0 precise color picker, custom molds, and layout fixes --- CHANGELOG.md | 3 +- prisma/schema.prisma | 2 + src/app/actions/inventory.ts | 10 ++ src/app/v/[vaultId]/bagcheck/page.tsx | 2 + src/components/AddDiscForm.tsx | 138 +++++++++++++++-------- src/components/BagCheckList.tsx | 4 + src/components/ColorInput.tsx | 129 +++++++++++++++++++++ src/components/DiscDetailView.tsx | 4 + src/components/DiscPreview.tsx | 8 +- src/components/EditDiscForm.tsx | 133 ++++++++++++++-------- src/components/FlightChart.tsx | 4 + src/components/InventoryInfiniteList.tsx | 4 + src/components/InventoryList.tsx | 4 + src/components/PreviewModal.tsx | 4 + src/lib/colorParser.ts | 33 ++++-- src/lib/constants.ts | 2 +- 16 files changed, 372 insertions(+), 112 deletions(-) create mode 100644 src/components/ColorInput.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2c282..797c036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.6.0] - 2026-05-12 ### Added +- **Precise Color Picker & Live Preview:** Replaced the generic color text inputs in Add and Edit forms with an interactive Color Input component. This new component combines text search with a native hex color picker and opacity slider, providing accurate visual representation while maintaining searchability. The form now features a live-updating `DiscPreview` that reflects changes in real-time. Hex color overrides seamlessly propagate to inventory lists, bag checklists, and flight charts. - **Custom Molds:** Added the ability to create Custom Molds directly inline from the "Add Disc" form. If a disc does not exist in the Discit API, users can now define the disc's name, brand, category, and flight numbers to add it to their vault. ### Fixed -- **Secondary Patterns:** Standardized Secondary Pattern names (e.g. "Halo/Overmold") to consistently display friendly labels across both the editor forms and the detail views instead of the raw database values. +- **Secondary Patterns:** Standardized Secondary Pattern names (e.g. "Halo/Rim") to consistently display friendly labels across both the editor forms and the detail views instead of the raw database values. Fixed an issue where long pattern names would truncate in form inputs on smaller screens. - **Disc Visuals:** Fixed an SVG rendering bug where translucent or "Clear" secondary colors in Half 'n Half and Halo patterns incorrectly showed the base disc color underneath them. - **Location Picker:** Fixed an issue where heavily nested location paths would be truncated after selection. The dropdown button now expands vertically to wrap long text. diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 833091c..7916447 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,6 +58,8 @@ model Inventory { userGlide Float? userTurn Float? secondaryColor String? + colorHex String? + secondaryColorHex String? secondaryPattern String? collection DiscCollection? @relation(fields: [collectionId], references: [id]) mold Mold @relation(fields: [moldId], references: [id]) diff --git a/src/app/actions/inventory.ts b/src/app/actions/inventory.ts index bfe2aa6..24a295c 100644 --- a/src/app/actions/inventory.ts +++ b/src/app/actions/inventory.ts @@ -52,6 +52,8 @@ export async function addDisc(formData: FormData) { const userGlide = parseUserFlight(formData.get('userGlide')) const userTurn = parseUserFlight(formData.get('userTurn')) const userFade = parseUserFlight(formData.get('userFade')) + const colorHex = (formData.get('colorHex') as string) || null + const secondaryColorHex = (formData.get('secondaryColorHex') as string) || null if (!moldId) throw new Error('Mold ID is required') @@ -63,6 +65,8 @@ export async function addDisc(formData: FormData) { weight, color, secondaryColor, + colorHex, + secondaryColorHex, secondaryPattern, plastic, stamp, @@ -103,6 +107,8 @@ export async function updateDisc(id: string, formData: FormData) { const userGlide = parseUserFlight(formData.get('userGlide')) const userTurn = parseUserFlight(formData.get('userTurn')) const userFade = parseUserFlight(formData.get('userFade')) + const colorHex = (formData.get('colorHex') as string) || null + const secondaryColorHex = (formData.get('secondaryColorHex') as string) || null try { await prisma.inventory.update({ @@ -112,6 +118,8 @@ export async function updateDisc(id: string, formData: FormData) { weight, color, secondaryColor, + colorHex, + secondaryColorHex, secondaryPattern, plastic, stamp, @@ -243,7 +251,9 @@ export async function importDiscs(records: any[], targetCollectionId?: string) { plastic: record.plastic || null, weight: parseSafeFloat(record.weight, NaN) || null, color: record.color || null, + colorHex: record.colorHex || null, secondaryColor: record.secondaryColor || null, + secondaryColorHex: record.secondaryColorHex || null, secondaryPattern: record.secondaryPattern || null, stamp: record.stamp || null, stampFoil: record.stampFoil || null, diff --git a/src/app/v/[vaultId]/bagcheck/page.tsx b/src/app/v/[vaultId]/bagcheck/page.tsx index 5aa00a2..0e9d3b6 100644 --- a/src/app/v/[vaultId]/bagcheck/page.tsx +++ b/src/app/v/[vaultId]/bagcheck/page.tsx @@ -81,7 +81,9 @@ export default async function BagCheckPage({ plastic: d.plastic, weight: d.weight, color: d.color, + colorHex: d.colorHex, secondaryColor: d.secondaryColor, + secondaryColorHex: d.secondaryColorHex, secondaryPattern: d.secondaryPattern, stampFoil: d.stampFoil, location: d.location, diff --git a/src/components/AddDiscForm.tsx b/src/components/AddDiscForm.tsx index 6bc89d5..ab41af4 100644 --- a/src/components/AddDiscForm.tsx +++ b/src/components/AddDiscForm.tsx @@ -23,6 +23,8 @@ import { Search, Loader2, Plus } from 'lucide-react' import LocationPicker from '@/components/LocationPicker' import { type LocationNode } from '@/lib/locationTree' import { SECONDARY_PATTERN_LABELS } from '@/lib/constants' +import ColorInput from './ColorInput' +import DiscPreview from './DiscPreview' interface Mold { id: string @@ -46,6 +48,18 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { const [isCustomMode, setIsCustomMode] = useState(false) const [isCreatingCustom, setIsCreatingCustom] = useState(false) + // Preview State + const [previewState, setPreviewState] = useState({ + color: '', + colorHex: null as string | null, + secondaryColor: '', + secondaryColorHex: null as string | null, + secondaryPattern: '', + stampFoil: '', + weight: '', + plastic: '' + }) + const handleCreateCustomMold = async () => { const brandInput = document.querySelector('input[name="customBrand"]') as HTMLInputElement const nameInput = document.querySelector('input[name="customName"]') as HTMLInputElement @@ -244,6 +258,7 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { step="0.1" name="weight" placeholder="175" + onChange={e => setPreviewState(s => ({ ...s, weight: e.target.value }))} className="w-full px-5 py-4 bg-white border border-slate-200 rounded-2xl font-black text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" />
@@ -253,6 +268,7 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { type="text" name="plastic" placeholder="Star, Champion..." + onChange={e => setPreviewState(s => ({ ...s, plastic: e.target.value }))} className="w-full px-5 py-4 bg-white border border-slate-200 rounded-2xl font-black text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" /> @@ -266,59 +282,83 @@ export default function AddDiscForm({ vaultId, tree }: AddDiscFormProps) { -
-
- - -
-
- - +
+
+
+ setPreviewState(s => ({ ...s, color: text, colorHex: hex }))} + /> + setPreviewState(s => ({ ...s, secondaryColor: text, secondaryColorHex: hex }))} + /> +
+ +
+
+ + +
+
+ + +
+
+ + setPreviewState(s => ({ ...s, stampFoil: e.target.value }))} + className="w-full px-3 py-4 bg-white border border-slate-200 rounded-2xl font-black text-sm text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" + /> +
+
-
- - + + {/* Live Preview */} +
+ {selectedMold || isCustomMode ? ( + + ) : ( +
+
+ Preview +
+

Select a mold to see preview

+
+ )}
-
-
- - -
-
- - -
-
+
diff --git a/src/components/BagCheckList.tsx b/src/components/BagCheckList.tsx index b9b4c6e..0ff6e76 100644 --- a/src/components/BagCheckList.tsx +++ b/src/components/BagCheckList.tsx @@ -33,7 +33,9 @@ interface BagCheckDisc { plastic: string | null weight: number | null color: string | null + colorHex: string | null secondaryColor: string | null + secondaryColorHex: string | null secondaryPattern: string | null stampFoil: string | null location: string | null @@ -292,7 +294,9 @@ export default function BagCheckList({ vaultName, bags, discs }: BagCheckListPro
void +} + +export default function ColorInput({ name, label, initialText, initialHex, onChange }: ColorInputProps) { + const [text, setText] = useState(initialText || '') + + // Track if the user has manually touched the color or opacity controls + const [isOverridden, setIsOverridden] = useState(!!initialHex) + + // Derive initial internal states + const [hex, setHex] = useState(() => { + if (initialHex) return initialHex.slice(0, 7) + return tinycolor(parseDiscColor(initialText || '')).toHexString() + }) + + const [opacity, setOpacity] = useState(() => { + if (initialHex && initialHex.length === 9) { + return parseInt(initialHex.slice(7, 9), 16) / 255 + } + if (initialHex) return 1 + return tinycolor(parseDiscColor(initialText || '')).getAlpha() + }) + + // When text changes (and not overridden), auto-update the swatch and opacity + useEffect(() => { + if (!isOverridden) { + const parsed = tinycolor(parseDiscColor(text)) + setHex(parsed.toHexString()) + setOpacity(parsed.getAlpha()) + } + }, [text, isOverridden]) + + // Use a ref for onChange to avoid infinite loops if it's passed as an inline function + const onChangeRef = useRef(onChange) + useEffect(() => { + onChangeRef.current = onChange + }, [onChange]) + + // Fire onChange whenever the derived output changes + useEffect(() => { + if (onChangeRef.current) { + const hex8 = isOverridden ? `${hex}${Math.round(opacity * 255).toString(16).padStart(2, '0')}` : null + onChangeRef.current(text, hex8) + } + }, [text, hex, opacity, isOverridden]) + + const hex8Output = isOverridden ? `${hex}${Math.round(opacity * 255).toString(16).padStart(2, '0')}` : '' + + return ( +
+
+ + +
+ +
+ setText(e.target.value)} + placeholder="Blue, Pink..." + className="w-full pl-5 pr-14 py-4 bg-white border border-slate-200 rounded-2xl font-black text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm text-ellipsis overflow-hidden whitespace-nowrap" + /> + + {/* Color Picker Swatch */} +
+ { + setHex(e.target.value) + setIsOverridden(true) + }} + className="absolute -inset-2 w-12 h-12 cursor-pointer bg-transparent border-0 outline-none" + /> +
+
+ + {/* Opacity Slider */} +
+
+ Opacity + + {Math.round(opacity * 100)}% + +
+ { + setOpacity(parseFloat(e.target.value)) + setIsOverridden(true) + }} + className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-500" + /> +
+ + {/* Hidden input for the DB hex override */} + +
+ ) +} diff --git a/src/components/DiscDetailView.tsx b/src/components/DiscDetailView.tsx index 70caf3b..4e094ce 100644 --- a/src/components/DiscDetailView.tsx +++ b/src/components/DiscDetailView.tsx @@ -12,7 +12,9 @@ interface DiscDetailViewProps { collectionId: string | null weight: number | null color: string | null + colorHex: string | null secondaryColor?: string | null + secondaryColorHex?: string | null secondaryPattern?: string | null plastic: string | null stamp: string | null @@ -133,7 +135,9 @@ export default function DiscDetailView({ disc, categoryColors, vaultId, bagPaths {disc.color && ( (disc.userTurn !== null && disc.userTurn !== undefined ? String(disc.userTurn) : '') const [userFade, setUserFade] = useState(disc.userFade !== null && disc.userFade !== undefined ? String(disc.userFade) : '') const hasTuning = userGlide !== '' || userTurn !== '' || userFade !== '' + + // Preview State + const [previewState, setPreviewState] = useState({ + color: disc.color || '', + colorHex: disc.colorHex, + secondaryColor: disc.secondaryColor || '', + secondaryColorHex: disc.secondaryColorHex, + secondaryPattern: disc.secondaryPattern || '', + stampFoil: disc.stampFoil || '', + weight: disc.weight ? String(disc.weight) : '', + plastic: disc.plastic || '' + }) const currentTree = (() => { if (!selectedVaultId) return [] @@ -142,6 +158,7 @@ export default function EditDiscForm({ disc, collections }: Omit setPreviewState(s => ({ ...s, weight: e.target.value }))} className="w-full px-5 py-4 bg-white border border-slate-200 rounded-2xl font-black text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" />
@@ -151,6 +168,7 @@ export default function EditDiscForm({ disc, collections }: Omit setPreviewState(s => ({ ...s, plastic: e.target.value }))} className="w-full px-5 py-4 bg-white border border-slate-200 rounded-2xl font-black text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" />
@@ -165,57 +183,74 @@ export default function EditDiscForm({ disc, collections }: Omit
-
-
- - -
-
- - -
-
- - -
-
+
+
+
+ setPreviewState(s => ({ ...s, color: text, colorHex: hex }))} + /> + setPreviewState(s => ({ ...s, secondaryColor: text, secondaryColorHex: hex }))} + /> +
-
-
- - +
+
+ + +
+
+ + +
+
+ + setPreviewState(s => ({ ...s, stampFoil: e.target.value }))} + className="w-full px-3 py-4 bg-white border border-slate-200 rounded-2xl font-black text-sm text-slate-900 focus:ring-4 focus:ring-indigo-100 focus:border-indigo-400 transition-all outline-none shadow-sm" + /> +
+
-
- - +
diff --git a/src/components/FlightChart.tsx b/src/components/FlightChart.tsx index d83aed9..fd53a63 100644 --- a/src/components/FlightChart.tsx +++ b/src/components/FlightChart.tsx @@ -35,7 +35,9 @@ interface Disc { fade: number } color?: string | null + colorHex?: string | null secondaryColor?: string | null + secondaryColorHex?: string | null secondaryPattern?: string | null stampFoil?: string | null plastic?: string | null @@ -336,7 +338,9 @@ export default function FlightChart({ discs, vaultId, defaultShowFlightPaths = f { + if (!isClear) return hex; + const c = tinycolor(hex).toRgb(); + return `rgba(${c.r}, ${c.g}, ${c.b}, 0.5)`; + } + + if (hexOverride) { + // If we have an 8-char hex, the alpha is embedded in the hex. + // If it's a 6-char hex and isClear, we apply 0.5 opacity. + const parsedHex = tinycolor(hexOverride); + if (parsedHex.isValid()) { + // If it already has an alpha < 1, just return its hex8 or rgba + if (parsedHex.getAlpha() < 1) { + return parsedHex.toRgbString(); + } + return applyClear(parsedHex.toHexString()); + } + } + // 1. Handle specific Disc Golf exact matches / overrides if (colorStr === 'glow' || colorStr === 'moonshine' || colorStr === 'eclipse') { return isClear ? 'rgba(220, 242, 181, 0.3)' : '#dcf2b5'; @@ -40,12 +59,6 @@ export function parseDiscColor(input: string | null | undefined): string { if (hasDark) colorStr = colorStr.replace('dark', '').trim(); if (hasDeep) colorStr = colorStr.replace('deep', '').trim(); - const applyClear = (hex: string) => { - if (!isClear) return hex; - const c = tinycolor(hex).toRgb(); - return `rgba(${c.r}, ${c.g}, ${c.b}, 0.5)`; - } - const applyModifiers = (color: tinycolor.Instance) => { if (hasNeon) color = color.saturate(100).lighten(10); if (hasLight) color = color.lighten(20); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ced58a0..810b165 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,7 +7,7 @@ export const DEFAULT_CATEGORY_COLORS: Record = { } export const SECONDARY_PATTERN_LABELS: Record = { - 'Halo': 'Halo/Overmold', + 'Halo': 'Halo/Rim', 'Burst': 'Center Burst', 'Split': 'Half \'n Half', 'Swirl': 'Swirly',