Compare commits
9 Commits
2bf0ac0f3e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 467bd88f95 | |||
| d75a30f459 | |||
| 06fa208dd8 | |||
| 883f76e488 | |||
| d8a9e9fd0a | |||
| 5c00be59f1 | |||
| 004698b604 | |||
| 096daffb3e | |||
| b179a88d4c |
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -160,7 +160,7 @@
|
||||
"oily",
|
||||
"medium-full body",
|
||||
"silky",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"smooth",
|
||||
"slightly warming",
|
||||
@@ -774,7 +774,7 @@
|
||||
],
|
||||
"texture": [
|
||||
"oily and waxy",
|
||||
"creamy and rounded",
|
||||
"creamy and rounded-sm",
|
||||
"medium weight",
|
||||
"silky with a gentle grip",
|
||||
"well-structured balancing sweetness and dryness",
|
||||
@@ -880,7 +880,7 @@
|
||||
"oily",
|
||||
"viscous",
|
||||
"silky",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"medium-bodied",
|
||||
"slightly-dry",
|
||||
@@ -1038,7 +1038,7 @@
|
||||
"polished",
|
||||
"well-integrated",
|
||||
"slightly oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"smooth",
|
||||
"lively"
|
||||
]
|
||||
@@ -1235,7 +1235,7 @@
|
||||
"softly effervescent",
|
||||
"polished oak feel",
|
||||
"refreshingly bright",
|
||||
"lean yet rounded"
|
||||
"lean yet rounded-sm"
|
||||
]
|
||||
},
|
||||
"Aultmore": {
|
||||
@@ -1527,7 +1527,7 @@
|
||||
"creamy",
|
||||
"silky",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"polished",
|
||||
"mellow",
|
||||
"gentle",
|
||||
@@ -1588,7 +1588,7 @@
|
||||
"Lingering sea-salt",
|
||||
"Sweet lemon",
|
||||
"Lime zest",
|
||||
"Dried apple ring",
|
||||
"Dried apple ring-3",
|
||||
"Vanilla oak",
|
||||
"Gentle oak tannin",
|
||||
"Black pepper",
|
||||
@@ -1883,7 +1883,7 @@
|
||||
"polished",
|
||||
"slightly resinous",
|
||||
"lively oak spice",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"structured",
|
||||
"sappy (young oak feel)"
|
||||
]
|
||||
@@ -2001,7 +2001,7 @@
|
||||
"oily",
|
||||
"creamy",
|
||||
"silky",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"medium-bodied",
|
||||
"polished",
|
||||
"gently spirity",
|
||||
@@ -2156,7 +2156,7 @@
|
||||
"oily",
|
||||
"creamy",
|
||||
"medium to full-bodied",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"silky",
|
||||
"slightly waxy",
|
||||
"balanced",
|
||||
@@ -2415,7 +2415,7 @@
|
||||
"grippy",
|
||||
"structured tannins",
|
||||
"cask-driven",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"robust",
|
||||
"powerful",
|
||||
@@ -2544,7 +2544,7 @@
|
||||
"waxy",
|
||||
"silky",
|
||||
"rich",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"medium-to-full bodied",
|
||||
"coating",
|
||||
"smooth",
|
||||
@@ -2781,7 +2781,7 @@
|
||||
"light-to-medium body",
|
||||
"soft",
|
||||
"polished",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-structured",
|
||||
"approachable",
|
||||
"gentle",
|
||||
@@ -2868,7 +2868,7 @@
|
||||
"creamy toffee and caramelized sugar",
|
||||
"vanilla oak spicing (cinnamon, white pepper)",
|
||||
"nutty undertones (almond, hazelnut)",
|
||||
"soft rounded bitterness (cocoa nibs, orange marmalade)",
|
||||
"soft rounded-sm bitterness (cocoa nibs, orange marmalade)",
|
||||
"mild stone fruit (apricot) and dried sultana",
|
||||
"waxy orchard skin texture and gentle oiliness",
|
||||
"cereal maltiness with a slight biscuit edge",
|
||||
@@ -2888,7 +2888,7 @@
|
||||
],
|
||||
"texture": [
|
||||
"waxy mouthfeel that softens with age",
|
||||
"creamy and rounded",
|
||||
"creamy and rounded-sm",
|
||||
"oily yet clean, not heavy",
|
||||
"slightly coating but not syrupy",
|
||||
"polished oak structure underneath",
|
||||
@@ -2992,7 +2992,7 @@
|
||||
"polished oak",
|
||||
"soft tannins",
|
||||
"gentle heat",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"clean",
|
||||
"crisp",
|
||||
"gliding",
|
||||
@@ -3591,7 +3591,7 @@
|
||||
"silky",
|
||||
"medium-bodied",
|
||||
"slightly oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-integrated alcohol",
|
||||
"polished tannins",
|
||||
"sprightly",
|
||||
@@ -3820,7 +3820,7 @@
|
||||
"viscous",
|
||||
"creamy",
|
||||
"velvety",
|
||||
"rounded mouthfeel",
|
||||
"rounded-sm mouthfeel",
|
||||
"slightly chewy",
|
||||
"resinous",
|
||||
"warming",
|
||||
@@ -4776,7 +4776,7 @@
|
||||
"silky",
|
||||
"oily",
|
||||
"creamy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"medium-bodied",
|
||||
"spry",
|
||||
@@ -5066,7 +5066,7 @@
|
||||
"well-balanced",
|
||||
"medium-to-full body",
|
||||
"coating",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"polished",
|
||||
"lively",
|
||||
"gentle",
|
||||
@@ -5424,7 +5424,7 @@
|
||||
"smooth",
|
||||
"creamy",
|
||||
"soft",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"medium-bodied",
|
||||
"polished",
|
||||
"well-balanced",
|
||||
@@ -5602,7 +5602,7 @@
|
||||
"quiet floral echo (heather)"
|
||||
],
|
||||
"texture": [
|
||||
"creamy and rounded",
|
||||
"creamy and rounded-sm",
|
||||
"silky with gentle oiliness",
|
||||
"medium body, never heavy",
|
||||
"polished oak grip (fine-grained tannins)",
|
||||
@@ -5861,7 +5861,7 @@
|
||||
"oily",
|
||||
"creamy",
|
||||
"velvety",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"medium-bodied",
|
||||
"approachable",
|
||||
"slightly waxy",
|
||||
@@ -6002,7 +6002,7 @@
|
||||
"smooth",
|
||||
"silky",
|
||||
"slightly waxy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"gentle",
|
||||
"well-balanced",
|
||||
"not overly viscous",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"silky",
|
||||
"oily (light)",
|
||||
"soft",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"clean",
|
||||
"crisp",
|
||||
"approachable",
|
||||
@@ -6230,7 +6230,7 @@
|
||||
"waxy",
|
||||
"silky",
|
||||
"medium-weight",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-structured",
|
||||
"balanced",
|
||||
"clean",
|
||||
@@ -6427,7 +6427,7 @@
|
||||
"waxy-coated",
|
||||
"chalky/mineral grip",
|
||||
"well-structured",
|
||||
"rounded and balanced",
|
||||
"rounded-sm and balanced",
|
||||
"smooth entry",
|
||||
"zesty lift",
|
||||
"slightly drying oak"
|
||||
@@ -6507,7 +6507,7 @@
|
||||
"waxy",
|
||||
"silky",
|
||||
"medium-bodied",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"slightly resinous",
|
||||
"prickly spice",
|
||||
"chewy",
|
||||
@@ -7021,7 +7021,7 @@
|
||||
"oily",
|
||||
"waxy",
|
||||
"coastal mouthfeel",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"creamy",
|
||||
"silky",
|
||||
@@ -7265,7 +7265,7 @@
|
||||
"smooth and approachable",
|
||||
"medium-bodied",
|
||||
"slightly oily",
|
||||
"soft and rounded",
|
||||
"soft and rounded-sm",
|
||||
"creamy",
|
||||
"well-integrated alcohol",
|
||||
"gentle spice",
|
||||
@@ -7648,7 +7648,7 @@
|
||||
"Slightly prickly",
|
||||
"Weighty yet agile",
|
||||
"Chewy",
|
||||
"Soft and rounded",
|
||||
"Soft and rounded-sm",
|
||||
"Peppery heat"
|
||||
]
|
||||
},
|
||||
@@ -7949,7 +7949,7 @@
|
||||
"waxy / coating",
|
||||
"oily",
|
||||
"medium-bodied",
|
||||
"softly rounded",
|
||||
"softly rounded-sm",
|
||||
"creamy",
|
||||
"silky",
|
||||
"slightly drying",
|
||||
@@ -8003,7 +8003,7 @@
|
||||
"oily and resinous",
|
||||
"creamy and luscious",
|
||||
"viscous mouthfeel",
|
||||
"rounded and polished",
|
||||
"rounded-sm and polished",
|
||||
"silky with grip",
|
||||
"balanced warmth",
|
||||
"velvety oak",
|
||||
@@ -8145,7 +8145,7 @@
|
||||
"silky",
|
||||
"creamy",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"plush",
|
||||
"well-integrated",
|
||||
"velvety",
|
||||
@@ -8258,12 +8258,12 @@
|
||||
"dry, gently cereal/biscuity tail",
|
||||
"clean, crisp acidity (a touch of citrus)",
|
||||
"overall dryness in later stages",
|
||||
"no sulphur, very smooth and rounded"
|
||||
"no sulphur, very smooth and rounded-sm"
|
||||
],
|
||||
"texture": [
|
||||
"smooth and approachable",
|
||||
"medium-bodied",
|
||||
"creamy and rounded",
|
||||
"creamy and rounded-sm",
|
||||
"silky and polished",
|
||||
"slightly oily in the glass but light on the palate",
|
||||
"well-balanced",
|
||||
@@ -8319,7 +8319,7 @@
|
||||
"smooth with a gentle prickle",
|
||||
"soft and approachable",
|
||||
"salty tactile impression",
|
||||
"rounded oak structure without heaviness",
|
||||
"rounded-sm oak structure without heaviness",
|
||||
"clean, brisk progression across the palate"
|
||||
]
|
||||
},
|
||||
@@ -8758,7 +8758,7 @@
|
||||
"silky",
|
||||
"slightly chewy",
|
||||
"polished",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"approachable",
|
||||
"maritime grip",
|
||||
@@ -9322,7 +9322,7 @@
|
||||
"creamy",
|
||||
"oily",
|
||||
"waxy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"smooth",
|
||||
"soft",
|
||||
"mouth-coating",
|
||||
@@ -9687,7 +9687,7 @@
|
||||
"polished",
|
||||
"slightly oily",
|
||||
"creamy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-balanced",
|
||||
"slightly drying",
|
||||
"crisp",
|
||||
@@ -9850,7 +9850,7 @@
|
||||
"silky",
|
||||
"creamy",
|
||||
"velvety",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"medium-bodied",
|
||||
"oily",
|
||||
"polished",
|
||||
@@ -9919,7 +9919,7 @@
|
||||
"Clean and medium length",
|
||||
"Subtle coconut",
|
||||
"A touch of herbal freshness",
|
||||
"Smooth and rounded close",
|
||||
"Smooth and rounded-sm close",
|
||||
"Fading floral note"
|
||||
],
|
||||
"texture": [
|
||||
@@ -9929,7 +9929,7 @@
|
||||
"Soft and approachable",
|
||||
"Slightly oily",
|
||||
"Polished",
|
||||
"Even and rounded",
|
||||
"Even and rounded-sm",
|
||||
"Mellow",
|
||||
"Clean and fresh",
|
||||
"Non-aggressive"
|
||||
@@ -10140,7 +10140,7 @@
|
||||
"creamy",
|
||||
"well-integrated alcohol",
|
||||
"polished oak influence",
|
||||
"soft and rounded",
|
||||
"soft and rounded-sm",
|
||||
"gently warming",
|
||||
"bright and lively",
|
||||
"clean and crisp"
|
||||
@@ -10267,7 +10267,7 @@
|
||||
"prickly pepper",
|
||||
"well-integrated heat",
|
||||
"chewy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"silky",
|
||||
"moderately weighted"
|
||||
]
|
||||
@@ -10409,7 +10409,7 @@
|
||||
"medium-bodied",
|
||||
"well-integrated alcohol",
|
||||
"polished oak",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"luscious",
|
||||
"slightly viscous",
|
||||
"smooth"
|
||||
@@ -10749,7 +10749,7 @@
|
||||
"thick",
|
||||
"viscous",
|
||||
"mouth-coating",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-integrated",
|
||||
"polished",
|
||||
"slightly drying",
|
||||
@@ -10845,7 +10845,7 @@
|
||||
"smooth",
|
||||
"fresh",
|
||||
"sprightly",
|
||||
"rounded"
|
||||
"rounded-sm"
|
||||
]
|
||||
},
|
||||
"Jameson": {
|
||||
@@ -10920,7 +10920,7 @@
|
||||
"creamy",
|
||||
"velvety",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"mellow",
|
||||
"balanced",
|
||||
"soft",
|
||||
@@ -11185,7 +11185,7 @@
|
||||
"waxy, candle-wax polish tone in older bottlings"
|
||||
],
|
||||
"texture": [
|
||||
"creamy, silky and rounded mouthfeel",
|
||||
"creamy, silky and rounded-sm mouthfeel",
|
||||
"viscous and coating (especially single pot still proofs)",
|
||||
"slightly oily and waxy",
|
||||
"buttery and smooth (vanilla custard texture)",
|
||||
@@ -11489,7 +11489,7 @@
|
||||
],
|
||||
"texture": [
|
||||
"Creamy",
|
||||
"Silky and rounded",
|
||||
"Silky and rounded-sm",
|
||||
"Medium viscosity",
|
||||
"Oiliness that coats the palate",
|
||||
"Soft and approachable",
|
||||
@@ -11868,7 +11868,7 @@
|
||||
"well-balanced",
|
||||
"lightly oily yet clean",
|
||||
"medium-bodied",
|
||||
"rounded and cohesive",
|
||||
"rounded-sm and cohesive",
|
||||
"soft-spiced",
|
||||
"refreshing acidity",
|
||||
"velvety oak impression",
|
||||
@@ -12021,7 +12021,7 @@
|
||||
"elegant and restrained",
|
||||
"refined and precise",
|
||||
"high-definition clarity",
|
||||
"smooth, rounded edges",
|
||||
"smooth, rounded-sm edges",
|
||||
"spry acidity (citrus lift)",
|
||||
"tight-grained oak feel",
|
||||
"lifted and airy",
|
||||
@@ -12044,7 +12044,7 @@
|
||||
"hint of coconut and banana from Mizunara/inactive oak"
|
||||
],
|
||||
"taste": [
|
||||
"soft, rounded mouthfeel",
|
||||
"soft, rounded-sm mouthfeel",
|
||||
"orchard fruit sweetness (pear, apple)",
|
||||
"peach and apricot preserve",
|
||||
"light floral notes (lilac, jasmine)",
|
||||
@@ -12072,7 +12072,7 @@
|
||||
"texture": [
|
||||
"silky and smooth",
|
||||
"light to medium body",
|
||||
"crisp yet rounded",
|
||||
"crisp yet rounded-sm",
|
||||
"polished and clean",
|
||||
"well-integrated alcohol",
|
||||
"slightly oily with a fresh core",
|
||||
@@ -12226,7 +12226,7 @@
|
||||
"Subtle smoke/char - faint ex-bourbon and Mizunara influence"
|
||||
],
|
||||
"taste": [
|
||||
"Velvety malt - soft, rounded mid-palate",
|
||||
"Velvety malt - soft, rounded-sm mid-palate",
|
||||
"Pear and Nashi fruit - clean orchard sweetness",
|
||||
"White peach and apricot - gentle stone fruit",
|
||||
"Citrus zest - lemon and yuzu acidity for balance",
|
||||
@@ -12615,7 +12615,7 @@
|
||||
"creamy",
|
||||
"silky",
|
||||
"smooth",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"firm",
|
||||
"robust",
|
||||
@@ -12764,7 +12764,7 @@
|
||||
"silky",
|
||||
"full-bodied",
|
||||
"rich",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"chewy",
|
||||
"polished",
|
||||
"luscious",
|
||||
@@ -12848,7 +12848,7 @@
|
||||
"rich",
|
||||
"viscous",
|
||||
"structured",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"bold",
|
||||
"smooth",
|
||||
"dense",
|
||||
@@ -12985,7 +12985,7 @@
|
||||
"chewy",
|
||||
"well-structured",
|
||||
"balanced",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"warming",
|
||||
"spicy prickle",
|
||||
"smooth",
|
||||
@@ -13067,7 +13067,7 @@
|
||||
"hot and vibrant",
|
||||
"slightly prickly",
|
||||
"buttery",
|
||||
"rounded and integrated",
|
||||
"rounded-sm and integrated",
|
||||
"unctuous",
|
||||
"mouth-filling",
|
||||
"thick pour"
|
||||
@@ -13176,7 +13176,7 @@
|
||||
"dense",
|
||||
"full-bodied",
|
||||
"rich",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-integrated",
|
||||
"balanced",
|
||||
"layered",
|
||||
@@ -13273,7 +13273,7 @@
|
||||
"creamy",
|
||||
"oily",
|
||||
"medium-bodied",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"slightly spicy",
|
||||
"warm",
|
||||
"velvety",
|
||||
@@ -13338,7 +13338,7 @@
|
||||
"luscious",
|
||||
"chewy",
|
||||
"rich",
|
||||
"rounded"
|
||||
"rounded-sm"
|
||||
]
|
||||
},
|
||||
"Elijah Craig": {
|
||||
@@ -13719,7 +13719,7 @@
|
||||
"chewy",
|
||||
"syrupy",
|
||||
"rich",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"coating",
|
||||
"smooth",
|
||||
"balanced",
|
||||
@@ -13821,7 +13821,7 @@
|
||||
"velvety",
|
||||
"silky",
|
||||
"chewy",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-integrated",
|
||||
"balanced",
|
||||
"smooth",
|
||||
@@ -13966,7 +13966,7 @@
|
||||
"smooth",
|
||||
"creamy",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"balanced",
|
||||
"mellow",
|
||||
"slightly viscous",
|
||||
@@ -14118,7 +14118,7 @@
|
||||
"creamy",
|
||||
"velvety",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"syrupy",
|
||||
"chewy",
|
||||
"medium-bodied",
|
||||
@@ -14224,7 +14224,7 @@
|
||||
"spirited",
|
||||
"well-integrated",
|
||||
"structured",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"luscious",
|
||||
"layered",
|
||||
"viscous",
|
||||
@@ -14365,7 +14365,7 @@
|
||||
"Creamy mouthfeel",
|
||||
"Dense and rich",
|
||||
"Syrupy sweetness balanced by oak",
|
||||
"Soft and rounded",
|
||||
"Soft and rounded-sm",
|
||||
"Velvety tannins",
|
||||
"Warming spice prickle",
|
||||
"Chewy and substantial",
|
||||
@@ -14506,7 +14506,7 @@
|
||||
"full-bodied",
|
||||
"rich",
|
||||
"coating",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"dense",
|
||||
"bold",
|
||||
"thick",
|
||||
@@ -14592,7 +14592,7 @@
|
||||
"chewy",
|
||||
"rich",
|
||||
"well-integrated",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"luscious",
|
||||
"dense",
|
||||
"polished",
|
||||
@@ -14818,7 +14818,7 @@
|
||||
"Luscious and succulent",
|
||||
"Polished and well-integrated",
|
||||
"Silky with a subtle grip",
|
||||
"Buttery and rounded",
|
||||
"Buttery and rounded-sm",
|
||||
"Warm and enveloping",
|
||||
"Concentrated and intense"
|
||||
]
|
||||
@@ -14870,7 +14870,7 @@
|
||||
"Oiliness from pot still character",
|
||||
"Creamy mouthfeel with soft oak grip",
|
||||
"Slightly viscous with tropical weight",
|
||||
"Polished and rounded tannins",
|
||||
"Polished and rounded-sm tannins",
|
||||
"Bright but not sharp, approachable",
|
||||
"Warming spice prickle",
|
||||
"Creamy vanilla custard texture",
|
||||
@@ -14931,7 +14931,7 @@
|
||||
"polished oak texture",
|
||||
"brine-tinged grip",
|
||||
"cocoa-dusted smoothness",
|
||||
"rounded yet angular spice",
|
||||
"rounded-sm yet angular spice",
|
||||
"medium-to-full bodied",
|
||||
"lively pepper-prickly sensation"
|
||||
]
|
||||
@@ -15027,7 +15027,7 @@
|
||||
"syrupy and viscous",
|
||||
"silky and velvety",
|
||||
"coating and mouth-coating",
|
||||
"rounded and plush",
|
||||
"rounded-sm and plush",
|
||||
"spicy-prickly (white pepper)",
|
||||
"warming yet refreshing",
|
||||
"balanced heat from virgin oak",
|
||||
@@ -15152,7 +15152,7 @@
|
||||
"oily (coat the palate)",
|
||||
"slightly syrupy",
|
||||
"well-integrated alcohol",
|
||||
"rounded oak texture",
|
||||
"rounded-sm oak texture",
|
||||
"polished tannin",
|
||||
"creamy (from vanilla/caramel)",
|
||||
"bright acidity (wine-cask lift)"
|
||||
@@ -15706,7 +15706,7 @@
|
||||
],
|
||||
"texture": [
|
||||
"Silky and creamy mouthfeel",
|
||||
"Medium body with a rounded profile",
|
||||
"Medium body with a rounded-sm profile",
|
||||
"Polished and gently coating",
|
||||
"Juicy fruit sensation",
|
||||
"Slightly waxy on the mid-palate",
|
||||
@@ -15955,7 +15955,7 @@
|
||||
"balanced dryness without harsh astringency"
|
||||
],
|
||||
"texture": [
|
||||
"silky and rounded mouthfeel",
|
||||
"silky and rounded-sm mouthfeel",
|
||||
"medium body, neither oily nor watery",
|
||||
"slightly waxy on the palate",
|
||||
"creamy texture reminiscent of crème anglaise",
|
||||
@@ -16073,7 +16073,7 @@
|
||||
"well-integrated alcohol",
|
||||
"slightly drying",
|
||||
"silky",
|
||||
"rounded mouthfeel",
|
||||
"rounded-sm mouthfeel",
|
||||
"soft and approachable",
|
||||
"polished oak"
|
||||
]
|
||||
@@ -16133,7 +16133,7 @@
|
||||
"texture": [
|
||||
"silky",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-balanced",
|
||||
"moderately creamy",
|
||||
"polished",
|
||||
@@ -16253,7 +16253,7 @@
|
||||
"smooth and approachable",
|
||||
"concentrated and dense",
|
||||
"chalky-dry towards the end",
|
||||
"rounded but structured",
|
||||
"rounded-sm but structured",
|
||||
"supple with a citrusy cut",
|
||||
"polished oak texture"
|
||||
]
|
||||
@@ -16310,11 +16310,11 @@
|
||||
"full-bodied for low ABV",
|
||||
"effervescent prickle",
|
||||
"sprightly zing",
|
||||
"smooth and rounded",
|
||||
"smooth and rounded-sm",
|
||||
"viscous syrup",
|
||||
"well-integrated heat",
|
||||
"slightly waxy",
|
||||
"rounded mouthfeel"
|
||||
"rounded-sm mouthfeel"
|
||||
]
|
||||
},
|
||||
"Berry Bros & Rudd": {
|
||||
@@ -16374,7 +16374,7 @@
|
||||
"silky/velvety mid-palate",
|
||||
"medium-bodied and balanced",
|
||||
"slightly oily with grip",
|
||||
"crisp yet rounded",
|
||||
"crisp yet rounded-sm",
|
||||
"chalky/mineral edge",
|
||||
"polished oak feel",
|
||||
"creamy without being heavy",
|
||||
@@ -16678,7 +16678,7 @@
|
||||
"medium-bodied",
|
||||
"slightly oily",
|
||||
"polished",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"well-integrated alcohol",
|
||||
"soft",
|
||||
"crisp",
|
||||
@@ -17228,7 +17228,7 @@
|
||||
"dense",
|
||||
"oily-tear legs",
|
||||
"weighty",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"polished",
|
||||
"plush",
|
||||
"satiny",
|
||||
@@ -17396,7 +17396,7 @@
|
||||
"clean smoke / ember whisper"
|
||||
],
|
||||
"texture": [
|
||||
"smooth and rounded",
|
||||
"smooth and rounded-sm",
|
||||
"silky / velvety",
|
||||
"creamy (reminiscent of crème anglaise)",
|
||||
"medium-bodied and well-balanced",
|
||||
@@ -17444,7 +17444,7 @@
|
||||
"gentle peat (whisper)"
|
||||
],
|
||||
"finish": [
|
||||
"smooth and rounded",
|
||||
"smooth and rounded-sm",
|
||||
"lingering honey",
|
||||
"soft oak",
|
||||
"dried fruit sultanas",
|
||||
@@ -17461,7 +17461,7 @@
|
||||
"creamy",
|
||||
"well-balanced",
|
||||
"soft",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"slightly oily",
|
||||
"polished",
|
||||
"approachable",
|
||||
@@ -17532,7 +17532,7 @@
|
||||
],
|
||||
"texture": [
|
||||
"smooth and polished",
|
||||
"creamy and rounded",
|
||||
"creamy and rounded-sm",
|
||||
"medium-bodied and balanced",
|
||||
"silky mouthfeel",
|
||||
"well-integrated alcohol (no harshness)",
|
||||
@@ -17640,7 +17640,7 @@
|
||||
"butterscotch"
|
||||
],
|
||||
"finish": [
|
||||
"smooth and rounded",
|
||||
"smooth and rounded-sm",
|
||||
"medium length",
|
||||
"lingering honey and vanilla",
|
||||
"sweet oak and gentle spice",
|
||||
@@ -17769,7 +17769,7 @@
|
||||
"balanced sweetness and spice"
|
||||
],
|
||||
"finish": [
|
||||
"smooth and rounded",
|
||||
"smooth and rounded-sm",
|
||||
"lingering smokiness",
|
||||
"creamy vanilla",
|
||||
"oak-driven warmth",
|
||||
@@ -17790,7 +17790,7 @@
|
||||
"velvety",
|
||||
"silky",
|
||||
"medium-bodied",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"oily",
|
||||
"smooth",
|
||||
"soft",
|
||||
@@ -17866,7 +17866,7 @@
|
||||
"approachable",
|
||||
"smooth",
|
||||
"sprightly",
|
||||
"rounded mouthfeel",
|
||||
"rounded-sm mouthfeel",
|
||||
"gentle"
|
||||
]
|
||||
},
|
||||
@@ -17930,7 +17930,7 @@
|
||||
"texture": [
|
||||
"creamy",
|
||||
"oily",
|
||||
"rounded",
|
||||
"rounded-sm",
|
||||
"full-bodied",
|
||||
"silky",
|
||||
"weighted",
|
||||
@@ -17989,7 +17989,7 @@
|
||||
"creamy and velvety",
|
||||
"oily and coating",
|
||||
"waxy and beeswax-like",
|
||||
"rounded and mellow",
|
||||
"rounded-sm and mellow",
|
||||
"medium-bodied and balanced",
|
||||
"silky and smooth",
|
||||
"slightly drying oak",
|
||||
|
||||
@@ -5,7 +5,11 @@ const nextConfig = {
|
||||
output: 'standalone',
|
||||
// Enable source maps for Sentry stack traces in production
|
||||
productionBrowserSourceMaps: !!process.env.GLITCHTIP_DSN,
|
||||
// React Compiler for automatic memoization (React 19+)
|
||||
reactCompiler: true,
|
||||
experimental: {
|
||||
// Note: cacheComponents (PPR) disabled - requires Suspense boundaries for all auth contexts
|
||||
// Can be enabled later after refactoring to RSC-first architecture
|
||||
serverActions: {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/node": "^20",
|
||||
@@ -51,13 +52,13 @@
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "16.1.0",
|
||||
"eslint-plugin-security": "^2.1.1",
|
||||
"jsdom": "^27.3.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
|
||||
588
pnpm-lock.yaml
generated
588
pnpm-lock.yaml
generated
@@ -19,7 +19,7 @@ importers:
|
||||
version: 1.11.0
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.34.0
|
||||
version: 10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.104.1)
|
||||
version: 10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.104.1)
|
||||
'@supabase/ssr':
|
||||
specifier: ^0.5.2
|
||||
version: 0.5.2(@supabase/supabase-js@2.88.0)
|
||||
@@ -64,7 +64,7 @@ importers:
|
||||
version: 5.1.6
|
||||
next:
|
||||
specifier: 16.1.0
|
||||
version: 16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
openai:
|
||||
specifier: ^6.15.0
|
||||
version: 6.15.0(ws@8.18.3)(zod@3.25.76)
|
||||
@@ -99,6 +99,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.57.0
|
||||
version: 1.57.0
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
@@ -119,10 +122,10 @@ importers:
|
||||
version: 10.0.0
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2(vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0))
|
||||
autoprefixer:
|
||||
specifier: ^10.0.1
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
version: 5.1.2(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))
|
||||
babel-plugin-react-compiler:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
eslint:
|
||||
specifier: ^8
|
||||
version: 8.57.1
|
||||
@@ -139,14 +142,14 @@ importers:
|
||||
specifier: ^8
|
||||
version: 8.5.6
|
||||
tailwindcss:
|
||||
specifier: ^3.3.0
|
||||
version: 3.4.19
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.0.16
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(jiti@1.21.7)(jsdom@27.3.0)(terser@5.46.0)
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1305,6 +1308,94 @@ packages:
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tailwindcss/node@4.1.18':
|
||||
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.18':
|
||||
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.18':
|
||||
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.18':
|
||||
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.18':
|
||||
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
|
||||
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
|
||||
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
|
||||
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.1.18':
|
||||
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@tailwindcss/postcss@4.1.18':
|
||||
resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
|
||||
|
||||
'@tanstack/query-core@5.90.12':
|
||||
resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==}
|
||||
|
||||
@@ -1770,16 +1861,10 @@ packages:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
@@ -1833,13 +1918,6 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
autoprefixer@10.4.23:
|
||||
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1860,6 +1938,9 @@ packages:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
babel-plugin-react-compiler@1.0.0:
|
||||
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -1961,10 +2042,6 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
camelcase-css@2.0.1:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
caniuse-lite@1.0.30001760:
|
||||
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
||||
|
||||
@@ -2017,10 +2094,6 @@ packages:
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
@@ -2045,11 +2118,6 @@ packages:
|
||||
css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
cssstyle@5.3.5:
|
||||
resolution: {integrity: sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2180,12 +2248,6 @@ packages:
|
||||
dexie@4.2.1:
|
||||
resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==}
|
||||
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
doctrine@2.1.0:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2446,10 +2508,6 @@ packages:
|
||||
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
||||
@@ -2504,9 +2562,6 @@ packages:
|
||||
forwarded-parse@2.1.2:
|
||||
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
|
||||
|
||||
fraction.js@5.3.4:
|
||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||
|
||||
framer-motion@12.23.26:
|
||||
resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
|
||||
peerDependencies:
|
||||
@@ -2878,8 +2933,8 @@ packages:
|
||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
jiti@1.21.7:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
jiti@2.6.1:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
js-tokens@4.0.0:
|
||||
@@ -2948,12 +3003,75 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
lightningcss-darwin-arm64@1.30.2:
|
||||
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.30.2:
|
||||
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.30.2:
|
||||
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.30.2:
|
||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
loader-runner@4.3.1:
|
||||
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
||||
@@ -3062,9 +3180,6 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -3137,10 +3252,6 @@ packages:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3274,14 +3385,6 @@ packages:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
platform@1.3.6:
|
||||
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
|
||||
|
||||
@@ -3299,49 +3402,6 @@ packages:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.0.0
|
||||
|
||||
postcss-js@4.1.0:
|
||||
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
|
||||
engines: {node: ^12 || ^14 || >= 16}
|
||||
peerDependencies:
|
||||
postcss: ^8.4.21
|
||||
|
||||
postcss-load-config@6.0.1:
|
||||
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
postcss-nested@6.2.0:
|
||||
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.2.14
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -3449,9 +3509,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3745,11 +3802,6 @@ packages:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
sucrase@3.35.1:
|
||||
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3765,10 +3817,8 @@ packages:
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
tailwindcss@3.4.19:
|
||||
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
tailwindcss@4.1.18:
|
||||
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
||||
|
||||
tapable@2.3.0:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
@@ -3820,13 +3870,6 @@ packages:
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -3873,9 +3916,6 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||
|
||||
@@ -5158,7 +5198,7 @@ snapshots:
|
||||
|
||||
'@sentry/core@10.34.0': {}
|
||||
|
||||
'@sentry/nextjs@10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.104.1)':
|
||||
'@sentry/nextjs@10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.104.1)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.39.0
|
||||
@@ -5171,7 +5211,7 @@ snapshots:
|
||||
'@sentry/react': 10.34.0(react@19.2.3)
|
||||
'@sentry/vercel-edge': 10.34.0
|
||||
'@sentry/webpack-plugin': 4.6.2(webpack@5.104.1)
|
||||
next: 16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next: 16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
rollup: 4.53.5
|
||||
stacktrace-parser: 0.1.11
|
||||
transitivePeerDependencies:
|
||||
@@ -5322,6 +5362,75 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tailwindcss/node@4.1.18':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
enhanced-resolve: 5.18.4
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
magic-string: 0.30.21
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.1.18
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.1.18':
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.1.18
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.1.18
|
||||
'@tailwindcss/oxide-darwin-x64': 4.1.18
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.1.18
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
|
||||
|
||||
'@tailwindcss/postcss@4.1.18':
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
'@tailwindcss/node': 4.1.18
|
||||
'@tailwindcss/oxide': 4.1.18
|
||||
postcss: 8.5.6
|
||||
tailwindcss: 4.1.18
|
||||
|
||||
'@tanstack/query-core@5.90.12': {}
|
||||
|
||||
'@tanstack/react-query@5.90.12(react@19.2.3)':
|
||||
@@ -5636,7 +5745,7 @@ snapshots:
|
||||
|
||||
'@vercel/oidc@3.0.5': {}
|
||||
|
||||
'@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0))':
|
||||
'@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
|
||||
@@ -5644,7 +5753,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.18.0
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -5657,13 +5766,13 @@ snapshots:
|
||||
chai: 6.2.1
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0))':
|
||||
'@vitest/mocker@4.0.16(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.16
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
|
||||
'@vitest/pretty-format@4.0.16':
|
||||
dependencies:
|
||||
@@ -5844,15 +5953,11 @@ snapshots:
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-query@5.3.0:
|
||||
@@ -5934,15 +6039,6 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
autoprefixer@10.4.23(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
caniuse-lite: 1.0.30001760
|
||||
fraction.js: 5.3.4
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
@@ -5953,6 +6049,10 @@ snapshots:
|
||||
|
||||
b4a@1.7.3: {}
|
||||
|
||||
babel-plugin-react-compiler@1.0.0:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
@@ -6061,8 +6161,6 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001760: {}
|
||||
|
||||
canvas-confetti@1.9.4: {}
|
||||
@@ -6114,8 +6212,6 @@ snapshots:
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commondir@1.0.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
@@ -6137,8 +6233,6 @@ snapshots:
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssstyle@5.3.5:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 4.1.1
|
||||
@@ -6254,10 +6348,6 @@ snapshots:
|
||||
|
||||
dexie@4.2.1: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
doctrine@2.1.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
@@ -6687,14 +6777,6 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fast-glob@3.3.3:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
@@ -6743,8 +6825,6 @@ snapshots:
|
||||
|
||||
forwarded-parse@2.1.2: {}
|
||||
|
||||
fraction.js@5.3.4: {}
|
||||
|
||||
framer-motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
motion-dom: 12.23.23
|
||||
@@ -7121,7 +7201,7 @@ snapshots:
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jiti@1.21.7: {}
|
||||
jiti@2.6.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
@@ -7198,9 +7278,54 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
lightningcss-darwin-arm64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.30.2:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64: 1.30.2
|
||||
lightningcss-darwin-arm64: 1.30.2
|
||||
lightningcss-darwin-x64: 1.30.2
|
||||
lightningcss-freebsd-x64: 1.30.2
|
||||
lightningcss-linux-arm-gnueabihf: 1.30.2
|
||||
lightningcss-linux-arm64-gnu: 1.30.2
|
||||
lightningcss-linux-arm64-musl: 1.30.2
|
||||
lightningcss-linux-x64-gnu: 1.30.2
|
||||
lightningcss-linux-x64-musl: 1.30.2
|
||||
lightningcss-win32-arm64-msvc: 1.30.2
|
||||
lightningcss-win32-x64-msvc: 1.30.2
|
||||
|
||||
loader-runner@4.3.1: {}
|
||||
|
||||
@@ -7285,12 +7410,6 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mz@2.7.0:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
@@ -7303,7 +7422,7 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
next@16.1.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.1.0
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -7324,6 +7443,7 @@ snapshots:
|
||||
'@next/swc-win32-x64-msvc': 16.1.0
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.57.0
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -7345,8 +7465,6 @@ snapshots:
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
@@ -7486,10 +7604,6 @@ snapshots:
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
platform@1.3.6: {}
|
||||
|
||||
playwright-core@1.57.0: {}
|
||||
@@ -7502,37 +7616,6 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.11
|
||||
|
||||
postcss-js@4.1.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
camelcase-css: 2.0.1
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-nested@6.2.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
postcss-selector-parser: 6.1.2
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -7654,10 +7737,6 @@ snapshots:
|
||||
|
||||
react@19.2.3: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
@@ -8078,16 +8157,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.7
|
||||
tinyglobby: 0.2.15
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -8100,33 +8169,7 @@ snapshots:
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tailwindcss@3.4.19:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
arg: 5.0.2
|
||||
chokidar: 3.6.0
|
||||
didyoumean: 1.2.2
|
||||
dlv: 1.1.3
|
||||
fast-glob: 3.3.3
|
||||
glob-parent: 6.0.2
|
||||
is-glob: 4.0.3
|
||||
jiti: 1.21.7
|
||||
lilconfig: 3.1.3
|
||||
micromatch: 4.0.8
|
||||
normalize-path: 3.0.0
|
||||
object-hash: 3.0.0
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.5.6
|
||||
postcss-import: 15.1.0(postcss@8.5.6)
|
||||
postcss-js: 4.1.0(postcss@8.5.6)
|
||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
|
||||
postcss-nested: 6.2.0(postcss@8.5.6)
|
||||
postcss-selector-parser: 6.1.2
|
||||
resolve: 1.22.11
|
||||
sucrase: 3.35.1
|
||||
transitivePeerDependencies:
|
||||
- tsx
|
||||
- yaml
|
||||
tailwindcss@4.1.18: {}
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
@@ -8206,14 +8249,6 @@ snapshots:
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
|
||||
thenify@3.3.1:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
@@ -8251,8 +8286,6 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
dependencies:
|
||||
'@types/json5': 0.0.29
|
||||
@@ -8399,7 +8432,7 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0):
|
||||
vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0):
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -8410,13 +8443,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.27
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
terser: 5.46.0
|
||||
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(jiti@1.21.7)(jsdom@27.3.0)(terser@5.46.0):
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.46.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.16
|
||||
'@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0))
|
||||
'@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -8433,7 +8467,7 @@ snapshots:
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(terser@5.46.0)
|
||||
vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function BannerManager({ initialBanners }: BannerManagerProps) {
|
||||
value={formData.title}
|
||||
onChange={e => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="Banner title"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ export default function BannerManager({ initialBanners }: BannerManagerProps) {
|
||||
value={formData.image_url}
|
||||
onChange={e => setFormData({ ...formData, image_url: e.target.value })}
|
||||
placeholder="https://example.com/banner.jpg"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ export default function BannerManager({ initialBanners }: BannerManagerProps) {
|
||||
value={formData.link_target}
|
||||
onChange={e => setFormData({ ...formData, link_target: e.target.value })}
|
||||
placeholder="/sessions"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -197,7 +197,7 @@ export default function BannerManager({ initialBanners }: BannerManagerProps) {
|
||||
value={formData.cta_text}
|
||||
onChange={e => setFormData({ ...formData, cta_text: e.target.value })}
|
||||
placeholder="Open"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,7 +320,7 @@ export default function BannerManager({ initialBanners }: BannerManagerProps) {
|
||||
/* Display Mode */
|
||||
<div className="flex gap-4">
|
||||
{/* Thumbnail */}
|
||||
<div className="w-32 h-20 rounded-lg overflow-hidden bg-zinc-800 flex-shrink-0">
|
||||
<div className="w-32 h-20 rounded-lg overflow-hidden bg-zinc-800 shrink-0">
|
||||
<img
|
||||
src={banner.image_url}
|
||||
alt={banner.title}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Search bottles, distilleries, or users..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
<select
|
||||
value={filterUser || ''}
|
||||
onChange={e => setFilterUser(e.target.value || null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none focus:border-orange-600 appearance-none cursor-pointer"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden focus:border-orange-600 appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Users</option>
|
||||
{users.map(([id, name]) => (
|
||||
@@ -145,7 +145,7 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
<select
|
||||
value={filterCategory || ''}
|
||||
onChange={e => setFilterCategory(e.target.value || null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none focus:border-orange-600 appearance-none cursor-pointer"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden focus:border-orange-600 appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
@@ -161,7 +161,7 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
setSortBy(by);
|
||||
setSortOrder(order);
|
||||
}}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none focus:border-orange-600 appearance-none cursor-pointer"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden focus:border-orange-600 appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="created_at-desc">Newest First</option>
|
||||
<option value="created_at-asc">Oldest First</option>
|
||||
@@ -202,7 +202,7 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
className="bg-zinc-900 rounded-2xl border border-zinc-800 overflow-hidden hover:border-zinc-700 transition-colors"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="aspect-[4/3] relative bg-zinc-800">
|
||||
<div className="aspect-4/3 relative bg-zinc-800">
|
||||
{bottle.image_url ? (
|
||||
<img
|
||||
src={bottle.image_url}
|
||||
@@ -217,14 +217,14 @@ export default function AdminBottlesList({ bottles }: AdminBottlesListProps) {
|
||||
|
||||
{/* Category Badge */}
|
||||
{bottle.category && (
|
||||
<span className="absolute top-2 left-2 px-2 py-1 bg-black/60 backdrop-blur-sm text-[10px] font-bold text-white rounded-lg uppercase">
|
||||
<span className="absolute top-2 left-2 px-2 py-1 bg-black/60 backdrop-blur-xs text-[10px] font-bold text-white rounded-lg uppercase">
|
||||
{bottle.category}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Rating Badge */}
|
||||
{avgRating > 0 && (
|
||||
<span className="absolute top-2 right-2 px-2 py-1 bg-orange-600/90 backdrop-blur-sm text-xs font-bold text-white rounded-lg flex items-center gap-1">
|
||||
<span className="absolute top-2 right-2 px-2 py-1 bg-orange-600/90 backdrop-blur-xs text-xs font-bold text-white rounded-lg flex items-center gap-1">
|
||||
<Star size={12} fill="currentColor" />
|
||||
{avgRating.toFixed(1)}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||
@@ -49,7 +48,7 @@ export default async function OcrLogsPage() {
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<Camera size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
@@ -60,7 +59,7 @@ export default async function OcrLogsPage() {
|
||||
<div className="text-xs text-zinc-500 mt-1">All time</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<Calendar size={20} className="text-green-600 dark:text-green-400" />
|
||||
@@ -71,7 +70,7 @@ export default async function OcrLogsPage() {
|
||||
<div className="text-xs text-zinc-500 mt-1">Scans today</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
|
||||
<Percent size={20} className="text-amber-600 dark:text-amber-400" />
|
||||
@@ -82,7 +81,7 @@ export default async function OcrLogsPage() {
|
||||
<div className="text-xs text-zinc-500 mt-1">Recognition quality</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<TrendingUp size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
@@ -100,7 +99,7 @@ export default async function OcrLogsPage() {
|
||||
|
||||
{/* Top Distilleries */}
|
||||
{stats.topDistilleries.length > 0 && (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<h2 className="text-xl font-black text-zinc-900 dark:text-white mb-4">Most Scanned Distilleries</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{stats.topDistilleries.map((d, i) => (
|
||||
@@ -119,7 +118,7 @@ export default async function OcrLogsPage() {
|
||||
)}
|
||||
|
||||
{/* OCR Logs Grid */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<h2 className="text-xl font-black text-zinc-900 dark:text-white mb-4">Recent OCR Scans</h2>
|
||||
|
||||
{logs.length === 0 ? (
|
||||
@@ -136,7 +135,7 @@ export default async function OcrLogsPage() {
|
||||
className="bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-4 border border-zinc-200 dark:border-zinc-700 hover:border-orange-500/50 transition-colors"
|
||||
>
|
||||
{/* Image Preview */}
|
||||
<div className="relative aspect-[4/3] rounded-lg overflow-hidden bg-zinc-200 dark:bg-zinc-700 mb-3">
|
||||
<div className="relative aspect-4/3 rounded-lg overflow-hidden bg-zinc-200 dark:bg-zinc-700 mb-3">
|
||||
{log.image_thumbnail ? (
|
||||
<img
|
||||
src={log.image_thumbnail}
|
||||
@@ -175,7 +174,7 @@ export default async function OcrLogsPage() {
|
||||
{log.distillery}
|
||||
</span>
|
||||
{log.distillery_source && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-700 rounded text-zinc-500">
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-700 rounded-sm text-zinc-500">
|
||||
{log.distillery_source}
|
||||
</span>
|
||||
)}
|
||||
@@ -190,22 +189,22 @@ export default async function OcrLogsPage() {
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{log.abv && (
|
||||
<span className="px-2 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded text-[10px] font-bold">
|
||||
<span className="px-2 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded-sm text-[10px] font-bold">
|
||||
{log.abv}%
|
||||
</span>
|
||||
)}
|
||||
{log.age && (
|
||||
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded text-[10px] font-bold">
|
||||
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded-sm text-[10px] font-bold">
|
||||
{log.age}y
|
||||
</span>
|
||||
)}
|
||||
{log.vintage && (
|
||||
<span className="px-2 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded text-[10px] font-bold">
|
||||
<span className="px-2 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-sm text-[10px] font-bold">
|
||||
{log.vintage}
|
||||
</span>
|
||||
)}
|
||||
{log.volume && (
|
||||
<span className="px-2 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400 rounded text-[10px] font-bold">
|
||||
<span className="px-2 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400 rounded-sm text-[10px] font-bold">
|
||||
{log.volume}
|
||||
</span>
|
||||
)}
|
||||
@@ -218,7 +217,7 @@ export default async function OcrLogsPage() {
|
||||
<summary className="text-[10px] font-bold text-zinc-400 cursor-pointer hover:text-orange-500 uppercase">
|
||||
Raw Text
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-900 rounded text-[9px] text-zinc-500 overflow-x-auto max-h-20 whitespace-pre-wrap">
|
||||
<pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-900 rounded-sm text-[9px] text-zinc-500 overflow-x-auto max-h-20 whitespace-pre-wrap">
|
||||
{log.raw_text}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { checkIsAdmin, getGlobalApiStats } from '@/services/track-api-usage';
|
||||
@@ -158,7 +157,7 @@ export default async function AdminPage() {
|
||||
|
||||
{/* Global Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<BarChart3 size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
@@ -169,7 +168,7 @@ export default async function AdminPage() {
|
||||
<div className="text-xs text-zinc-500 mt-1">All time</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<Calendar size={20} className="text-green-600 dark:text-green-400" />
|
||||
@@ -188,7 +187,7 @@ export default async function AdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
|
||||
<TrendingUp size={20} className="text-amber-600 dark:text-amber-400" />
|
||||
@@ -199,7 +198,7 @@ export default async function AdminPage() {
|
||||
<div className="text-xs text-zinc-500 mt-1">Whiskybase searches</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<Users size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
@@ -212,7 +211,7 @@ export default async function AdminPage() {
|
||||
</div>
|
||||
|
||||
{/* Top Users */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<h2 className="text-xl font-black text-zinc-900 dark:text-white mb-4">Top Users by API Usage</h2>
|
||||
<div className="space-y-3">
|
||||
{topUsersWithStats.map((user, index) => (
|
||||
@@ -235,7 +234,7 @@ export default async function AdminPage() {
|
||||
</div>
|
||||
|
||||
{/* Recent API Calls */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<h2 className="text-xl font-black text-zinc-900 dark:text-white mb-4">Recent API Calls</h2>
|
||||
<div className="text-sm text-zinc-500 mb-4">
|
||||
Total calls logged: {recentUsage?.length || 0}
|
||||
@@ -296,7 +295,7 @@ export default async function AdminPage() {
|
||||
{call.response_text && (
|
||||
<details className="text-[10px]">
|
||||
<summary className="cursor-pointer text-orange-600 hover:text-orange-700 font-bold uppercase transition-colors">Response</summary>
|
||||
<pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-950 rounded border border-zinc-200 dark:border-zinc-800 overflow-x-auto max-w-sm whitespace-pre-wrap font-mono text-[9px] text-zinc-400">
|
||||
<pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-950 rounded-sm border border-zinc-200 dark:border-zinc-800 overflow-x-auto max-w-sm whitespace-pre-wrap font-mono text-[9px] text-zinc-400">
|
||||
{call.response_text}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -310,7 +309,7 @@ export default async function AdminPage() {
|
||||
<div className="group relative">
|
||||
<span className="text-red-600 dark:text-red-400 font-black text-xs cursor-help">ERR</span>
|
||||
{call.error_message && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-red-600 text-white text-[9px] rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-red-600 text-white text-[9px] rounded-sm shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
|
||||
{call.error_message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function AdminSessionsList({ sessions }: AdminSessionsListProps)
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Search sessions or hosts..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function AdminSessionsList({ sessions }: AdminSessionsListProps)
|
||||
<select
|
||||
value={filterHost || ''}
|
||||
onChange={e => setFilterHost(e.target.value || null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="">All Hosts</option>
|
||||
{hosts.map(([id, name]) => (
|
||||
@@ -112,7 +112,7 @@ export default function AdminSessionsList({ sessions }: AdminSessionsListProps)
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={e => setFilterStatus(e.target.value as any)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Search bottles, hosts, or slugs..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
<select
|
||||
value={filterHost || ''}
|
||||
onChange={e => setFilterHost(e.target.value || null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="">All Hosts</option>
|
||||
{hosts.map(([id, name]) => (
|
||||
@@ -94,7 +94,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={e => setFilterStatus(e.target.value as any)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
@@ -124,7 +124,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
}`}
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="aspect-[16/9] relative bg-zinc-800">
|
||||
<div className="aspect-video relative bg-zinc-800">
|
||||
{split.bottle?.image_url ? (
|
||||
<img
|
||||
src={split.bottle.image_url}
|
||||
@@ -147,7 +147,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
</span>
|
||||
|
||||
{/* Participants Badge */}
|
||||
<span className="absolute top-2 right-2 px-2 py-1 bg-black/60 backdrop-blur-sm text-xs font-bold text-white rounded-lg flex items-center gap-1">
|
||||
<span className="absolute top-2 right-2 px-2 py-1 bg-black/60 backdrop-blur-xs text-xs font-bold text-white rounded-lg flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
{split.participantCount}
|
||||
</span>
|
||||
@@ -170,7 +170,7 @@ export default function AdminSplitsList({ splits }: AdminSplitsListProps) {
|
||||
</div>
|
||||
<div className="h-2 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 to-orange-600 transition-all"
|
||||
className="h-full bg-linear-to-r from-orange-500 to-orange-600 transition-all"
|
||||
style={{ width: `${fillPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function AdminTagsPage() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Tags suchen..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all dark:text-zinc-200"
|
||||
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-hidden transition-all dark:text-zinc-200"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -96,7 +96,7 @@ export default function AdminTagsPage() {
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value as any)}
|
||||
className="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-3 py-2 text-sm font-bold uppercase tracking-tight outline-none focus:ring-2 focus:ring-amber-500 dark:text-zinc-200"
|
||||
className="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-3 py-2 text-sm font-bold uppercase tracking-tight outline-hidden focus:ring-2 focus:ring-amber-500 dark:text-zinc-200"
|
||||
>
|
||||
<option value="all">Alle Kategorien</option>
|
||||
<option value="nose">Nose</option>
|
||||
@@ -154,7 +154,7 @@ export default function AdminTagsPage() {
|
||||
key={score}
|
||||
onClick={() => updatePopularity(tag.id, score)}
|
||||
className={`w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-black transition-all ${tag.popularity_score === score
|
||||
? 'bg-amber-600 text-white shadow-sm'
|
||||
? 'bg-amber-600 text-white shadow-xs'
|
||||
: 'bg-zinc-100 text-zinc-400 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function AdminTastingsList({ tastings }: AdminTastingsListProps)
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Search bottles, users, or notes..."
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600"
|
||||
className="w-full pl-12 pr-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function AdminTastingsList({ tastings }: AdminTastingsListProps)
|
||||
<select
|
||||
value={filterUser || ''}
|
||||
onChange={e => setFilterUser(e.target.value || null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="">All Users</option>
|
||||
{users.map(([id, name]) => (
|
||||
@@ -105,7 +105,7 @@ export default function AdminTastingsList({ tastings }: AdminTastingsListProps)
|
||||
<select
|
||||
value={filterRating ?? ''}
|
||||
onChange={e => setFilterRating(e.target.value ? parseInt(e.target.value) : null)}
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-none"
|
||||
className="px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-300 focus:outline-hidden"
|
||||
>
|
||||
<option value="">All Ratings</option>
|
||||
<option value="5">5 Stars</option>
|
||||
@@ -134,7 +134,7 @@ export default function AdminTastingsList({ tastings }: AdminTastingsListProps)
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
{/* Bottle Image */}
|
||||
<div className="w-16 h-16 rounded-xl overflow-hidden bg-zinc-800 flex-shrink-0">
|
||||
<div className="w-16 h-16 rounded-xl overflow-hidden bg-zinc-800 shrink-0">
|
||||
{tasting.bottle?.image_url ? (
|
||||
<img
|
||||
src={tasting.bottle.image_url}
|
||||
@@ -159,7 +159,7 @@ export default function AdminTastingsList({ tastings }: AdminTastingsListProps)
|
||||
{tasting.bottle?.distillery || 'Unknown Distillery'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="shrink-0">
|
||||
{tasting.rating > 0 ? renderStars(tasting.rating) : (
|
||||
<span className="text-xs text-zinc-600">No rating</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||
@@ -53,7 +52,7 @@ export default async function AdminUsersPage() {
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<Users size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
@@ -63,7 +62,7 @@ export default async function AdminUsersPage() {
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalUsers}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<Coins size={20} className="text-green-600 dark:text-green-400" />
|
||||
@@ -73,7 +72,7 @@ export default async function AdminUsersPage() {
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalCreditsInCirculation.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
|
||||
<TrendingUp size={20} className="text-amber-600 dark:text-amber-400" />
|
||||
@@ -83,7 +82,7 @@ export default async function AdminUsersPage() {
|
||||
<div className="text-3xl font-black text-zinc-900 dark:text-white">{totalCreditsPurchased.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xs">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<TrendingDown size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import sharp from 'sharp';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function BuddiesPage() {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('buddy.placeholder')}
|
||||
className="flex-1 bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="flex-1 bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -138,7 +138,7 @@ export default function BuddiesPage() {
|
||||
placeholder="Search buddies..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600/50"
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,58 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-zinc-50: #fafafa;
|
||||
--color-zinc-100: #f4f4f5;
|
||||
--color-zinc-200: #e4e4e7;
|
||||
--color-zinc-300: #d4d4d8;
|
||||
--color-zinc-400: #a8a8b3;
|
||||
--color-zinc-500: #8a8a95;
|
||||
--color-zinc-600: #6b6b75;
|
||||
--color-zinc-700: #4a4a52;
|
||||
--color-zinc-800: #2a2a2e;
|
||||
--color-zinc-900: #1a1a1e;
|
||||
--color-zinc-950: #0d0d0f;
|
||||
|
||||
--background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
|
||||
--background-image-gradient-conic: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
var(--tw-gradient-stops)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@utility glass {
|
||||
@apply backdrop-blur-md bg-zinc-900/50 border border-zinc-800/50;
|
||||
}
|
||||
|
||||
@utility glass-dark {
|
||||
@apply backdrop-blur-md bg-zinc-950/80 border border-zinc-900/50;
|
||||
}
|
||||
|
||||
@utility scrollbar-hide {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@@ -20,16 +72,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
body {
|
||||
@apply bg-[#1c1c1e] text-[#fafafa] antialiased selection:bg-orange-500/30;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
}
|
||||
|
||||
/* Global Input Text Fix */
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
@apply bg-zinc-950 text-white border-zinc-800 focus:ring-1 focus:ring-orange-600 outline-none transition-all;
|
||||
@apply bg-zinc-950 text-white border-zinc-800 focus:ring-1 focus:ring-orange-600 outline-hidden transition-all;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
@@ -45,22 +98,4 @@ h4,
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.glass {
|
||||
@apply backdrop-blur-md bg-zinc-900/50 border border-zinc-800/50;
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
@apply backdrop-blur-md bg-zinc-950/80 border border-zinc-900/50;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { MetadataRoute } from 'next'
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'Whisky Vault',
|
||||
short_name: 'WhiskyVault',
|
||||
name: 'Dramlog.eu',
|
||||
short_name: 'Dramlog',
|
||||
description: 'Dein persönlicher Whisky-Begleiter zum Scannen und Verkosten.',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
|
||||
@@ -5,13 +5,12 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import BottleGrid from "@/components/BottleGrid";
|
||||
import AuthForm from "@/components/AuthForm";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import OfflineIndicator from "@/components/OfflineIndicator";
|
||||
import { useI18n } from "@/i18n/I18nContext";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useSession } from "@/context/SessionContext";
|
||||
import TastingHub from "@/components/TastingHub";
|
||||
import { Sparkles, Loader2, Search, SlidersHorizontal, Settings, CircleUser } from "lucide-react";
|
||||
import { Sparkles, Loader2, Search, SlidersHorizontal } from "lucide-react";
|
||||
import { BottomNavigation } from '@/components/BottomNavigation';
|
||||
import ScanAndTasteFlow from '@/components/ScanAndTasteFlow';
|
||||
import UserStatusBadge from '@/components/UserStatusBadge';
|
||||
@@ -19,7 +18,6 @@ import { getActiveSplits } from '@/services/split-actions';
|
||||
import SplitCard from '@/components/SplitCard';
|
||||
import HeroBanner from '@/components/HeroBanner';
|
||||
import QuickActionsGrid from '@/components/QuickActionsGrid';
|
||||
import DramOfTheDay from '@/components/DramOfTheDay';
|
||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||
|
||||
export default function Home() {
|
||||
@@ -152,9 +150,6 @@ export default function Home() {
|
||||
<p className="text-zinc-500 max-w-sm mx-auto font-bold tracking-wide">
|
||||
{t('home.tagline')}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<AuthForm />
|
||||
|
||||
@@ -185,18 +180,28 @@ export default function Home() {
|
||||
|
||||
// Authenticated Home View - New Layout
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-[var(--background)] relative">
|
||||
<div className="flex flex-col min-h-screen bg-(--background) relative">
|
||||
{/* Scrollable Content Area */}
|
||||
<div className="flex-1 overflow-y-auto pb-24">
|
||||
{/* 1. Header */}
|
||||
<header className="px-4 pt-4 pb-2">
|
||||
<header className="px-4 pt-4 pb-2 space-y-2">
|
||||
{/* Row 1: Logo + Logout */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl font-bold text-zinc-50 tracking-tighter">
|
||||
DRAM<span className="text-orange-600">LOG</span>
|
||||
</h1>
|
||||
{activeSession && (
|
||||
<div className="flex items-center gap-2 mt-0.5 animate-in fade-in slide-in-from-left-2 duration-700">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-[10px] font-bold uppercase tracking-widest text-zinc-500 hover:text-white transition-colors"
|
||||
>
|
||||
{t('home.logout')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Session info + Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
{activeSession ? (
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-left-2 duration-700">
|
||||
<div className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-600 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-600"></span>
|
||||
@@ -206,19 +211,12 @@ export default function Home() {
|
||||
Live: {activeSession.name}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<UserStatusBadge />
|
||||
<OfflineIndicator />
|
||||
<LanguageSwitcher />
|
||||
<DramOfTheDay bottles={bottles} />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-[9px] font-bold uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
|
||||
>
|
||||
{t('home.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -244,7 +242,7 @@ export default function Home() {
|
||||
placeholder={t('home.searchPlaceholder') || 'Search collection...'}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600/50 focus:ring-1 focus:ring-orange-600/20"
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600/50 focus:ring-1 focus:ring-orange-600/20"
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2.5 bg-zinc-900 border border-zinc-800 rounded-xl text-zinc-500 hover:text-white hover:border-zinc-700 transition-colors">
|
||||
|
||||
@@ -308,7 +308,7 @@ export default function SessionDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--background)] p-4 md:p-12 lg:p-24 pb-32">
|
||||
<main className="min-h-screen bg-(--background) p-4 md:p-12 lg:p-24 pb-32">
|
||||
<div className="max-w-6xl mx-auto space-y-12">
|
||||
{/* Back Link & Info */}
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -337,7 +337,7 @@ export default function SessionDetailPage() {
|
||||
{/* Immersive Header */}
|
||||
<header className="relative bg-zinc-900 border border-white/5 rounded-[48px] p-8 md:p-12 shadow-[0_20px_80px_rgba(0,0,0,0.5)] overflow-hidden group">
|
||||
{/* Background Visuals */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-zinc-900 via-zinc-900 to-black z-0" />
|
||||
<div className="absolute inset-0 bg-linear-to-br from-zinc-900 via-zinc-900 to-black z-0" />
|
||||
{tastings.length > 0 && tastings[0].bottles.image_url && (
|
||||
<div className="absolute top-0 right-0 w-2/3 h-full opacity-30 z-0">
|
||||
<div
|
||||
@@ -570,7 +570,7 @@ export default function SessionDetailPage() {
|
||||
if (e.target.value) handleAddParticipant(e.target.value);
|
||||
e.target.value = "";
|
||||
}}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3 text-[10px] font-black uppercase tracking-wider text-zinc-400 outline-none focus:border-orange-500/50 transition-colors appearance-none"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3 text-[10px] font-black uppercase tracking-wider text-zinc-400 outline-hidden focus:border-orange-500/50 transition-colors appearance-none"
|
||||
style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'none\' viewBox=\'0 0 24 24\' stroke=\'%23a1a1aa\'%3E%3Cpath stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M19 9l-7 7-7-7\'/%3E%3C/svg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem center', backgroundSize: '1rem' }}
|
||||
>
|
||||
<option value="">Auswahl...</option>
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function SessionsPage() {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('session.sessionName')}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-none focus:border-orange-600 mb-3"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-3 text-white placeholder-zinc-600 focus:outline-hidden focus:border-orange-600 mb-3"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
@@ -186,7 +186,7 @@ export default function SessionsPage() {
|
||||
{/* Active Session Banner */}
|
||||
{activeSession && (
|
||||
<Link href={`/sessions/${activeSession.id}`}>
|
||||
<div className="mb-6 p-4 bg-gradient-to-r from-orange-600/20 to-orange-500/10 rounded-2xl border border-orange-600/30 flex items-center gap-4 group hover:border-orange-500/50 transition-colors">
|
||||
<div className="mb-6 p-4 bg-linear-to-r from-orange-600/20 to-orange-500/10 rounded-2xl border border-orange-600/30 flex items-center gap-4 group hover:border-orange-500/50 transition-colors">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 rounded-xl bg-orange-600/20 flex items-center justify-center">
|
||||
<Sparkles size={24} className="text-orange-500" />
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function SplitPublicPage() {
|
||||
alt={split.bottle.name}
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900 via-transparent to-transparent" />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-zinc-900 via-transparent to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function SplitManagePage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-bold text-white truncate">{split.bottleName}</p>
|
||||
{!split.isActive && (
|
||||
<span className="px-2 py-0.5 bg-zinc-800 rounded text-[10px] font-bold text-zinc-500">
|
||||
<span className="px-2 py-0.5 bg-zinc-800 rounded-sm text-[10px] font-bold text-zinc-500">
|
||||
Geschlossen
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import AnalyticsDashboard from '@/components/AnalyticsDashboard';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { ChartSkeleton, StatsCardSkeleton } from '@/components/Skeletons';
|
||||
|
||||
export default function StatsPage() {
|
||||
const router = useRouter();
|
||||
@@ -59,8 +60,20 @@ export default function StatsPage() {
|
||||
|
||||
{/* Dashboard */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<div className="space-y-6">
|
||||
{/* KPI Cards Skeleton */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatsCardSkeleton />
|
||||
<StatsCardSkeleton />
|
||||
<StatsCardSkeleton />
|
||||
<StatsCardSkeleton />
|
||||
</div>
|
||||
{/* Charts Skeleton */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<ChartSkeleton height={300} />
|
||||
<ChartSkeleton height={300} />
|
||||
</div>
|
||||
<ChartSkeleton height={400} />
|
||||
</div>
|
||||
) : (
|
||||
<AnalyticsDashboard bottles={bottles} />
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ActiveSessionBanner() {
|
||||
initial={{ y: 50, opacity: 0, x: '-50%' }}
|
||||
animate={{ y: 0, opacity: 1, x: '-50%' }}
|
||||
exit={{ y: 50, opacity: 0, x: '-50%' }}
|
||||
className="fixed bottom-32 left-1/2 z-[50] w-[calc(100%-2rem)] max-w-sm"
|
||||
className="fixed bottom-32 left-1/2 z-50 w-[calc(100%-2rem)] max-w-sm"
|
||||
>
|
||||
<div className="bg-zinc-900/90 backdrop-blur-2xl border border-orange-500/20 rounded-[32px] p-2 flex items-center justify-between shadow-2xl ring-1 ring-white/5 overflow-hidden">
|
||||
{/* Session Info Link */}
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function AuthForm() {
|
||||
placeholder="dein_username"
|
||||
required
|
||||
maxLength={20}
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-hidden transition-all text-white placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-zinc-600 ml-1">Nur Kleinbuchstaben, Zahlen und _</p>
|
||||
@@ -149,7 +149,7 @@ export default function AuthForm() {
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
placeholder="Max Mustermann"
|
||||
maxLength={50}
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-hidden transition-all text-white placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ export default function AuthForm() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@beispiel.de"
|
||||
required
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-hidden transition-all text-white placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,7 +181,7 @@ export default function AuthForm() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-hidden transition-all text-white placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function AvatarStack({ names, limit = 3, size = 'sm' }: AvatarSta
|
||||
{visibleNames.map((name, i) => (
|
||||
<div
|
||||
key={`${name}-${i}`}
|
||||
className={`${sizeClasses} rounded-full bg-orange-900/30 border-2 border-zinc-950 flex items-center justify-center text-orange-400 font-bold ring-1 ring-orange-500/10 shadow-sm relative group`}
|
||||
className={`${sizeClasses} rounded-full bg-orange-900/30 border-2 border-zinc-950 flex items-center justify-center text-orange-400 font-bold ring-1 ring-orange-500/10 shadow-xs relative group`}
|
||||
title={name}
|
||||
>
|
||||
{getInitials(name)}
|
||||
@@ -42,7 +42,7 @@ export default function AvatarStack({ names, limit = 3, size = 'sm' }: AvatarSta
|
||||
))}
|
||||
{extraCount > 0 && (
|
||||
<div
|
||||
className={`${sizeClasses} rounded-full bg-zinc-100 dark:bg-zinc-800 border-2 border-white dark:border-zinc-900 flex items-center justify-center text-zinc-500 dark:text-zinc-400 font-black ring-1 ring-zinc-500/10 shadow-sm relative group`}
|
||||
className={`${sizeClasses} rounded-full bg-zinc-100 dark:bg-zinc-800 border-2 border-white dark:border-zinc-900 flex items-center justify-center text-zinc-500 dark:text-zinc-400 font-black ring-1 ring-zinc-500/10 shadow-xs relative group`}
|
||||
title={`${extraCount} weitere Personen`}
|
||||
>
|
||||
+{extraCount}
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto pb-24">
|
||||
{/* Header / Hero Section */}
|
||||
<div className="relative w-full overflow-hidden bg-[var(--surface)] shadow-2xl">
|
||||
<div className="relative w-full overflow-hidden bg-(--surface) shadow-2xl">
|
||||
{/* Back Button Overlay */}
|
||||
<div className="absolute top-6 left-6 z-20">
|
||||
<Link
|
||||
@@ -110,9 +110,9 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
|
||||
</div>
|
||||
|
||||
{/* Hero Image - Slightly More Compact Aspect for better title flow */}
|
||||
<div className="relative aspect-[4/3] md:aspect-[16/8] w-full flex items-center justify-center p-6 md:p-10 overflow-hidden">
|
||||
<div className="relative aspect-4/3 md:aspect-16/8 w-full flex items-center justify-center p-6 md:p-10 overflow-hidden">
|
||||
{/* Background Glow */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-orange-600/10 via-transparent to-transparent opacity-30" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,var(--tw-gradient-stops))] from-orange-600/10 via-transparent to-transparent opacity-30" />
|
||||
<img
|
||||
src={getStorageUrl(bottle.image_url)}
|
||||
alt={bottle.name}
|
||||
@@ -121,7 +121,7 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
|
||||
</div>
|
||||
|
||||
{/* Info Overlay - Mobile Gradient */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-48 bg-gradient-to-t from-[var(--background)] to-transparent pointer-events-none" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-48 bg-linear-to-t from-(--background) to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
@@ -134,7 +134,7 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
|
||||
<p className="text-[9px] font-black uppercase tracking-widest text-orange-500">Offline Mode</p>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-xs md:text-sm font-black text-orange-500 uppercase tracking-[0.25em] drop-shadow-sm">
|
||||
<h2 className="text-xs md:text-sm font-black text-orange-500 uppercase tracking-[0.25em] drop-shadow-xs">
|
||||
{bottle.distillery || 'Unknown Distillery'}
|
||||
</h2>
|
||||
<h1 className="text-4xl md:text-6xl font-extrabold text-white tracking-tight leading-[1.05] drop-shadow-md">
|
||||
|
||||
@@ -37,10 +37,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
|
||||
return (
|
||||
<Link
|
||||
href={`/bottles/${bottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`}
|
||||
className="block h-full group relative overflow-hidden rounded-2xl bg-zinc-900 border border-white/[0.05] transition-all duration-500 hover:border-orange-500/30 hover:shadow-2xl hover:shadow-orange-950/20 active:scale-[0.98]"
|
||||
className="block h-full group relative overflow-hidden rounded-2xl bg-zinc-900 border border-white/5 transition-all duration-500 hover:border-orange-500/30 hover:shadow-2xl hover:shadow-orange-950/20 active:scale-[0.98]"
|
||||
>
|
||||
{/* === SPOTIFY-STYLE IMAGE SECTION === */}
|
||||
<div className="relative aspect-[3/4] overflow-hidden">
|
||||
<div className="relative aspect-3/4 overflow-hidden">
|
||||
|
||||
{/* Layer 1: Blurred Backdrop */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -103,10 +103,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-sm text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-xs text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
{shortenCategory(bottle.category)}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-sm text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-xs text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
{bottle.abv}% VOL
|
||||
</span>
|
||||
</div>
|
||||
@@ -216,7 +216,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
placeholder={t('grid.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-8 pr-8 py-4 bg-transparent border-b border-zinc-800 focus:border-orange-500 outline-none transition-all text-zinc-50 placeholder:text-zinc-500"
|
||||
className="w-full pl-8 pr-8 py-4 bg-transparent border-b border-zinc-800 focus:border-orange-500 outline-hidden transition-all text-zinc-50 placeholder:text-zinc-500"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
@@ -232,7 +232,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="bg-transparent border-none text-zinc-500 text-xs font-bold uppercase tracking-widest outline-none cursor-pointer hover:text-white transition-colors appearance-none"
|
||||
className="bg-transparent border-none text-zinc-500 text-xs font-bold uppercase tracking-widest outline-hidden cursor-pointer hover:text-white transition-colors appearance-none"
|
||||
>
|
||||
<option value="created_at" className="bg-zinc-950">{t('grid.sortBy.createdAt')}</option>
|
||||
<option value="last_tasted" className="bg-zinc-950">{t('grid.sortBy.lastTasted')}</option>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function BottleSkeletonCard({
|
||||
}`}
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="aspect-[3/4] bg-zinc-950 relative overflow-hidden">
|
||||
<div className="aspect-3/4 bg-zinc-950 relative overflow-hidden">
|
||||
{imageUrl ? (
|
||||
<img
|
||||
src={imageUrl}
|
||||
@@ -81,12 +81,12 @@ export default function BottleSkeletonCard({
|
||||
{/* Info */}
|
||||
<div className="p-3">
|
||||
{/* Skeleton Name */}
|
||||
<div className="h-4 bg-zinc-800 rounded animate-pulse mb-2 w-3/4" />
|
||||
<div className="h-4 bg-zinc-800 rounded-sm animate-pulse mb-2 w-3/4" />
|
||||
|
||||
{/* Skeleton Details */}
|
||||
<div className="flex gap-2">
|
||||
<div className="h-3 bg-zinc-800/50 rounded animate-pulse w-12" />
|
||||
<div className="h-3 bg-zinc-800/50 rounded animate-pulse w-8" />
|
||||
<div className="h-3 bg-zinc-800/50 rounded-sm animate-pulse w-12" />
|
||||
<div className="h-3 bg-zinc-800/50 rounded-sm animate-pulse w-8" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function BuddyHandshake({ isOpen, onClose, onSuccess }: BuddyHand
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
@@ -240,7 +240,7 @@ export default function BuddyHandshake({ isOpen, onClose, onSuccess }: BuddyHand
|
||||
}
|
||||
}}
|
||||
placeholder="XXXXXX"
|
||||
className="w-full text-center text-3xl font-black tracking-[0.4em] bg-zinc-950 border-2 border-zinc-800 rounded-2xl py-4 text-white placeholder:text-zinc-700 focus:outline-none focus:border-orange-500 transition-colors font-mono"
|
||||
className="w-full text-center text-3xl font-black tracking-[0.4em] bg-zinc-950 border-2 border-zinc-800 rounded-2xl py-4 text-white placeholder:text-zinc-700 focus:outline-hidden focus:border-orange-500 transition-colors font-mono"
|
||||
maxLength={6}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function BuddyList() {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('buddy.placeholder')}
|
||||
className="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -183,12 +183,12 @@ export default function BuddyList() {
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
|
||||
<div className="flex -space-x-1.5 overflow-hidden">
|
||||
{buddies.slice(0, 5).map((b, i) => (
|
||||
<div key={b.id} className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[10px] font-bold text-orange-600 shadow-sm">
|
||||
<div key={b.id} className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[10px] font-bold text-orange-600 shadow-xs">
|
||||
{b.name[0].toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
{buddies.length > 5 && (
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[8px] font-bold text-zinc-500 shadow-sm">
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[8px] font-bold text-zinc-500 shadow-xs">
|
||||
+{buddies.length - 5}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function BulkScanSheet({
|
||||
className="fixed inset-0 bg-black z-50 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-800">
|
||||
<div className="flex items-center justify-between p-4 bg-zinc-900/80 backdrop-blur-xs border-b border-zinc-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-orange-600/20 flex items-center justify-center">
|
||||
<Zap size={20} className="text-orange-500" />
|
||||
@@ -240,7 +240,7 @@ export default function BulkScanSheet({
|
||||
<X size={12} className="text-white" />
|
||||
</button>
|
||||
)}
|
||||
<span className="absolute bottom-0.5 left-0.5 text-[9px] font-bold text-white bg-black/60 px-1 rounded">
|
||||
<span className="absolute bottom-0.5 left-0.5 text-[9px] font-bold text-white bg-black/60 px-1 rounded-sm">
|
||||
#{i + 1}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
@@ -473,7 +473,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
||||
)}
|
||||
|
||||
{isQueued && (
|
||||
<div className="flex flex-col gap-3 p-5 bg-gradient-to-br from-purple-500/10 to-amber-500/10 dark:from-purple-900/20 dark:to-amber-900/20 border border-purple-500/20 rounded-3xl w-full animate-in zoom-in-95 duration-500">
|
||||
<div className="flex flex-col gap-3 p-5 bg-linear-to-br from-purple-500/10 to-amber-500/10 dark:from-purple-900/20 dark:to-amber-900/20 border border-purple-500/20 rounded-3xl w-full animate-in zoom-in-95 duration-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-2xl bg-purple-500 flex items-center justify-center text-white shadow-lg shadow-purple-500/30"><Sparkles size={20} /></div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function CookieBanner() {
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||
className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6"
|
||||
className="fixed bottom-0 left-0 right-0 z-100 p-4 md:p-6"
|
||||
>
|
||||
<div className="max-w-4xl mx-auto bg-zinc-900 border border-zinc-800 rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) {
|
||||
</button>
|
||||
|
||||
{suggestion && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-zinc-950/80 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="fixed inset-0 z-100 flex items-center justify-center p-6 bg-zinc-950/80 backdrop-blur-xs animate-in fade-in duration-300">
|
||||
<div className="bg-zinc-900 w-full max-w-sm rounded-[40px] p-8 shadow-2xl border border-orange-500/20 relative animate-in zoom-in-95 duration-300">
|
||||
<button
|
||||
onClick={() => setSuggestion(null)}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
type="text"
|
||||
value={formData.distillery}
|
||||
onChange={(e) => setFormData({ ...formData, distillery: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
type="text"
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
inputMode="decimal"
|
||||
value={formData.abv}
|
||||
onChange={(e) => setFormData({ ...formData, abv: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-orange-500 text-sm font-bold transition-all"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-orange-500 text-sm font-bold transition-all"
|
||||
placeholder="e.g. 46.3"
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
inputMode="numeric"
|
||||
value={formData.age}
|
||||
onChange={(e) => setFormData({ ...formData, age: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all"
|
||||
placeholder="e.g. 12"
|
||||
/>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
placeholder="YYYY"
|
||||
value={formData.distilled_at}
|
||||
onChange={(e) => setFormData({ ...formData, distilled_at: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -187,7 +187,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
placeholder="YYYY"
|
||||
value={formData.bottled_at}
|
||||
onChange={(e) => setFormData({ ...formData, bottled_at: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,7 +202,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
placeholder="0.00"
|
||||
value={formData.purchase_price}
|
||||
onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 font-bold text-orange-500 text-sm transition-all"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 font-bold text-orange-500 text-sm transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -225,7 +225,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
inputMode="numeric"
|
||||
value={formData.whiskybase_id}
|
||||
onChange={(e) => setFormData({ ...formData, whiskybase_id: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-300 text-sm font-mono transition-all"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-300 text-sm font-mono transition-all"
|
||||
/>
|
||||
{discoveryResult && (
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-3 p-4 bg-zinc-900 border border-orange-500/30 rounded-2xl shadow-2xl animate-in zoom-in-95 duration-300">
|
||||
@@ -263,7 +263,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
placeholder="e.g. Batch 12 or L-Code"
|
||||
value={formData.batch_info}
|
||||
onChange={(e) => setFormData({ ...formData, batch_info: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -273,7 +273,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
placeholder="e.g. Oloroso Sherry"
|
||||
value={formData.cask_type}
|
||||
onChange={(e) => setFormData({ ...formData, cask_type: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-none focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
className="w-full px-5 py-4 bg-black/40 border border-white/5 rounded-2xl outline-hidden focus:ring-2 focus:ring-orange-600/50 text-zinc-100 text-sm font-bold transition-all placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,7 +296,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex-[2] py-4 bg-orange-600 hover:bg-orange-700 text-white rounded-2xl font-black uppercase tracking-widest text-xs transition-all flex items-center justify-center gap-2 shadow-xl shadow-orange-950/20 disabled:opacity-50"
|
||||
className="flex-2 py-4 bg-orange-600 hover:bg-orange-700 text-white rounded-2xl font-black uppercase tracking-widest text-xs transition-all flex items-center justify-center gap-2 shadow-xl shadow-orange-950/20 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
||||
{t('bottle.saveChanges')}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function FloatingScannerButton({ onImageSelected }: FloatingScann
|
||||
ease: "easeInOut",
|
||||
repeatDelay: 3
|
||||
}}
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent skew-x-12 -z-0"
|
||||
className="absolute inset-0 bg-linear-to-r from-transparent via-white/40 to-transparent skew-x-12 z-0"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function HeroBanner() {
|
||||
}}
|
||||
>
|
||||
{/* Overlay gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
|
||||
@@ -11,7 +11,7 @@ const LanguageSwitcher = () => {
|
||||
<button
|
||||
onClick={() => setLocale('de')}
|
||||
className={`p-1.5 rounded-lg transition-all ${locale === 'de'
|
||||
? 'bg-orange-950/30 scale-110 shadow-sm shadow-orange-950/20'
|
||||
? 'bg-orange-950/30 scale-110 shadow-xs shadow-orange-950/20'
|
||||
: 'opacity-50 hover:opacity-100 grayscale hover:grayscale-0'
|
||||
}`}
|
||||
title="Deutsch"
|
||||
@@ -21,7 +21,7 @@ const LanguageSwitcher = () => {
|
||||
<button
|
||||
onClick={() => setLocale('en')}
|
||||
className={`p-1.5 rounded-lg transition-all ${locale === 'en'
|
||||
? 'bg-orange-950/30 scale-110 shadow-sm shadow-orange-950/20'
|
||||
? 'bg-orange-950/30 scale-110 shadow-xs shadow-orange-950/20'
|
||||
: 'opacity-50 hover:opacity-100 grayscale hover:grayscale-0'
|
||||
}`}
|
||||
title="English"
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function NativeOCRScanner({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black">
|
||||
{/* Header */}
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between p-4 bg-gradient-to-b from-black/80 to-transparent">
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between p-4 bg-linear-to-b from-black/80 to-transparent">
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<Zap size={20} className="text-orange-500" />
|
||||
<span className="font-bold text-sm">Native OCR</span>
|
||||
@@ -236,7 +236,7 @@ export default function NativeOCRScanner({
|
||||
</div>
|
||||
|
||||
{/* Detected Text Display */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/90 to-transparent">
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-linear-to-t from-black/90 to-transparent">
|
||||
{extractedData.distillery && (
|
||||
<div className="mb-2 px-3 py-1 bg-orange-600 rounded-full inline-block">
|
||||
<span className="text-white text-sm font-bold">
|
||||
@@ -247,12 +247,12 @@ export default function NativeOCRScanner({
|
||||
|
||||
<div className="flex gap-2 flex-wrap mb-2">
|
||||
{extractedData.abv && (
|
||||
<span className="px-2 py-1 bg-white/20 rounded text-white text-xs">
|
||||
<span className="px-2 py-1 bg-white/20 rounded-sm text-white text-xs">
|
||||
{extractedData.abv}% ABV
|
||||
</span>
|
||||
)}
|
||||
{extractedData.age && (
|
||||
<span className="px-2 py-1 bg-white/20 rounded text-white text-xs">
|
||||
<span className="px-2 py-1 bg-white/20 rounded-sm text-white text-xs">
|
||||
{extractedData.age} Years
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function OnboardingTutorial() {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[200] bg-black/90 backdrop-blur-sm flex items-center justify-center p-6"
|
||||
className="fixed inset-0 z-200 bg-black/90 backdrop-blur-xs flex items-center justify-center p-6"
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function PasswordChangeForm() {
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 pr-12 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
className="w-full px-4 py-3 pr-12 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -98,7 +98,7 @@ export default function PasswordChangeForm() {
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Actions Bar */}
|
||||
<div className="bg-zinc-900 rounded-2xl p-6 border border-zinc-800 shadow-sm">
|
||||
<div className="bg-zinc-900 rounded-2xl p-6 border border-zinc-800 shadow-xs">
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
@@ -167,10 +167,10 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
className={`bg-zinc-900 rounded-[32px] p-6 border-2 ${plan.is_active
|
||||
? 'border-orange-500/30'
|
||||
: 'border-zinc-800 opacity-60'
|
||||
} shadow-sm relative`}
|
||||
} shadow-xs relative`}
|
||||
>
|
||||
{!plan.is_active && (
|
||||
<div className="absolute top-4 right-4 px-2 py-1 bg-zinc-800 text-zinc-400 text-[8px] font-bold uppercase tracking-widest rounded">
|
||||
<div className="absolute top-4 right-4 px-2 py-1 bg-zinc-800 text-zinc-400 text-[8px] font-bold uppercase tracking-widest rounded-sm">
|
||||
Inactive
|
||||
</div>
|
||||
)}
|
||||
@@ -210,7 +210,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
|
||||
{/* Edit/Create Modal */}
|
||||
{(editingPlan || isCreating) && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-zinc-900 rounded-[32px] p-6 max-w-2xl w-full border border-zinc-800 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-2xl font-bold text-white uppercase tracking-tighter">
|
||||
@@ -233,7 +233,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="e.g. starter"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -243,7 +243,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
value={formData.display_name}
|
||||
onChange={(e) => setFormData({ ...formData, display_name: e.target.value })}
|
||||
placeholder="e.g. Starter"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +255,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
type="number"
|
||||
value={formData.monthly_credits}
|
||||
onChange={(e) => setFormData({ ...formData, monthly_credits: parseInt(e.target.value) || 0 })}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -265,7 +265,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
step="0.01"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +277,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Brief description of the plan"
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -288,7 +288,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
type="number"
|
||||
value={formData.sort_order}
|
||||
onChange={(e) => setFormData({ ...formData, sort_order: parseInt(e.target.value) || 0 })}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
@@ -297,7 +297,7 @@ export default function PlanManagementClient({ initialPlans }: PlanManagementCli
|
||||
type="checkbox"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
className="w-5 h-5 rounded border-zinc-700 bg-zinc-800 text-orange-600 focus:ring-orange-600"
|
||||
className="w-5 h-5 rounded-sm border-zinc-700 bg-zinc-800 text-orange-600 focus:ring-orange-600"
|
||||
/>
|
||||
<span className="text-sm font-bold text-white">Active</span>
|
||||
</label>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function ProfileForm({ initialData }: ProfileFormProps) {
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t('bottle.nameLabel')}
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ResultCard({ data, bottleName, image, onShare }: ResultC
|
||||
className="flex flex-col items-center gap-6 w-full max-w-sm"
|
||||
>
|
||||
{/* The Trading Card */}
|
||||
<div className="relative w-full aspect-[3/4] rounded-[32px] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.9)] border border-zinc-800 bg-zinc-950 group">
|
||||
<div className="relative w-full aspect-3/4 rounded-[32px] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.9)] border border-zinc-800 bg-zinc-950 group">
|
||||
{/* Bottle Image with Vignette */}
|
||||
<div className="absolute inset-0">
|
||||
{image ? (
|
||||
@@ -46,7 +46,7 @@ export default function ResultCard({ data, bottleName, image, onShare }: ResultC
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-zinc-900 flex items-center justify-center opacity-40 text-[20px] font-bold uppercase tracking-[1em] rotate-90">No Data</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-zinc-950 via-zinc-950/20 to-transparent opacity-90" />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-zinc-950 via-zinc-950/20 to-transparent opacity-90" />
|
||||
</div>
|
||||
|
||||
{/* Content Overlay */}
|
||||
|
||||
@@ -354,12 +354,12 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[60] bg-zinc-950 flex flex-col h-[100dvh] w-screen overflow-hidden overscroll-none"
|
||||
className="fixed inset-0 z-60 bg-zinc-950 flex flex-col h-dvh w-screen overflow-hidden overscroll-none"
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 z-[70] p-2 rounded-full bg-zinc-900 border border-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
||||
className="absolute top-6 right-6 z-70 p-2 rounded-full bg-zinc-900 border border-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
@@ -520,7 +520,7 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="absolute inset-0 z-[80] bg-zinc-950/80 backdrop-blur-sm flex flex-col items-center justify-center gap-6"
|
||||
className="absolute inset-0 z-80 bg-zinc-950/80 backdrop-blur-xs flex flex-col items-center justify-center gap-6"
|
||||
>
|
||||
<Loader2 size={48} className="animate-spin text-orange-600" />
|
||||
<h2 className="text-xl font-bold text-zinc-50 uppercase tracking-tight">
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-zinc-950/80 backdrop-blur-sm z-[80]"
|
||||
className="fixed inset-0 bg-zinc-950/80 backdrop-blur-xs z-80"
|
||||
/>
|
||||
|
||||
{/* Sheet */}
|
||||
@@ -85,7 +85,7 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="fixed bottom-0 left-0 right-0 bg-[var(--background)] border-t border-white/5 rounded-t-[40px] z-[90] p-8 pb-12 max-h-[85vh] overflow-y-auto shadow-[0_-20px_60px_rgba(0,0,0,0.8)] ring-1 ring-white/5"
|
||||
className="fixed bottom-0 left-0 right-0 bg-(--background) border-t border-white/5 rounded-t-[40px] z-90 p-8 pb-12 max-h-[85vh] overflow-y-auto shadow-[0_-20px_60px_rgba(0,0,0,0.8)] ring-1 ring-white/5"
|
||||
>
|
||||
{/* Drag Handle */}
|
||||
<div className="w-10 h-1 bg-white/10 rounded-full mx-auto mb-8" />
|
||||
@@ -100,7 +100,7 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
|
||||
onChange={(e) => setNewSessionName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateSession()}
|
||||
placeholder="Neue Session erstellen..."
|
||||
className="w-full bg-zinc-900 border border-zinc-800 rounded-2xl py-4 px-6 text-zinc-50 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-900 border border-zinc-800 rounded-2xl py-4 px-6 text-zinc-50 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateSession}
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function SessionList() {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('session.sessionName')}
|
||||
className="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -201,7 +201,7 @@ export default function SessionList() {
|
||||
<div
|
||||
key={session.id}
|
||||
className={`group relative flex items-center justify-between p-5 rounded-[28px] border transition-all duration-500 overflow-hidden ${activeSession?.id === session.id
|
||||
? 'bg-orange-500/[0.03] border-orange-500/40 shadow-[0_0_40px_rgba(234,88,12,0.1)]'
|
||||
? 'bg-orange-500/3 border-orange-500/40 shadow-[0_0_40px_rgba(234,88,12,0.1)]'
|
||||
: 'bg-zinc-950/50 border-white/5 hover:border-white/10'
|
||||
}`}
|
||||
>
|
||||
@@ -289,12 +289,12 @@ export default function SessionList() {
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
|
||||
<div className="flex -space-x-1.5 overflow-hidden">
|
||||
{sessions.slice(0, 3).map((s, i) => (
|
||||
<div key={s.id} className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[10px] font-bold text-orange-600 shadow-sm">
|
||||
<div key={s.id} className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[10px] font-bold text-orange-600 shadow-xs">
|
||||
{s.name[0].toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
{sessions.length > 3 && (
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[8px] font-bold text-zinc-500 shadow-sm">
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-950 border border-zinc-800 flex items-center justify-center text-[8px] font-bold text-zinc-500 shadow-xs">
|
||||
+{sessions.length - 3}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -67,11 +67,11 @@ export default function SessionTimeline({ tastings, sessionStart, isBlind, isRev
|
||||
return (
|
||||
<div key={tasting.id} className="relative group">
|
||||
{/* Dot */}
|
||||
<div className={`absolute -left-[22px] top-4 w-5 h-5 rounded-full border-[3px] border-zinc-950 shadow-sm z-10 flex items-center justify-center ${isSmoky && showDetails ? 'bg-orange-600' : 'bg-zinc-600'}`}>
|
||||
<div className={`absolute -left-[22px] top-4 w-5 h-5 rounded-full border-[3px] border-zinc-950 shadow-xs z-10 flex items-center justify-center ${isSmoky && showDetails ? 'bg-orange-600' : 'bg-zinc-600'}`}>
|
||||
{isSmoky && showDetails && <Droplets size={8} className="text-white fill-white" />}
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900 p-4 rounded-2xl border border-zinc-800 shadow-sm hover:shadow-md transition-shadow group-hover:border-orange-500/30">
|
||||
<div className="bg-zinc-900 p-4 rounded-2xl border border-zinc-800 shadow-xs hover:shadow-md transition-shadow group-hover:border-orange-500/30">
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -92,7 +92,7 @@ export default function SessionTimeline({ tastings, sessionStart, isBlind, isRev
|
||||
{displayName}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-sm font-bold text-zinc-100 bg-zinc-800/30 blur-[4px] px-2 py-0.5 rounded-md select-none">
|
||||
<div className="text-sm font-bold text-zinc-100 bg-zinc-800/30 blur-xs px-2 py-0.5 rounded-md select-none">
|
||||
Unknown Bottle
|
||||
</div>
|
||||
)}
|
||||
|
||||
102
src/components/Skeletons.tsx
Normal file
102
src/components/Skeletons.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// Generic Skeleton components for Suspense fallbacks
|
||||
|
||||
export function TastingListSkeleton({ count = 3 }: { count?: number }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 animate-pulse"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-zinc-800 rounded-xl shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-zinc-800 rounded w-3/4" />
|
||||
<div className="h-3 bg-zinc-800 rounded w-1/2" />
|
||||
<div className="h-3 bg-zinc-800 rounded w-1/4" />
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-zinc-800 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BottleDetailsSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
{/* Image skeleton */}
|
||||
<div className="aspect-square bg-zinc-900 rounded-3xl" />
|
||||
|
||||
{/* Title skeleton */}
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 bg-zinc-800 rounded w-3/4" />
|
||||
<div className="h-5 bg-zinc-800 rounded w-1/2" />
|
||||
</div>
|
||||
|
||||
{/* Badges skeleton */}
|
||||
<div className="flex gap-2">
|
||||
<div className="h-8 w-20 bg-zinc-800 rounded-xl" />
|
||||
<div className="h-8 w-16 bg-zinc-800 rounded-xl" />
|
||||
<div className="h-8 w-24 bg-zinc-800 rounded-xl" />
|
||||
</div>
|
||||
|
||||
{/* Stats skeleton */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-20 bg-zinc-900 rounded-2xl" />
|
||||
<div className="h-20 bg-zinc-900 rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartSkeleton({ height = 200 }: { height?: number }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-2xl animate-pulse flex items-center justify-center"
|
||||
style={{ height }}
|
||||
>
|
||||
<div className="text-zinc-700 text-sm">Loading chart...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsCardSkeleton() {
|
||||
return (
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 animate-pulse">
|
||||
<div className="h-3 bg-zinc-800 rounded w-1/2 mb-2" />
|
||||
<div className="h-8 bg-zinc-800 rounded w-1/3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SessionTimelineSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4 animate-pulse">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<div className="w-3 h-3 bg-zinc-700 rounded-full" />
|
||||
<div className="flex-1 h-16 bg-zinc-900 border border-zinc-800 rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GridSkeleton({ count = 6 }: { count?: number }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-[3/4] bg-zinc-900 border border-zinc-800 rounded-2xl animate-pulse"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default function SplitCard({ split, isParticipant, onSelect, showChevron
|
||||
<Package size={10} />
|
||||
{split.amountCl}cl
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded bg-white/5 border border-white/5 ${split.status === 'SHIPPED' ? 'text-green-500' : 'text-zinc-400'}`}>
|
||||
<span className={`px-1.5 py-0.5 rounded-sm bg-white/5 border border-white/5 ${split.status === 'SHIPPED' ? 'text-green-500' : 'text-zinc-400'}`}>
|
||||
{statusLabels[split.status || ''] || split.status}
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -75,23 +75,23 @@ export default function SplitProgressBar({
|
||||
{showLabels && (
|
||||
<div className="flex flex-wrap gap-3 text-[10px] font-bold">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm bg-zinc-600" />
|
||||
<div className="w-2.5 h-2.5 rounded-xs bg-zinc-600" />
|
||||
<span className="text-zinc-500">Host ({hostShare}cl)</span>
|
||||
</div>
|
||||
{taken > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm bg-orange-600" />
|
||||
<div className="w-2.5 h-2.5 rounded-xs bg-orange-600" />
|
||||
<span className="text-zinc-500">Vergeben ({taken}cl)</span>
|
||||
</div>
|
||||
)}
|
||||
{reserved > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm bg-yellow-500" />
|
||||
<div className="w-2.5 h-2.5 rounded-xs bg-yellow-500" />
|
||||
<span className="text-zinc-500">Reserviert ({reserved}cl)</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm bg-green-500" />
|
||||
<div className="w-2.5 h-2.5 rounded-xs bg-green-500" />
|
||||
<span className="text-zinc-500">Verfügbar ({available}cl)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function TagSelector({ category, selectedTagIds, onToggleTag, lab
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Tag suchen oder hinzufügen..."
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-2xl text-[11px] font-medium focus:ring-1 focus:ring-orange-600/50 focus:border-orange-600/50 outline-none transition-all text-zinc-200 placeholder:text-zinc-600"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-2xl text-[11px] font-medium focus:ring-1 focus:ring-orange-600/50 focus:border-orange-600/50 outline-hidden transition-all text-zinc-200 placeholder:text-zinc-600"
|
||||
/>
|
||||
{isCreating && (
|
||||
<Loader2 className="absolute right-3.5 animate-spin text-orange-600" size={14} />
|
||||
@@ -150,7 +150,7 @@ export default function TagSelector({ category, selectedTagIds, onToggleTag, lab
|
||||
key={tag.id}
|
||||
type="button"
|
||||
onClick={() => onToggleTag(tag.id)}
|
||||
className="px-3 py-1.5 rounded-xl bg-orange-950/20 text-orange-500 text-[10px] font-black uppercase tracking-tight hover:bg-orange-600 hover:text-white transition-all border border-orange-600/20 flex items-center gap-1.5 shadow-sm"
|
||||
className="px-3 py-1.5 rounded-xl bg-orange-950/20 text-orange-500 text-[10px] font-black uppercase tracking-tight hover:bg-orange-600 hover:text-white transition-all border border-orange-600/20 flex items-center gap-1.5 shadow-xs"
|
||||
>
|
||||
<Sparkles size={10} />
|
||||
{tag.is_system_default ? t(`aroma.${tag.name}`) : tag.name}
|
||||
|
||||
@@ -376,7 +376,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleName}
|
||||
onChange={(e) => setBottleName(e.target.value)}
|
||||
placeholder="e.g. 12 Year Old"
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-all ${!checkConfidence('name') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ${!checkConfidence('name') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -398,7 +398,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleDistillery}
|
||||
onChange={(e) => setBottleDistillery(e.target.value)}
|
||||
placeholder="e.g. Lagavulin"
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-all ${!checkConfidence('distillery') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ${!checkConfidence('distillery') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -420,7 +420,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleAbv}
|
||||
onChange={(e) => setBottleAbv(e.target.value)}
|
||||
placeholder="43.0"
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-all ${!checkConfidence('abv') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ${!checkConfidence('abv') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -439,7 +439,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleAge}
|
||||
onChange={(e) => setBottleAge(e.target.value)}
|
||||
placeholder="12"
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-all ${!checkConfidence('age') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ${!checkConfidence('age') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleCategory}
|
||||
onChange={(e) => setBottleCategory(e.target.value)}
|
||||
placeholder="e.g. Single Malt"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
{/* Cask Type */}
|
||||
@@ -475,7 +475,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleCaskType}
|
||||
onChange={(e) => setBottleCaskType(e.target.value)}
|
||||
placeholder="e.g. Oloroso Sherry Cask"
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-all ${!checkConfidence('cask_type') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
className={`w-full bg-zinc-950 border rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ${!checkConfidence('cask_type') ? 'border-yellow-600/50 shadow-[0_0_10px_rgba(202,138,4,0.1)]' : 'border-zinc-800'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -492,7 +492,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleVintage}
|
||||
onChange={(e) => setBottleVintage(e.target.value)}
|
||||
placeholder="e.g. 2007"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -506,7 +506,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleBottler}
|
||||
onChange={(e) => setBottleBottler(e.target.value)}
|
||||
placeholder="e.g. Independent Bottler"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -520,7 +520,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleDistilledAt}
|
||||
onChange={(e) => setBottleDistilledAt(e.target.value)}
|
||||
placeholder="e.g. 2007"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -534,7 +534,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleBottledAt}
|
||||
onChange={(e) => setBottleBottledAt(e.target.value)}
|
||||
placeholder="e.g. 2024"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -548,7 +548,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleBatchInfo}
|
||||
onChange={(e) => setBottleBatchInfo(e.target.value)}
|
||||
placeholder="e.g. Oloroso Sherry Cask"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -562,7 +562,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={bottleCode}
|
||||
onChange={(e) => setBottleCode(e.target.value)}
|
||||
placeholder="e.g. WB271235"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 font-mono focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-700 font-mono focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -575,7 +575,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-orange-600 transition-colors"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-hidden focus:border-orange-600 transition-colors"
|
||||
>
|
||||
<option value="sealed">Versiegelt</option>
|
||||
<option value="open">Offen</option>
|
||||
@@ -672,7 +672,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={guessAbv}
|
||||
onChange={(e) => setGuessAbv(e.target.value)}
|
||||
placeholder="z.B. 46.3"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-none transition-all"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-hidden transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -682,7 +682,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={guessAge}
|
||||
onChange={(e) => setGuessAge(e.target.value)}
|
||||
placeholder="z.B. 12"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-none transition-all"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-hidden transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
@@ -692,7 +692,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
value={guessRegion}
|
||||
onChange={(e) => setGuessRegion(e.target.value)}
|
||||
placeholder="z.B. Islay / Lagavulin"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-none transition-all"
|
||||
className="w-full bg-black/40 border border-purple-500/20 rounded-2xl px-5 py-3 text-sm text-white focus:border-purple-500/50 outline-hidden transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -729,7 +729,7 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
|
||||
</div>
|
||||
|
||||
{/* Fixed/Sticky Footer for Save Action */}
|
||||
<div className="w-full p-6 bg-gradient-to-t from-zinc-950 via-zinc-950/95 to-transparent border-t border-white/5 shrink-0 z-20">
|
||||
<div className="w-full p-6 bg-linear-to-t from-zinc-950 via-zinc-950/95 to-transparent border-t border-white/5 shrink-0 z-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<button
|
||||
onClick={handleInternalSave}
|
||||
|
||||
@@ -161,7 +161,7 @@ export default function TastingFormBody({
|
||||
onChange={(e) => setNose(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-none resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-hidden resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ export default function TastingFormBody({
|
||||
onChange={(e) => setPalate(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-none resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-hidden resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,7 +281,7 @@ export default function TastingFormBody({
|
||||
onChange={(e) => setFinish(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-none resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
className="w-full p-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-sm focus:ring-1 focus:ring-orange-600 outline-hidden resize-none transition-all text-zinc-200 placeholder:text-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function TastingHub({ isOpen, onClose }: TastingHubProps) {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[60]"
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-xs z-60"
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
@@ -188,7 +188,7 @@ export default function TastingHub({ isOpen, onClose }: TastingHubProps) {
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="fixed bottom-0 left-0 right-0 h-[85vh] bg-[#09090b] border-t border-white/10 rounded-t-[40px] z-[70] flex flex-col shadow-2xl overflow-hidden"
|
||||
className="fixed bottom-0 left-0 right-0 h-[85vh] bg-[#09090b] border-t border-white/10 rounded-t-[40px] z-70 flex flex-col shadow-2xl overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-8 pb-4 flex items-center justify-between shrink-0">
|
||||
@@ -242,7 +242,7 @@ export default function TastingHub({ isOpen, onClose }: TastingHubProps) {
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('hub.placeholders.sessionName')}
|
||||
className="flex-1 bg-black/40 border border-white/5 rounded-2xl px-6 py-4 text-sm font-bold text-white placeholder:text-zinc-700 focus:outline-none focus:border-orange-600 transition-all ring-inset focus:ring-1 focus:ring-orange-600/50"
|
||||
className="flex-1 bg-black/40 border border-white/5 rounded-2xl px-6 py-4 text-sm font-bold text-white placeholder:text-zinc-700 focus:outline-hidden focus:border-orange-600 transition-all ring-inset focus:ring-1 focus:ring-orange-600/50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function TastingList({ initialTastings, currentUserId, bottleId }
|
||||
{sortedTastings.map((note) => (
|
||||
<div
|
||||
key={note.id}
|
||||
className="bg-white dark:bg-zinc-900 p-6 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm space-y-4 hover:border-amber-500/30 transition-all hover:shadow-md group"
|
||||
className="bg-white dark:bg-zinc-900 p-6 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-xs space-y-4 hover:border-amber-500/30 transition-all hover:shadow-md group"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
@@ -192,7 +192,7 @@ export default function TastingList({ initialTastings, currentUserId, bottleId }
|
||||
<button
|
||||
onClick={() => note.id && note.bottle_id && handleDelete(note.id, note.bottle_id)}
|
||||
disabled={!!isDeleting}
|
||||
className="px-3 py-1.5 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded-xl transition-all disabled:opacity-50 flex items-center gap-2 border border-red-100 dark:border-red-900/30 font-black text-[10px] uppercase tracking-widest shadow-sm hover:bg-red-600 hover:text-white dark:hover:bg-red-600 dark:hover:text-white"
|
||||
className="px-3 py-1.5 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded-xl transition-all disabled:opacity-50 flex items-center gap-2 border border-red-100 dark:border-red-900/30 font-black text-[10px] uppercase tracking-widest shadow-xs hover:bg-red-600 hover:text-white dark:hover:bg-red-600 dark:hover:text-white"
|
||||
title="Tasting löschen"
|
||||
>
|
||||
{isDeleting === note.id ? (
|
||||
|
||||
@@ -305,7 +305,7 @@ export default function TastingNoteForm({ bottleId, pendingBottleId, sessionId,
|
||||
)}
|
||||
|
||||
{/* Sticky Save Button Container */}
|
||||
<div className="sticky bottom-0 -mx-6 px-6 py-4 bg-gradient-to-t from-zinc-950 via-zinc-950/90 to-transparent z-10">
|
||||
<div className="sticky bottom-0 -mx-6 px-6 py-4 bg-linear-to-t from-zinc-950 via-zinc-950/90 to-transparent z-10">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
|
||||
@@ -274,7 +274,7 @@ export default function UploadQueue() {
|
||||
if (totalInQueue === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 right-6 md:bottom-6 md:right-6 z-[100] flex flex-col items-end gap-3 translate-y-0">
|
||||
<div className="fixed bottom-24 right-6 md:bottom-6 md:right-6 z-100 flex flex-col items-end gap-3 translate-y-0">
|
||||
<AnimatePresence mode="wait">
|
||||
{!isCollapsed ? (
|
||||
<motion.div
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Search Bar */}
|
||||
<div className="bg-zinc-900 rounded-[32px] p-6 border border-zinc-800 shadow-sm">
|
||||
<div className="bg-zinc-900 rounded-[32px] p-6 border border-zinc-800 shadow-xs">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" size={20} />
|
||||
<input
|
||||
@@ -174,13 +174,13 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
placeholder="Search users by email or username..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-800 border border-zinc-700 rounded-2xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full pl-10 pr-4 py-3 bg-zinc-800 border border-zinc-700 rounded-2xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Table */}
|
||||
<div className="bg-zinc-900 rounded-[32px] p-6 border border-zinc-800 shadow-sm overflow-hidden">
|
||||
<div className="bg-zinc-900 rounded-[32px] p-6 border border-zinc-800 shadow-xs overflow-hidden">
|
||||
<h2 className="text-xl font-bold text-white uppercase tracking-tighter mb-6">Users ({filteredUsers.length})</h2>
|
||||
<div className="overflow-x-auto -mx-6">
|
||||
<table className="w-full">
|
||||
@@ -235,7 +235,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editingUser && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-zinc-900 rounded-[32px] p-8 max-w-2xl w-full max-h-[90vh] overflow-y-auto border border-zinc-800 shadow-2xl space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -278,7 +278,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
value={creditAmount}
|
||||
onChange={(e) => setCreditAmount(e.target.value)}
|
||||
placeholder="e.g. 100 or -50"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -288,7 +288,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="e.g. Monthly bonus"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +319,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
<select
|
||||
value={selectedPlan}
|
||||
onChange={(e) => setSelectedPlan(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white appearance-none"
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white appearance-none"
|
||||
>
|
||||
<option value="">Select a plan...</option>
|
||||
{plans.map(plan => (
|
||||
@@ -352,7 +352,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
value={dailyLimit}
|
||||
onChange={(e) => setDailyLimit(e.target.value)}
|
||||
placeholder="Global (80)"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -361,7 +361,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
type="number"
|
||||
value={googleCost}
|
||||
onChange={(e) => setGoogleCost(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -370,7 +370,7 @@ export default function UserManagementClient({ initialUsers, plans }: UserManage
|
||||
type="number"
|
||||
value={geminiCost}
|
||||
onChange={(e) => setGeminiCost(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-orange-600/50 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
70
src/hooks/useOptimistic.ts
Normal file
70
src/hooks/useOptimistic.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { useOptimistic, useTransition } from 'react';
|
||||
|
||||
/**
|
||||
* Hook for optimistic updates with automatic rollback on error.
|
||||
* Uses React 19's useOptimistic + startTransition pattern.
|
||||
*
|
||||
* @example
|
||||
* const { optimisticData, isPending, mutate } = useOptimisticMutation(
|
||||
* tastings,
|
||||
* async (newTasting) => await saveTasting(newTasting)
|
||||
* );
|
||||
*/
|
||||
export function useOptimisticMutation<T, TInput>(
|
||||
initialData: T[],
|
||||
mutationFn: (input: TInput) => Promise<{ success: boolean; error?: string }>
|
||||
) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const [optimisticData, addOptimistic] = useOptimistic<T[], TInput>(
|
||||
initialData,
|
||||
(currentData, newItem) => [...currentData, newItem as unknown as T]
|
||||
);
|
||||
|
||||
const mutate = async (input: TInput, optimisticValue: T) => {
|
||||
startTransition(async () => {
|
||||
// Immediately show optimistic update
|
||||
addOptimistic(input);
|
||||
|
||||
// Perform actual mutation
|
||||
const result = await mutationFn(input);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('[OptimisticMutation] Failed:', result.error);
|
||||
// Note: React will automatically rollback on error
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
optimisticData,
|
||||
isPending,
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple optimistic state for single values (like ratings).
|
||||
*/
|
||||
export function useOptimisticValue<T>(
|
||||
serverValue: T,
|
||||
updateFn: (value: T) => Promise<{ success: boolean }>
|
||||
) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [optimisticValue, setOptimistic] = useOptimistic(serverValue);
|
||||
|
||||
const setValue = (value: T) => {
|
||||
startTransition(async () => {
|
||||
setOptimistic(value);
|
||||
await updateFn(value);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
value: optimisticValue,
|
||||
isPending,
|
||||
setValue,
|
||||
};
|
||||
}
|
||||
@@ -18,19 +18,28 @@ const translations: Record<Locale, TranslationKeys> = { de, en };
|
||||
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
||||
|
||||
export const I18nProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [locale, setLocaleState] = useState<Locale>('de');
|
||||
const [locale, setLocaleState] = useState<Locale>('en');
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run on client side
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Check for saved preference first
|
||||
const savedLocale = localStorage.getItem('locale') as Locale;
|
||||
if (savedLocale && (savedLocale === 'de' || savedLocale === 'en')) {
|
||||
setLocaleState(savedLocale);
|
||||
} else {
|
||||
// Try to detect browser language
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
if (browserLang === 'en') {
|
||||
setLocaleState('en');
|
||||
// Auto-detect from browser: default to English, switch to German if detected
|
||||
const browserLang = navigator.language?.toLowerCase() || 'en';
|
||||
if (browserLang.startsWith('de')) {
|
||||
setLocaleState('de');
|
||||
localStorage.setItem('locale', 'de');
|
||||
} else {
|
||||
localStorage.setItem('locale', 'en');
|
||||
}
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}, []);
|
||||
|
||||
const setLocale = (newLocale: Locale) => {
|
||||
|
||||
@@ -4,15 +4,19 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
let supabaseClient: SupabaseClient | null = null;
|
||||
|
||||
export function createClient() {
|
||||
if (supabaseClient) return supabaseClient;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Supabase URL and Anon Key must be defined');
|
||||
if (typeof window === 'undefined') {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
return createBrowserClient(supabaseUrl, supabaseAnonKey);
|
||||
}
|
||||
|
||||
supabaseClient = createBrowserClient(supabaseUrl, supabaseAnonKey);
|
||||
return supabaseClient;
|
||||
// Singleton for client-side to prevent multiple instances
|
||||
// Use window object to persist across module reloads in dev
|
||||
if (!(window as any).supabase) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
(window as any).supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);
|
||||
}
|
||||
|
||||
return (window as any).supabase as SupabaseClient;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ export async function middleware(request: NextRequest) {
|
||||
path.startsWith("/_next"); // Static assets
|
||||
|
||||
// 2. Specialized Logic for /splits
|
||||
// - Public: /splits/[slug]
|
||||
// - Protected: /splits/create, /splits/manage
|
||||
const isSplitsPublic =
|
||||
path.startsWith("/splits/") &&
|
||||
!path.startsWith("/splits/create") &&
|
||||
@@ -75,11 +73,6 @@ export async function middleware(request: NextRequest) {
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
// 4. Admin Protection (Optional Layer in Middleware, but Page also checks)
|
||||
// We rely on Page component for Admin role check to avoid DB hit in middleware if possible,
|
||||
// OR we can trust getUser() + Page logic.
|
||||
// Middleware mainly ensures *Authentication*. Authorization is Page level.
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
53
src/proxy.ts
53
src/proxy.ts
@@ -1,53 +0,0 @@
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll();
|
||||
},
|
||||
setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
request.cookies.set(name, value)
|
||||
);
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
response.cookies.set(name, value, options)
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
const url = new URL(request.url);
|
||||
const isStatic = url.pathname.startsWith('/_next') || url.pathname.includes('/icon-') || url.pathname === '/favicon.ico';
|
||||
|
||||
if (!isStatic) {
|
||||
const status = user ? `User:${user.id.slice(0, 8)}` : 'No Session';
|
||||
console.log(`[Proxy] ${request.method} ${url.pathname} | ${status}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
};
|
||||
191
src/services/analyze-bottle-mistral.ts
Normal file
191
src/services/analyze-bottle-mistral.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
'use server';
|
||||
|
||||
import { Mistral } from '@mistralai/mistralai';
|
||||
import { getSystemPrompt } from '@/lib/ai-prompts';
|
||||
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { createHash } from 'crypto';
|
||||
import { trackApiUsage } from './track-api-usage';
|
||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||
|
||||
// WICHTIG: Wir akzeptieren jetzt FormData statt Strings
|
||||
export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
if (!process.env.MISTRAL_API_KEY) {
|
||||
return { success: false, error: 'MISTRAL_API_KEY is not configured.' };
|
||||
}
|
||||
|
||||
let supabase;
|
||||
try {
|
||||
// Helper to get value from either FormData or POJO
|
||||
const getValue = (obj: any, key: string): any => {
|
||||
if (obj && typeof obj.get === 'function') return obj.get(key);
|
||||
if (obj && typeof obj[key] !== 'undefined') return obj[key];
|
||||
return null;
|
||||
};
|
||||
|
||||
// 1. Daten extrahieren
|
||||
const file = getValue(input, 'file') as File;
|
||||
const tagsString = getValue(input, 'tags') as string;
|
||||
const locale = getValue(input, 'locale') || 'de';
|
||||
|
||||
if (!file) {
|
||||
return { success: false, error: 'Kein Bild empfangen.' };
|
||||
}
|
||||
|
||||
const tags = tagsString ? (typeof tagsString === 'string' ? JSON.parse(tagsString) : tagsString) : [];
|
||||
|
||||
// 2. Auth & Credits
|
||||
supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' };
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
const creditCheck = await checkCreditBalance(userId, 'gemini_ai');
|
||||
if (!creditCheck.allowed) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Nicht genügend Credits. Benötigt: ${creditCheck.cost}, Verfügbar: ${creditCheck.balance}.`
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Datei in Buffer umwandeln
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// 4. Hash für Cache erstellen
|
||||
const imageHash = createHash('sha256').update(buffer).digest('hex');
|
||||
|
||||
// Cache Check
|
||||
const { data: cachedResult } = await supabase
|
||||
.from('vision_cache')
|
||||
.select('result')
|
||||
.eq('hash', imageHash)
|
||||
.maybeSingle();
|
||||
|
||||
if (cachedResult) {
|
||||
return {
|
||||
success: true,
|
||||
data: cachedResult.result as any,
|
||||
perf: {
|
||||
apiDuration: 0,
|
||||
parseDuration: 0,
|
||||
uploadSize: buffer.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Für Mistral vorbereiten
|
||||
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
|
||||
const base64Data = buffer.toString('base64');
|
||||
const mimeType = file.type || 'image/webp';
|
||||
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
||||
const uploadSize = buffer.length;
|
||||
|
||||
const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
||||
|
||||
try {
|
||||
const startApi = performance.now();
|
||||
const chatResponse = await client.chat.complete({
|
||||
model: 'mistral-large-latest',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', imageUrl: dataUrl }
|
||||
]
|
||||
}
|
||||
],
|
||||
responseFormat: { type: 'json_object' },
|
||||
temperature: 0.1
|
||||
});
|
||||
const endApi = performance.now();
|
||||
|
||||
const startParse = performance.now();
|
||||
const rawContent = chatResponse.choices?.[0].message.content;
|
||||
if (!rawContent) throw new Error("Keine Antwort von Mistral");
|
||||
|
||||
let jsonData;
|
||||
try {
|
||||
jsonData = JSON.parse(rawContent as string);
|
||||
} catch (e) {
|
||||
const cleanedText = (rawContent as string).replace(/```json/g, '').replace(/```/g, '');
|
||||
jsonData = JSON.parse(cleanedText);
|
||||
}
|
||||
|
||||
if (Array.isArray(jsonData)) jsonData = jsonData[0];
|
||||
console.log('[Mistral AI] JSON Response:', jsonData);
|
||||
|
||||
const searchString = jsonData.search_string;
|
||||
delete jsonData.search_string;
|
||||
|
||||
if (typeof jsonData.abv === 'string') {
|
||||
// Fix: Global replace to remove all % signs
|
||||
jsonData.abv = parseFloat(jsonData.abv.replace(/%/g, '').trim());
|
||||
}
|
||||
|
||||
if (jsonData.age) jsonData.age = parseInt(jsonData.age);
|
||||
if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage);
|
||||
|
||||
const validatedData = BottleMetadataSchema.parse(jsonData);
|
||||
const endParse = performance.now();
|
||||
|
||||
await trackApiUsage({
|
||||
userId: userId,
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'mistral/mistral-large',
|
||||
success: true,
|
||||
provider: 'mistral',
|
||||
model: 'mistral-large-latest',
|
||||
responseText: rawContent as string
|
||||
});
|
||||
|
||||
await deductCredits(userId, 'gemini_ai', 'Mistral AI analysis');
|
||||
|
||||
await supabase
|
||||
.from('vision_cache')
|
||||
.insert({ hash: imageHash, result: validatedData });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: validatedData,
|
||||
search_string: searchString,
|
||||
perf: {
|
||||
apiDuration: endApi - startApi,
|
||||
parseDuration: endParse - startParse,
|
||||
uploadSize: uploadSize
|
||||
},
|
||||
raw: jsonData
|
||||
};
|
||||
|
||||
} catch (aiError: any) {
|
||||
console.warn('[MistralAnalysis] AI Analysis failed, providing fallback path:', aiError.message);
|
||||
|
||||
await trackApiUsage({
|
||||
userId: userId,
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'mistral/mistral-large',
|
||||
success: false,
|
||||
errorMessage: aiError.message,
|
||||
provider: 'mistral',
|
||||
model: 'mistral-large-latest'
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
isAiError: true,
|
||||
error: aiError.message,
|
||||
imageHash: imageHash
|
||||
} as any;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mistral Analysis Global Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Mistral AI analysis failed.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
// High-contrast zinc scale for better readability
|
||||
// Original zinc-500 (#71717a) is now brighter for better contrast on dark backgrounds
|
||||
colors: {
|
||||
zinc: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a8a8b3', // Brighter (was #a1a1aa)
|
||||
500: '#8a8a95', // Brighter (was #71717a) - main secondary text
|
||||
600: '#6b6b75', // Brighter (was #52525b) - subtle text
|
||||
700: '#4a4a52', // Brighter (was #3f3f46)
|
||||
800: '#2a2a2e', // Slightly adjusted
|
||||
900: '#1a1a1e', // Dark background
|
||||
950: '#0d0d0f', // Darkest
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
Reference in New Issue
Block a user