diff --git a/app/[locale]/[username]/ProfilePageClient.tsx b/app/[locale]/[username]/ProfilePageClient.tsx index 094a745b..49a38a41 100644 --- a/app/[locale]/[username]/ProfilePageClient.tsx +++ b/app/[locale]/[username]/ProfilePageClient.tsx @@ -122,23 +122,33 @@ const ProfilePageClient: React.FC = ({ setFetching(true) try { - // Construct URL for fetching more data - const url = new URL(`/api/parties`, window.location.origin) - url.searchParams.set('username', initialData.user.username) + // Construct URL for fetching more data - using the users endpoint + const url = new URL(`${process.env.NEXT_PUBLIC_SIERO_API_URL}/users/${initialData.user.username}`, window.location.origin) url.searchParams.set('page', (currentPage + 1).toString()) if (element) url.searchParams.set('element', element.toString()) - if (raid) url.searchParams.set('raid', raid) + if (raid) url.searchParams.set('raid_id', raid) if (recency) url.searchParams.set('recency', recency) - const response = await fetch(url.toString()) + const response = await fetch(url.toString(), { + headers: { + 'Content-Type': 'application/json' + } + }) const data = await response.json() - if (data.parties && Array.isArray(data.parties)) { - setParties([...parties, ...data.parties]) - setCurrentPage(data.pagination?.current_page || currentPage + 1) - setTotalPages(data.pagination?.total_pages || totalPages) - setRecordCount(data.pagination?.record_count || recordCount) + // Extract parties from the profile response + const newParties = data.profile?.parties || [] + + if (newParties.length > 0) { + setParties([...parties, ...newParties]) + + // Update pagination from meta + if (data.meta) { + setCurrentPage(currentPage + 1) + setTotalPages(data.meta.total_pages || totalPages) + setRecordCount(data.meta.count || recordCount) + } } } catch (error) { console.error('Error loading more parties', error) @@ -222,12 +232,7 @@ const ProfilePageClient: React.FC = ({ raidGroups={initialData.raidGroups} recency={recency} > - +
{renderInfiniteScroll}
diff --git a/app/[locale]/[username]/page.tsx b/app/[locale]/[username]/page.tsx index a6ee7f5d..6bfd45e1 100644 --- a/app/[locale]/[username]/page.tsx +++ b/app/[locale]/[username]/page.tsx @@ -58,7 +58,6 @@ export default async function ProfilePage({ notFound() } - // Prepare data for client component const initialData = { user: userData.user, teams: teamsData.results || [], diff --git a/app/[locale]/about/AboutPageClient.tsx b/app/[locale]/about/AboutPageClient.tsx new file mode 100644 index 00000000..71789014 --- /dev/null +++ b/app/[locale]/about/AboutPageClient.tsx @@ -0,0 +1,99 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' +import { useRouter, usePathname } from '~/i18n/navigation' +import { AboutTabs } from '~/utils/enums' + +import AboutPage from '~/components/about/AboutPage' +import UpdatesPage from '~/components/about/UpdatesPage' +import RoadmapPage from '~/components/about/RoadmapPage' +import SegmentedControl from '~/components/common/SegmentedControl' +import Segment from '~/components/common/Segment' + +export default function AboutPageClient() { + const t = useTranslations('common') + const router = useRouter() + const pathname = usePathname() + + const [currentTab, setCurrentTab] = useState(AboutTabs.About) + + useEffect(() => { + const parts = pathname.split('/') + const lastPart = parts[parts.length - 1] + + switch (lastPart) { + case 'about': + setCurrentTab(AboutTabs.About) + break + case 'updates': + setCurrentTab(AboutTabs.Updates) + break + case 'roadmap': + setCurrentTab(AboutTabs.Roadmap) + break + default: + setCurrentTab(AboutTabs.About) + } + }, [pathname]) + + function handleTabClicked(event: React.ChangeEvent) { + const value = event.target.value + router.push(`/${value}`) + + switch (value) { + case 'about': + setCurrentTab(AboutTabs.About) + break + case 'updates': + setCurrentTab(AboutTabs.Updates) + break + case 'roadmap': + setCurrentTab(AboutTabs.Roadmap) + break + } + } + + const currentSection = () => { + switch (currentTab) { + case AboutTabs.About: + return + case AboutTabs.Updates: + return + case AboutTabs.Roadmap: + return + } + } + + return ( +
+ + + {t('about.segmented_control.about')} + + + {t('about.segmented_control.updates')} + + + {t('about.segmented_control.roadmap')} + + + {currentSection()} +
+ ) +} \ No newline at end of file diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx new file mode 100644 index 00000000..a4b6c9e9 --- /dev/null +++ b/app/[locale]/about/page.tsx @@ -0,0 +1,28 @@ +import { Metadata } from 'next' +import { getTranslations } from 'next-intl/server' +import AboutPageClient from './AboutPageClient' + +export async function generateMetadata({ + params: { locale } +}: { + params: { locale: string } +}): Promise { + const t = await getTranslations({ locale, namespace: 'common' }) + + return { + title: t('page.titles.about'), + description: t('page.descriptions.about') + } +} + +export default async function AboutPage({ + params: { locale } +}: { + params: { locale: string } +}) { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/app/[locale]/roadmap/RoadmapPageClient.tsx b/app/[locale]/roadmap/RoadmapPageClient.tsx new file mode 100644 index 00000000..50dd3f57 --- /dev/null +++ b/app/[locale]/roadmap/RoadmapPageClient.tsx @@ -0,0 +1,66 @@ +'use client' + +import React, { useState } from 'react' +import { useTranslations } from 'next-intl' +import { useRouter } from '~/i18n/navigation' +import { AboutTabs } from '~/utils/enums' + +import AboutPage from '~/components/about/AboutPage' +import UpdatesPage from '~/components/about/UpdatesPage' +import RoadmapPage from '~/components/about/RoadmapPage' +import SegmentedControl from '~/components/common/SegmentedControl' +import Segment from '~/components/common/Segment' + +export default function RoadmapPageClient() { + const t = useTranslations('common') + const router = useRouter() + const [currentTab] = useState(AboutTabs.Roadmap) + + function handleTabClicked(event: React.ChangeEvent) { + const value = event.target.value + router.push(`/${value}`) + } + + const currentSection = () => { + switch (currentTab) { + case AboutTabs.About: + return + case AboutTabs.Updates: + return + case AboutTabs.Roadmap: + return + } + } + + return ( +
+ + + {t('about.segmented_control.about')} + + + {t('about.segmented_control.updates')} + + + {t('about.segmented_control.roadmap')} + + + {currentSection()} +
+ ) +} \ No newline at end of file diff --git a/app/[locale]/roadmap/page.tsx b/app/[locale]/roadmap/page.tsx new file mode 100644 index 00000000..76fbb125 --- /dev/null +++ b/app/[locale]/roadmap/page.tsx @@ -0,0 +1,28 @@ +import { Metadata } from 'next' +import { getTranslations } from 'next-intl/server' +import RoadmapPageClient from './RoadmapPageClient' + +export async function generateMetadata({ + params: { locale } +}: { + params: { locale: string } +}): Promise { + const t = await getTranslations({ locale, namespace: 'common' }) + + return { + title: t('page.titles.roadmap'), + description: t('page.descriptions.roadmap') + } +} + +export default async function RoadmapPage({ + params: { locale } +}: { + params: { locale: string } +}) { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/app/[locale]/updates/UpdatesPageClient.tsx b/app/[locale]/updates/UpdatesPageClient.tsx new file mode 100644 index 00000000..88faf9e9 --- /dev/null +++ b/app/[locale]/updates/UpdatesPageClient.tsx @@ -0,0 +1,66 @@ +'use client' + +import React, { useState } from 'react' +import { useTranslations } from 'next-intl' +import { useRouter } from '~/i18n/navigation' +import { AboutTabs } from '~/utils/enums' + +import AboutPage from '~/components/about/AboutPage' +import UpdatesPage from '~/components/about/UpdatesPage' +import RoadmapPage from '~/components/about/RoadmapPage' +import SegmentedControl from '~/components/common/SegmentedControl' +import Segment from '~/components/common/Segment' + +export default function UpdatesPageClient() { + const t = useTranslations('common') + const router = useRouter() + const [currentTab] = useState(AboutTabs.Updates) + + function handleTabClicked(event: React.ChangeEvent) { + const value = event.target.value + router.push(`/${value}`) + } + + const currentSection = () => { + switch (currentTab) { + case AboutTabs.About: + return + case AboutTabs.Updates: + return + case AboutTabs.Roadmap: + return + } + } + + return ( +
+ + + {t('about.segmented_control.about')} + + + {t('about.segmented_control.updates')} + + + {t('about.segmented_control.roadmap')} + + + {currentSection()} +
+ ) +} \ No newline at end of file diff --git a/app/[locale]/updates/page.tsx b/app/[locale]/updates/page.tsx new file mode 100644 index 00000000..787607ab --- /dev/null +++ b/app/[locale]/updates/page.tsx @@ -0,0 +1,28 @@ +import { Metadata } from 'next' +import { getTranslations } from 'next-intl/server' +import UpdatesPageClient from './UpdatesPageClient' + +export async function generateMetadata({ + params: { locale } +}: { + params: { locale: string } +}): Promise { + const t = await getTranslations({ locale, namespace: 'common' }) + + return { + title: t('page.titles.updates'), + description: t('page.descriptions.updates') + } +} + +export default async function UpdatesPage({ + params: { locale } +}: { + params: { locale: string } +}) { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/app/api/characters/[id]/route.ts b/app/api/characters/[id]/route.ts new file mode 100644 index 00000000..6fab5b30 --- /dev/null +++ b/app/api/characters/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { fetchFromApi } from '~/app/lib/api-utils'; + +// GET handler for fetching a single character +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + if (!id) { + return NextResponse.json( + { error: 'Character ID is required' }, + { status: 400 } + ); + } + + const data = await fetchFromApi(`/characters/${id}`); + + return NextResponse.json(data); + } catch (error: any) { + console.error(`Error fetching character ${params.id}`, error); + return NextResponse.json( + { error: error.message || 'Failed to fetch character' }, + { status: error.response?.status || 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/raids/[id]/route.ts b/app/api/raids/[id]/route.ts new file mode 100644 index 00000000..f438e125 --- /dev/null +++ b/app/api/raids/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { fetchFromApi } from '~/app/lib/api-utils'; + +// GET handler for fetching a single raid +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + if (!id) { + return NextResponse.json( + { error: 'Raid ID is required' }, + { status: 400 } + ); + } + + const data = await fetchFromApi(`/raids/${id}`); + + return NextResponse.json(data); + } catch (error: any) { + console.error(`Error fetching raid ${params.id}`, error); + return NextResponse.json( + { error: error.message || 'Failed to fetch raid' }, + { status: error.response?.status || 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/summons/[id]/route.ts b/app/api/summons/[id]/route.ts new file mode 100644 index 00000000..38e8c351 --- /dev/null +++ b/app/api/summons/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { fetchFromApi } from '~/app/lib/api-utils'; + +// GET handler for fetching a single summon +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + if (!id) { + return NextResponse.json( + { error: 'Summon ID is required' }, + { status: 400 } + ); + } + + const data = await fetchFromApi(`/summons/${id}`); + + return NextResponse.json(data); + } catch (error: any) { + console.error(`Error fetching summon ${params.id}`, error); + return NextResponse.json( + { error: error.message || 'Failed to fetch summon' }, + { status: error.response?.status || 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/weapons/[id]/route.ts b/app/api/weapons/[id]/route.ts new file mode 100644 index 00000000..1c9c8d72 --- /dev/null +++ b/app/api/weapons/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { fetchFromApi } from '~/app/lib/api-utils'; + +// GET handler for fetching a single weapon +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + if (!id) { + return NextResponse.json( + { error: 'Weapon ID is required' }, + { status: 400 } + ); + } + + const data = await fetchFromApi(`/weapons/${id}`); + + return NextResponse.json(data); + } catch (error: any) { + console.error(`Error fetching weapon ${params.id}`, error); + return NextResponse.json( + { error: error.message || 'Failed to fetch weapon' }, + { status: error.response?.status || 500 } + ); + } +} \ No newline at end of file diff --git a/components/about/AboutPage/index.tsx b/components/about/AboutPage/index.tsx index 12941658..e9a3b4b6 100644 --- a/components/about/AboutPage/index.tsx +++ b/components/about/AboutPage/index.tsx @@ -22,19 +22,17 @@ const AboutPage: React.FC = (props: Props) => {

{common('about.segmented_control.about')}

- {/* TODO: Refactor to about.rich() */} - {about("about.subtitle")} - {/* - Granblue.team is a tool to save and share team compositions for{' '} - - Granblue Fantasy - - , a social RPG from Cygames. - */} + {about.rich('about.subtitle', { + gameLink: (chunks) => ( + + {chunks} + + ) + })}

{about('about.explanation.0')}

{about('about.explanation.1')}

@@ -56,59 +54,52 @@ const AboutPage: React.FC = (props: Props) => {

{about('about.credits.title')}

- {/* TODO: Refactor to about.rich() */} - {about('about.credits.maintainer')} - {/* - Granblue.team was built and is maintained by{' '} - - @jedmund - - . - */} + {about.rich('about.credits.maintainer', { + link: (chunks) => ( + + {chunks} + + ) + })}

- {/* TODO: Refactor to about.rich() */} - {about('about.credits.assistance')} - {/* - Many thanks to{' '} - - @lalalalinna - {' '} - and{' '} - - @tarngerine - - , who both provided a lot of help and advice as I was ramping up. - */} + {about.rich('about.credits.assistance', { + link1: (chunks) => ( + + {chunks} + + ), + link2: (chunks) => ( + + {chunks} + + ) + })}

- {/* TODO: Refactor to about.rich() */} - {about('about.credits.support')} - {/* - Many thanks also go to everyone in{' '} - - Fireplace - {' '} - and the granblue-tools Discord for all of their help with with bug - testing, feature requests, and moral support. (P.S. We're - recruiting!) - */} + {about.rich('about.credits.support', { + link: (chunks) => ( + + {chunks} + + ) + })}

@@ -134,19 +125,17 @@ const AboutPage: React.FC = (props: Props) => {

{about('about.license.title')}

- {/* TODO: Refactor to about.rich() */} - {about('about.license.license')} - {/* - This app is licensed under{' '} - - GNU AGPLv3 - - . - */} + {about.rich('about.license.license', { + link: (chunks) => ( + + {chunks} + + ) + })}

{about('about.license.explanation')}

diff --git a/components/about/ChangelogUnit/index.tsx b/components/about/ChangelogUnit/index.tsx index 6451db43..393fec95 100644 --- a/components/about/ChangelogUnit/index.tsx +++ b/components/about/ChangelogUnit/index.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useEffect, useState } from 'react' import { getCookie } from 'cookies-next' -import api from '~utils/api' import styles from './index.module.scss' @@ -28,49 +27,41 @@ const ChangelogUnit = ({ id, type, image }: Props) => { // Hooks useEffect(() => { - fetch() - }, []) + fetchItem() + }, [id, type]) - async function fetch() { - switch (type) { - case 'character': - const character = await fetchCharacter() - setItem(character.data) - break - - case 'weapon': - const weapon = await fetchWeapon() - setItem(weapon.data) - break - - case 'summon': - const summon = await fetchSummon() - setItem(summon.data) - break - - case 'raid': - const raid = await fetchRaid() - setItem(raid.data) - break + async function fetchItem() { + try { + let endpoint = '' + + switch (type) { + case 'character': + endpoint = `/api/characters/${id}` + break + case 'weapon': + endpoint = `/api/weapons/${id}` + break + case 'summon': + endpoint = `/api/summons/${id}` + break + case 'raid': + endpoint = `/api/raids/${id}` + break + default: + return + } + + const response = await fetch(endpoint) + + if (response.ok) { + const data = await response.json() + setItem(data) + } + } catch (error) { + console.error(`Error fetching ${type} ${id}:`, error) } } - async function fetchCharacter() { - return api.endpoints.characters.getOne({ id: id }) - } - - async function fetchWeapon() { - return api.endpoints.weapons.getOne({ id: id }) - } - - async function fetchSummon() { - return api.endpoints.summons.getOne({ id: id }) - } - - async function fetchRaid() { - return api.endpoints.raids.getOne({ id: id }) - } - const imageUrl = () => { let src = '' diff --git a/components/about/RoadmapPage/index.tsx b/components/about/RoadmapPage/index.tsx index 0d5646a1..367b6f32 100644 --- a/components/about/RoadmapPage/index.tsx +++ b/components/about/RoadmapPage/index.tsx @@ -1,3 +1,5 @@ +'use client' + import React from 'react' import { useTranslations } from 'next-intl' @@ -11,8 +13,8 @@ import styles from './index.module.scss' const ROADMAP_ITEMS = 6 const RoadmapPage = () => { - const { t: common } = useTranslation('common') - const { t: about } = useTranslation('about') + const common = useTranslations('common') + const about = useTranslations('about') const classes = classNames(styles.roadmap, 'PageContent') diff --git a/components/about/UpdatesPage/index.tsx b/components/about/UpdatesPage/index.tsx index db927114..60f38e24 100644 --- a/components/about/UpdatesPage/index.tsx +++ b/components/about/UpdatesPage/index.tsx @@ -14,7 +14,8 @@ const UpdatesPage = () => { const classes = classNames(styles.updates, 'PageContent') - const [activeYear, setActiveYear] = useState(new Date().getFullYear()) + // Default to most recent year with content (2024) + const [activeYear, setActiveYear] = useState(2024) const getYearButtonClass = (year: number) => classNames({ [styles.yearButton]: true, diff --git a/public/locales/en/about.json b/public/locales/en/about.json index ecd23959..31b174ff 100644 --- a/public/locales/en/about.json +++ b/public/locales/en/about.json @@ -5,7 +5,7 @@ } }, "about": { - "subtitle": "Granblue.team is a tool to save and share team compositions for <2>Granblue Fantasy, a social RPG from Cygames.", + "subtitle": "Granblue.team is a tool to save and share team compositions for Granblue Fantasy, a social RPG from Cygames.", "explanation": [ "To get started, all you have to do is add an item to a team and a URL will be created for you to share wherever you like, no account needed.", "If you do make an account, you can save any teams you find for future reference and keep all of your teams together in one place." @@ -17,9 +17,9 @@ }, "credits": { "title": "Credits", - "maintainer": "Granblue.team was built and is maintained by <2>@jedmund.", - "assistance": "Many thanks to <2>@lalalalinna and <6>@tarngerine, who both provided a lot of help and advice as I was ramping up.", - "support": "Many thanks also go to everyone in <2>Fireplace and the granblue-tools Discord for all of their help with with bug testing, feature requests, and moral support. (P.S. We're recruiting!)" + "maintainer": "Granblue.team was built and is maintained by @jedmund.", + "assistance": "Many thanks to @lalalalinna and @tarngerine, who both provided a lot of help and advice as I was ramping up.", + "support": "Many thanks also go to everyone in Fireplace and the granblue-tools Discord for all of their help with with bug testing, feature requests, and moral support. (P.S. We're recruiting!)" }, "contributing": { "title": "Contributing", @@ -27,7 +27,7 @@ }, "license": { "title": "License", - "license": "This app is licensed under <2>GNU AGPLv3.", + "license": "This app is licensed under GNU AGPLv3.", "explanation": "Plainly, that means you can download the source, modify it, and redistribute it as long as you attribute this project, use the same license, and keep your derivative work open source as well." }, "copyright": { diff --git a/public/locales/en/updates.json b/public/locales/en/updates.json index b873b65f..00344f8a 100644 --- a/public/locales/en/updates.json +++ b/public/locales/en/updates.json @@ -1,4 +1,5 @@ { + "noUpdates": "No updates available for this year", "labels": { "characters": "New characters", "weapons": "New weapons", @@ -22,7 +23,7 @@ "updates": "Other updates" }, "events": { - "date": "{{month}}/{{year}}", + "date": "{month}/{year}", "legfest": "Legend Festival", "flash": "Flash Gala", "content": "Content Update", diff --git a/public/locales/ja/about.json b/public/locales/ja/about.json index 61f62d3e..bce19aac 100644 --- a/public/locales/ja/about.json +++ b/public/locales/ja/about.json @@ -5,7 +5,7 @@ } }, "about": { - "subtitle": "Granblue.teamは<2>グランブルーファンタジーの編成を作成・保存・共有するサイトです。", + "subtitle": "Granblue.teamはグランブルーファンタジーの編成を作成・保存・共有するサイトです。", "explanation": [ "新しい編成にキャラクター・武器・召喚石を追加するだけで、好きな場所で共有できるURLが作成されます—アカウントなしで!", "しかしアカウントを作れば、見つけた編成を保存して今後の参考の参考にすることができますし、すべての編成を1つの場所にまとめておくことができます。" @@ -17,9 +17,9 @@ }, "credits": { "title": "謝意", - "maintainer": "Granblue.teamの創造者・維持者は<2>@jedmund.", - "assistance": "<2>@lalalalinnaさんと<6>@tarngerineさんがプロジェクト開始の時にたくさん助かりました。", - "support": "<2>Fireplace団(アナザーver)とgranblue-toolsのDiscordの皆さんの協力も感謝しています。(募集中です!)" + "maintainer": "Granblue.teamの創造者・維持者は@jedmund.", + "assistance": "@lalalalinnaさんと@tarngerineさんがプロジェクト開始の時にたくさん助かりました。", + "support": "Fireplace団(アナザーver)とgranblue-toolsのDiscordの皆さんの協力も感謝しています。(募集中です!)" }, "contributing": { "title": "協力", @@ -27,7 +27,7 @@ }, "license": { "title": "ライセンス", - "license": "このサイトは<2>GNU AGPLv3のライセンスで提供されています.", + "license": "このサイトはGNU AGPLv3のライセンスで提供されています.", "explanation": "派生のプロジェクトを作成したら、このプロジェクトをちゃんとリンクしてリンクし・派生のプロジェクトもオープンソースで同じライセンスで提供されたら、自由にソースコードをダウンロド・改変・再配布を許可されています。" }, "copyright": { diff --git a/public/locales/ja/updates.json b/public/locales/ja/updates.json index d6c3b81b..050f2506 100644 --- a/public/locales/ja/updates.json +++ b/public/locales/ja/updates.json @@ -1,4 +1,5 @@ { + "noUpdates": "この年のアップデートはありません", "labels": { "characters": "新キャラクター", "weapons": "新武器", @@ -22,7 +23,7 @@ "updates": "その他の更新" }, "events": { - "date": "{{year}}年{{month}}月", + "date": "{year}年{month}月", "legfest": "レジェンドフェス", "flash": "グランデフェス", "content": "アップデート",