UPDATE: Update UI
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 50s

This commit is contained in:
2026-03-28 21:24:45 +07:00
parent c447618a15
commit d775f6cdc1
14 changed files with 159 additions and 165 deletions

View File

@@ -15,8 +15,7 @@ The application is deployed and available at the following URLs:
| Role | URL | | Role | URL |
| :--- | :--- | | :--- | :--- |
| **Main Site** | [srtools.kain.id.vn](https://srtools.kain.id.vn) | | **Main Site** | [srtools.punklorde.org](https://srtools.punklorde.org) |
| **Backup / Vercel Mirror** | [firefly-srtools.vercel.app](https://firefly-srtools.vercel.app) |
--- ---

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "技能类型", "skillType": "技能类型",

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Skill Type", "skillType": "Skill Type",

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "スキルタイプ", "skillType": "スキルタイプ",

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "스킬 유형", "skillType": "스킬 유형",

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Loại kỹ năng", "skillType": "Loại kỹ năng",

View File

@@ -1,7 +1,7 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Kain" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "技能类型", "skillType": "技能类型",

View File

@@ -25,7 +25,7 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Firefly SrTools", title: "Firefly SrTools",
description: "SrTools by Kain", description: "SrTools by Firefly Shelter",
icons: { icons: {
icon: "/ff-srtool.png", icon: "/ff-srtool.png",
shortcut: "/ff-srtool.ico", shortcut: "/ff-srtool.ico",
@@ -33,12 +33,12 @@ export const metadata: Metadata = {
}, },
openGraph: { openGraph: {
title: "Firefly SrTools", title: "Firefly SrTools",
description: "SrTools by Kain", description: "SrTools by Firefly Shelter",
url: "https://srtools.kain.id.vn", url: "https://srtools.punklorde.org",
siteName: "Firefly SrTools", siteName: "Firefly SrTools",
images: [ images: [
{ {
url: "https://srtools.kain.id.vn/ff-srtool.png", url: "https://srtools.punklorde.org/ff-srtool.png",
width: 312, width: 312,
height: 312, height: 312,
alt: "Firefly SrTools Logo", alt: "Firefly SrTools Logo",
@@ -50,8 +50,8 @@ export const metadata: Metadata = {
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "Firefly SrTools", title: "Firefly SrTools",
description: "SrTools by Kain", description: "SrTools by Firefly Shelter",
images: ["https://srtools.kain.id.vn/ff-srtool.png"], images: ["https://srtools.punklorde.org/ff-srtool.png"],
}, },
}; };

View File

@@ -417,7 +417,7 @@ export default function ActionBar() {
{modalConfigs.map(({ id, title, onClose, content }) => ( {modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal"> <dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}

View File

@@ -1,6 +1,6 @@
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { Check, AlertCircle, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react'; import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() { export default function ChangelogBar() {
const { changelog } = useLocaleStore() const { changelog } = useLocaleStore()
@@ -47,12 +47,9 @@ export default function ChangelogBar() {
return ( return (
<div className="md:p-4"> <div className="md:p-4">
{/* Alert */} {/* Alert */}
<div className="alert alert-info mb-4 md:p-2 p-1"> {/* <div className="alert alert-info mb-4 md:p-2 p-1">
<AlertCircle className="w-6 h-6" /> <AlertCircle className="w-6 h-6" />
<div> </div> */}
<h3 className="font-bold text-sm">If you have any suggestions or problems, please contact me! @kain0304</h3>
</div>
</div>
{/* Timeline */} {/* Timeline */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -2,7 +2,7 @@ export default function Footer() {
return ( return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10"> <footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside> <aside>
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p> <p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
</aside> </aside>
</footer> </footer>
) )

View File

@@ -359,7 +359,7 @@ export default function Header() {
Tools Tools
</span> </span>
</h1> </h1>
<p className="text-sm text-gray-500">By Kain</p> <p className="text-sm text-gray-500">Firefly Shelter</p>
</div> </div>
</div> </div>
</a> </a>
@@ -567,7 +567,7 @@ export default function Header() {
{modalConfigs?.map(({ id, title, onClose, content }) => ( {modalConfigs?.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal"> <dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}

View File

@@ -245,7 +245,7 @@ export default function RelicMaker() {
{transI18n("relicMaker")} {transI18n("relicMaker")}
</h3> </h3>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{/* Left Panel */} {/* Left Panel */}
<div className="space-y-6"> <div className="space-y-6">

View File

@@ -40,7 +40,7 @@ export default function RelicsInfo() {
const { avatarSelected } = useCurrentDataStore() const { avatarSelected } = useCurrentDataStore()
const { mapRelicSet, subAffix } = useDetailDataStore() const { mapRelicSet, subAffix } = useDetailDataStore()
const { locale } = useLocaleStore() const { locale } = useLocaleStore()
const handleShow = (modalId: string) => { const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
@@ -155,16 +155,16 @@ export default function RelicsInfo() {
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
for (const item of modalConfigs) { for (const item of modalConfigs) {
if (!item?.isOpen) { if (!item?.isOpen) {
handleCloseModal(item?.id || "") handleCloseModal(item?.id || "")
} }
} }
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
for (const item of modalConfigs) { for (const item of modalConfigs) {
handleCloseModal(item?.id || "") handleCloseModal(item?.id || "")
}
} }
}
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
@@ -173,164 +173,162 @@ export default function RelicsInfo() {
return ( return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden"> <div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
<div className="max-w-7xl"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Section - Items Grid */} {/* Left Section - Items Grid */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> <div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content"> <h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> <div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("relics")} {transI18n("relics")}
</h2> </h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 max-w-2xl"> <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{["1", "2", "3", "4", "5", "6"].map((item, index) => ( {["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div key={index} className="relative group"> <div key={index} className="relative group">
<div <div
onClick={() => { onClick={() => {
if (item === selectedRelicSlot) { if (item === selectedRelicSlot) {
setSelectedRelicSlot("") setSelectedRelicSlot("")
} else { } else {
setSelectedRelicSlot(item) setSelectedRelicSlot(item)
} }
handlerChangeRelic(item)
}}
className="cursor-pointer"
>
<RelicCard
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item) handlerChangeRelic(item)
}} }}
className="cursor-pointer" className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")}
> >
<RelicCard <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
slot={item} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
avatarId={avatarSelected?.ID.toString() || ""} </svg>
/> </button>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1"> {getRelic(item) && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handlerChangeRelic(item) if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}} }}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200" className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")} title={transI18n("deleteRelic")}
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
</button> </button>
)}
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
</div> </div>
))} </div>
</div> ))}
<div className="grid grid-cols-2 gap-2 mt-10"> </div>
<button <div className="grid grid-cols-2 gap-2 mt-10">
disabled={!selectedRelicSlot}
onClick={() => {
handlerChangeRelic(selectedRelicSlot)
}}
className="btn btn-info"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button>
</div>
<button <button
disabled={!selectedRelicSlot}
onClick={() => { onClick={() => {
setIsOpenQuickView(true) handlerChangeRelic(selectedRelicSlot)
handleShow("quick_view_modal")
}} }}
className="btn btn-info w-full mt-2" className="btn btn-info"
> >
{transI18n("quickView")} {transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button> </button>
</div> </div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div> </div>
</div>
{/* Right Section - Stats and Set Effects */} {/* Right Section - Stats and Set Effects */}
<div className="space-y-6"> <div className="space-y-6">
{/* Set Effects Panel */} {/* Set Effects Panel */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> <div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content"> <h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> <div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("setEffects")} {transI18n("setEffects")}
</h3> </h3>
<div className="space-y-6"> <div className="space-y-6">
{relicEffects.map((setEffect, index) => { {relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key]; const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null; if (!relicInfo) return null;
return ( return (
<div key={index} className="space-y-3"> <div key={index} className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="font-bold text-warning" className="font-bold text-warning"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, relicInfo.Name), getLocaleName(locale, relicInfo.Name),
[] []
)
}}
/>
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div>
<div className="space-y-2 pl-4">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
) )
})} }}
</div> />
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div> </div>
)
})} <div className="space-y-2 pl-4">
</div> {Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div> </div>
</div> </div>
</div> </div>
@@ -338,7 +336,7 @@ export default function RelicsInfo() {
{modalConfigs.map(({ id, title, onClose, content }) => ( {modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal"> <dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}