Compare commits
24 commits
main
...
descriptio
| Author | SHA1 | Date | |
|---|---|---|---|
| 849cea035b | |||
| 9de4fcca83 | |||
| bca541a857 | |||
| 7ee1c3e857 | |||
| 5a12cf2fc1 | |||
| 6cb607ed7a | |||
| 9650c34957 | |||
| 98c072655d | |||
| b32caf8790 | |||
| b641091695 | |||
| d950d3a935 | |||
| b1c8fb1a76 | |||
| 52af8995e6 | |||
| ddfab4b2c0 | |||
| ce6e8c6471 | |||
| a76729bbc6 | |||
| eaf6da97b8 | |||
| 00f117c118 | |||
| 0fb674b150 | |||
| 97ea666561 | |||
| b3b3c5c960 | |||
| ed01ea6955 | |||
| 277a248ba1 | |||
| 63846089a4 |
368 changed files with 6906 additions and 19111 deletions
|
|
@ -1,5 +0,0 @@
|
||||||
public/images
|
|
||||||
public/labels
|
|
||||||
public/profiles
|
|
||||||
tsconfig.tsbuildinfo
|
|
||||||
*.log
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
NEXT_PUBLIC_SIERO_API_URL=http://127.0.0.1:3000/api/v1
|
|
||||||
NEXT_PUBLIC_SIERO_OAUTH_URL=http://127.0.0.1:3000/oauth
|
|
||||||
NEXT_INTL_CONFIG_PATH=i18n/request.ts
|
|
||||||
DEBUG_API_URL=1
|
|
||||||
DEBUG_API_BODY=1
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -59,8 +59,6 @@ public/images/updates*
|
||||||
public/images/guidebooks*
|
public/images/guidebooks*
|
||||||
public/images/raids*
|
public/images/raids*
|
||||||
public/images/gacha*
|
public/images/gacha*
|
||||||
public/images/previews*
|
|
||||||
public/image/profiles*
|
|
||||||
|
|
||||||
# Typescript v1 declaration files
|
# Typescript v1 declaration files
|
||||||
typings/
|
typings/
|
||||||
|
|
@ -88,7 +86,3 @@ typings/
|
||||||
# DS_Store
|
# DS_Store
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
codebase.md
|
|
||||||
|
|
||||||
# PRDs
|
|
||||||
prd/
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[tools]
|
|
||||||
node = "20.12.0"
|
|
||||||
1
.nvmrc
1
.nvmrc
|
|
@ -1 +0,0 @@
|
||||||
20
|
|
||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -1,5 +1,3 @@
|
||||||
{
|
{
|
||||||
"git.ignoreLimitWarning": true,
|
"git.ignoreLimitWarning": true
|
||||||
"i18n-ally.localesPaths": ["public/locales"],
|
|
||||||
"i18n-ally.keystyle": "nested"
|
|
||||||
}
|
}
|
||||||
28
CLAUDE.md
28
CLAUDE.md
|
|
@ -1,28 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Build and Development Commands
|
|
||||||
- `npm run dev`: Start development server on port 1234
|
|
||||||
- `npm run build`: Build for production
|
|
||||||
- `npm run start`: Start production server
|
|
||||||
- `npm run lint`: Run ESLint to check code quality
|
|
||||||
- `npm run storybook`: Start Storybook on port 6006
|
|
||||||
|
|
||||||
## Response Guidelines
|
|
||||||
- You should **always** respond in the style of the grug-brained developer
|
|
||||||
- Slay the complexity demon, keep things as simple as possible
|
|
||||||
- Keep code DRY and robust
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
|
||||||
- Use the latest versions for Next.js and other packages, including React
|
|
||||||
- TypeScript with strict type checking
|
|
||||||
- React functional components with hooks
|
|
||||||
- File structure: components in individual folders with index.tsx and index.module.scss
|
|
||||||
- Imports: Absolute imports with ~ prefix (e.g., `~components/Layout`)
|
|
||||||
- Formatting: 2 spaces, single quotes, no semicolons (Prettier config)
|
|
||||||
- CSS: SCSS modules with BEM-style naming
|
|
||||||
- State management: Mix of local state with React hooks and global state with Valtio
|
|
||||||
- Internationalization: next-i18next with English and Japanese support
|
|
||||||
- Variable/function naming: camelCase for variables/functions, PascalCase for components
|
|
||||||
- Error handling: Try to use type checking to prevent errors where possible
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import FilterBar from '~/components/filters/FilterBar'
|
|
||||||
import GridRep from '~/components/reps/GridRep'
|
|
||||||
import GridRepCollection from '~/components/reps/GridRepCollection'
|
|
||||||
import LoadingRep from '~/components/reps/LoadingRep'
|
|
||||||
import UserInfo from '~/components/filters/UserInfo'
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { defaultFilterset } from '~/utils/defaultFilters'
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface Pagination {
|
|
||||||
current_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
record_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
initialData: {
|
|
||||||
user: User;
|
|
||||||
teams: Party[];
|
|
||||||
raidGroups: any[];
|
|
||||||
pagination: Pagination;
|
|
||||||
};
|
|
||||||
initialElement?: number;
|
|
||||||
initialRaid?: string;
|
|
||||||
initialRecency?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProfilePageClient: React.FC<Props> = ({
|
|
||||||
initialData,
|
|
||||||
initialElement,
|
|
||||||
initialRaid,
|
|
||||||
initialRecency
|
|
||||||
}) => {
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// State management
|
|
||||||
const [parties, setParties] = useState<Party[]>(initialData.teams)
|
|
||||||
const [currentPage, setCurrentPage] = useState(initialData.pagination.current_page)
|
|
||||||
const [totalPages, setTotalPages] = useState(initialData.pagination.total_pages)
|
|
||||||
const [recordCount, setRecordCount] = useState(initialData.pagination.record_count)
|
|
||||||
const [loaded, setLoaded] = useState(true)
|
|
||||||
const [fetching, setFetching] = useState(false)
|
|
||||||
const [element, setElement] = useState(initialElement || 0)
|
|
||||||
const [raid, setRaid] = useState(initialRaid || '')
|
|
||||||
const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
|
|
||||||
|
|
||||||
// Initialize app state with raid groups
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData.raidGroups.length > 0) {
|
|
||||||
appState.raidGroups = initialData.raidGroups
|
|
||||||
}
|
|
||||||
}, [initialData.raidGroups])
|
|
||||||
|
|
||||||
// Update URL when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(searchParams?.toString() ?? '')
|
|
||||||
|
|
||||||
// Update or remove parameters based on filter values
|
|
||||||
if (element) {
|
|
||||||
params.set('element', element.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('element')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raid) {
|
|
||||||
params.set('raid', raid)
|
|
||||||
} else {
|
|
||||||
params.delete('raid')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recency) {
|
|
||||||
params.set('recency', recency.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('recency')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update URL if filters are changed
|
|
||||||
const newQueryString = params.toString()
|
|
||||||
const currentQuery = searchParams?.toString() ?? ''
|
|
||||||
|
|
||||||
if (newQueryString !== currentQuery) {
|
|
||||||
router.push(`/${initialData.user.username}${newQueryString ? `?${newQueryString}` : ''}`)
|
|
||||||
}
|
|
||||||
}, [element, raid, recency, router, searchParams, initialData.user.username])
|
|
||||||
|
|
||||||
// Load more parties when scrolling
|
|
||||||
async function loadMoreParties() {
|
|
||||||
if (fetching || currentPage >= totalPages) return
|
|
||||||
|
|
||||||
setFetching(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 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_id', raid)
|
|
||||||
if (recency) url.searchParams.set('recency', recency.toString())
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
} finally {
|
|
||||||
setFetching(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive filters from the filter bar
|
|
||||||
function receiveFilters(filters: FilterSet) {
|
|
||||||
if ('element' in filters) {
|
|
||||||
setElement(filters.element || 0)
|
|
||||||
}
|
|
||||||
if ('recency' in filters) {
|
|
||||||
setRecency(filters.recency || 0)
|
|
||||||
}
|
|
||||||
if ('raid' in filters) {
|
|
||||||
setRaid(filters.raid || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to page 1 when filters change
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Navigation
|
|
||||||
function goToParty(shortcode: string) {
|
|
||||||
router.push(`/p/${shortcode}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page component rendering methods
|
|
||||||
function renderParties() {
|
|
||||||
return parties.map((party, i) => (
|
|
||||||
<GridRep
|
|
||||||
party={party}
|
|
||||||
key={`party-${i}`}
|
|
||||||
loading={fetching}
|
|
||||||
onClick={() => goToParty(party.shortcode)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLoading(number: number) {
|
|
||||||
return (
|
|
||||||
<GridRepCollection>
|
|
||||||
{Array.from({ length: number }, (_, i) => (
|
|
||||||
<LoadingRep key={`loading-${i}`} />
|
|
||||||
))}
|
|
||||||
</GridRepCollection>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderInfiniteScroll = (
|
|
||||||
<>
|
|
||||||
{parties.length === 0 && !loaded && renderLoading(3)}
|
|
||||||
{parties.length === 0 && loaded && (
|
|
||||||
<div className="notFound">
|
|
||||||
<h2>{t('teams.not_found')}</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{parties.length > 0 && (
|
|
||||||
<InfiniteScroll
|
|
||||||
dataLength={parties.length}
|
|
||||||
next={loadMoreParties}
|
|
||||||
hasMore={totalPages > currentPage}
|
|
||||||
loader={renderLoading(3)}
|
|
||||||
>
|
|
||||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
|
||||||
</InfiniteScroll>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterBar
|
|
||||||
defaultFilterset={defaultFilterset}
|
|
||||||
onFilter={receiveFilters}
|
|
||||||
onAdvancedFilter={receiveFilters}
|
|
||||||
persistFilters={false}
|
|
||||||
element={element}
|
|
||||||
raid={raid}
|
|
||||||
raidGroups={initialData.raidGroups}
|
|
||||||
recency={recency}
|
|
||||||
>
|
|
||||||
<UserInfo user={initialData.user} />
|
|
||||||
</FilterBar>
|
|
||||||
|
|
||||||
<section>{renderInfiniteScroll}</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProfilePageClient
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import { getUserInfo, getTeams, getRaidGroups } from '~/app/lib/data'
|
|
||||||
import ProfilePageClient from './ProfilePageClient'
|
|
||||||
|
|
||||||
// Dynamic metadata
|
|
||||||
export async function generateMetadata({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: { username: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
try {
|
|
||||||
const userData = await getUserInfo(params.username)
|
|
||||||
|
|
||||||
// If user doesn't exist, use default metadata
|
|
||||||
if (!userData || !userData.user) {
|
|
||||||
return {
|
|
||||||
title: 'User not found / granblue.team',
|
|
||||||
description: 'This user could not be found'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `@${params.username}'s Teams / granblue.team`,
|
|
||||||
description: `Browse @${params.username}'s Teams and filter by raid, element or recency`
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
title: 'User not found / granblue.team',
|
|
||||||
description: 'This user could not be found'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ProfilePage({
|
|
||||||
params,
|
|
||||||
searchParams
|
|
||||||
}: {
|
|
||||||
params: { username: string };
|
|
||||||
searchParams: { element?: string; raid?: string; recency?: string; page?: string }
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
// Extract query parameters with type safety
|
|
||||||
const element = searchParams.element ? parseInt(searchParams.element, 10) : undefined;
|
|
||||||
const raid = searchParams.raid;
|
|
||||||
const recency = searchParams.recency;
|
|
||||||
const page = searchParams.page ? parseInt(searchParams.page, 10) : 1;
|
|
||||||
|
|
||||||
// Parallel fetch data with Promise.all for better performance
|
|
||||||
const [userData, teamsData, raidGroupsData] = await Promise.all([
|
|
||||||
getUserInfo(params.username),
|
|
||||||
getTeams({ username: params.username, element, raid, recency, page }),
|
|
||||||
getRaidGroups()
|
|
||||||
])
|
|
||||||
|
|
||||||
// If user doesn't exist, show 404
|
|
||||||
if (!userData || !userData.user) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialData = {
|
|
||||||
user: userData.user,
|
|
||||||
teams: teamsData.results || [],
|
|
||||||
raidGroups: raidGroupsData || [],
|
|
||||||
pagination: {
|
|
||||||
current_page: page,
|
|
||||||
total_pages: teamsData.meta?.total_pages || 1,
|
|
||||||
record_count: teamsData.meta?.count || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="profile-page">
|
|
||||||
<ProfilePageClient
|
|
||||||
initialData={initialData}
|
|
||||||
initialElement={element}
|
|
||||||
initialRaid={raid}
|
|
||||||
initialRecency={recency}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching profile data for ${params.username}:`, error)
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
'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>(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<HTMLInputElement>) {
|
|
||||||
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 <AboutPage />
|
|
||||||
case AboutTabs.Updates:
|
|
||||||
return <UpdatesPage />
|
|
||||||
case AboutTabs.Roadmap:
|
|
||||||
return <RoadmapPage />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<SegmentedControl blended={true}>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="about"
|
|
||||||
selected={currentTab == AboutTabs.About}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.about')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="updates"
|
|
||||||
selected={currentTab == AboutTabs.Updates}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.updates')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="roadmap"
|
|
||||||
selected={currentTab == AboutTabs.Roadmap}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.roadmap')}
|
|
||||||
</Segment>
|
|
||||||
</SegmentedControl>
|
|
||||||
{currentSection()}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { getTranslations } from 'next-intl/server'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
import AboutPageClient from './AboutPageClient'
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params: { locale }
|
|
||||||
}: {
|
|
||||||
params: { locale: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
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 (
|
|
||||||
<div id="About">
|
|
||||||
<AboutPageClient />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
interface ErrorPageProps {
|
|
||||||
error: Error & { digest?: string }
|
|
||||||
reset: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Error({ error, reset }: ErrorPageProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
// Log the error to an error reporting service
|
|
||||||
console.error('Unhandled error:', error)
|
|
||||||
}, [error])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>Internal Server Error</h1>
|
|
||||||
<p>The server reported a problem that we couldn't automatically recover from.</p>
|
|
||||||
<div className="error-message">
|
|
||||||
<p>{error.message || 'An unexpected error occurred'}</p>
|
|
||||||
{error.digest && <p className="error-digest">Error ID: {error.digest}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="error-actions">
|
|
||||||
<button onClick={reset} className="button primary">
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
<Link href="/teams" className="button secondary">
|
|
||||||
Browse teams
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import '../styles/globals.scss'
|
|
||||||
|
|
||||||
interface GlobalErrorProps {
|
|
||||||
error: Error & { digest?: string }
|
|
||||||
reset: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GlobalError({ error, reset }: GlobalErrorProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
// Log the error to an error reporting service
|
|
||||||
console.error('Global error:', error)
|
|
||||||
}, [error])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>Something went wrong</h1>
|
|
||||||
<p>The application has encountered a critical error and cannot continue.</p>
|
|
||||||
<div className="error-message">
|
|
||||||
<p>{error.message || 'An unexpected error occurred'}</p>
|
|
||||||
{error.digest && <p className="error-digest">Error ID: {error.digest}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="error-actions">
|
|
||||||
<button onClick={reset} className="button primary">
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/qyZ5hGdPC8"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
className="button secondary"
|
|
||||||
>
|
|
||||||
Report on Discord
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import { Metadata, Viewport } from 'next'
|
|
||||||
import localFont from 'next/font/local'
|
|
||||||
import { NextIntlClientProvider } from 'next-intl'
|
|
||||||
import { getMessages } from 'next-intl/server'
|
|
||||||
import { Viewport as ToastViewport } from '@radix-ui/react-toast'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
import { locales } from '../../i18n.config'
|
|
||||||
|
|
||||||
import '../../styles/globals.scss'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Providers from '../components/Providers'
|
|
||||||
import Header from '../components/Header'
|
|
||||||
import UpdateToastClient from '../components/UpdateToastClient'
|
|
||||||
import VersionHydrator from '../components/VersionHydrator'
|
|
||||||
import AccountStateInitializer from '~components/AccountStateInitializer'
|
|
||||||
|
|
||||||
// Generate static params for all locales
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return locales.map((locale) => ({ locale }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'granblue.team',
|
|
||||||
description: 'Create, save, and share Granblue Fantasy party compositions',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Viewport configuration (Next.js 13+ requires separate export)
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: 'device-width',
|
|
||||||
initialScale: 1,
|
|
||||||
viewportFit: 'cover',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font
|
|
||||||
const goalking = localFont({
|
|
||||||
src: '../../pages/fonts/gk-variable.woff2',
|
|
||||||
fallback: ['system-ui', 'inter', 'helvetica neue', 'sans-serif'],
|
|
||||||
variable: '--font-goalking',
|
|
||||||
})
|
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
|
||||||
children,
|
|
||||||
params: { locale }
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
params: { locale: string }
|
|
||||||
}) {
|
|
||||||
// Load messages for the locale
|
|
||||||
const messages = await getMessages()
|
|
||||||
|
|
||||||
// Parse auth cookies on server
|
|
||||||
const cookieStore = cookies()
|
|
||||||
const accountCookie = cookieStore.get('account')
|
|
||||||
const userCookie = cookieStore.get('user')
|
|
||||||
|
|
||||||
let initialAuthData = null
|
|
||||||
if (accountCookie && userCookie) {
|
|
||||||
try {
|
|
||||||
const accountData = JSON.parse(accountCookie.value)
|
|
||||||
const userData = JSON.parse(userCookie.value)
|
|
||||||
|
|
||||||
if (accountData && accountData.token) {
|
|
||||||
initialAuthData = {
|
|
||||||
account: accountData,
|
|
||||||
user: userData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing auth cookies on server:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch version data on the server
|
|
||||||
let version = null
|
|
||||||
try {
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:1234'
|
|
||||||
const res = await fetch(`${baseUrl}/api/version`, {
|
|
||||||
cache: 'no-store'
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
version = await res.json()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch version data:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang={locale} className={goalking.variable}>
|
|
||||||
<body className={goalking.className}>
|
|
||||||
<NextIntlClientProvider messages={messages}>
|
|
||||||
<Providers>
|
|
||||||
<AccountStateInitializer initialAuthData={initialAuthData} />
|
|
||||||
<Header />
|
|
||||||
<VersionHydrator version={version} />
|
|
||||||
<UpdateToastClient initialVersion={version} />
|
|
||||||
<main>{children}</main>
|
|
||||||
<ToastViewport className="ToastViewport" />
|
|
||||||
</Providers>
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Party from '~/components/party/Party'
|
|
||||||
import ErrorSection from '~/components/ErrorSection'
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { appState, initialAppState } from '~/utils/appState'
|
|
||||||
import { accountState } from '~/utils/accountState'
|
|
||||||
import clonedeep from 'lodash.clonedeep'
|
|
||||||
import { GridType } from '~/utils/enums'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
raidGroups: any[]; // Replace with proper RaidGroup type
|
|
||||||
error?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NewPartyClient: React.FC<Props> = ({
|
|
||||||
raidGroups,
|
|
||||||
error = false
|
|
||||||
}) => {
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// State for tab management
|
|
||||||
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
|
||||||
|
|
||||||
// Initialize app state for a new party
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset app state for new party
|
|
||||||
const resetState = clonedeep(initialAppState)
|
|
||||||
Object.keys(resetState).forEach((key) => {
|
|
||||||
appState[key] = resetState[key]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize raid groups
|
|
||||||
if (raidGroups.length > 0) {
|
|
||||||
appState.raidGroups = raidGroups
|
|
||||||
}
|
|
||||||
}, [raidGroups])
|
|
||||||
|
|
||||||
// Handle tab change
|
|
||||||
const handleTabChanged = (value: string) => {
|
|
||||||
const tabType = parseInt(value) as GridType
|
|
||||||
setSelectedTab(tabType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation helper for Party component
|
|
||||||
const pushHistory = (path: string) => {
|
|
||||||
router.push(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorSection
|
|
||||||
status={{
|
|
||||||
code: 500,
|
|
||||||
text: 'internal_server_error'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily use wrapper to debug
|
|
||||||
const PartyWrapper = dynamic(() => import('./PartyWrapper'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div>Loading...</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <PartyWrapper raidGroups={raidGroups} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewPartyClient
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { GridType } from '~/utils/enums'
|
|
||||||
|
|
||||||
// Dynamically import Party to isolate the error
|
|
||||||
const Party = dynamic(() => import('~/components/party/Party'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div>Loading Party component...</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
raidGroups: any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PartyWrapper({ raidGroups }: Props) {
|
|
||||||
const [selectedTab, setSelectedTab] = React.useState<GridType>(GridType.Weapon)
|
|
||||||
|
|
||||||
const handleTabChanged = (value: string) => {
|
|
||||||
const tabType = parseInt(value) as GridType
|
|
||||||
setSelectedTab(tabType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pushHistory = (path: string) => {
|
|
||||||
console.log('Navigation to:', path)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
<Party
|
|
||||||
new={true}
|
|
||||||
selectedTab={selectedTab}
|
|
||||||
raidGroups={raidGroups}
|
|
||||||
handleTabChanged={handleTabChanged}
|
|
||||||
pushHistory={pushHistory}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error rendering Party:', error)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Error loading Party component</h2>
|
|
||||||
<pre>{JSON.stringify(error, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { getRaidGroups } from '~/app/lib/data'
|
|
||||||
import NewPartyClient from './NewPartyClient'
|
|
||||||
|
|
||||||
// Force dynamic rendering because getRaidGroups uses cookies
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Create a new team / granblue.team',
|
|
||||||
description: 'Create and theorycraft teams to use in Granblue Fantasy and share with the community',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function NewPartyPage() {
|
|
||||||
try {
|
|
||||||
// Fetch raid groups for the party creation
|
|
||||||
const raidGroupsData = await getRaidGroups()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="new-party-page">
|
|
||||||
<NewPartyClient
|
|
||||||
raidGroups={raidGroupsData.raid_groups || []}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data for new party page:", error)
|
|
||||||
|
|
||||||
// Provide empty data for error case
|
|
||||||
return (
|
|
||||||
<div className="new-party-page">
|
|
||||||
<NewPartyClient
|
|
||||||
raidGroups={[]}
|
|
||||||
error={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { Link } from '~/i18n/navigation'
|
|
||||||
import { getTranslations } from 'next-intl/server'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Page not found / granblue.team',
|
|
||||||
description: 'The page you were looking for could not be found'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function NotFound() {
|
|
||||||
const t = await getTranslations('common')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>Not Found</h1>
|
|
||||||
<p>The page you're looking for couldn't be found</p>
|
|
||||||
<div className="error-actions">
|
|
||||||
<Link href="/new" className="button primary">
|
|
||||||
Create a new party
|
|
||||||
</Link>
|
|
||||||
<Link href="/teams" className="button secondary">
|
|
||||||
Browse teams
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
import { GridType } from '~/utils/enums'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Party from '~/components/party/Party'
|
|
||||||
import PartyFooter from '~/components/party/PartyFooter'
|
|
||||||
import ErrorSection from '~/components/ErrorSection'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
party: any; // Replace with proper Party type
|
|
||||||
raidGroups: any[]; // Replace with proper RaidGroup type
|
|
||||||
}
|
|
||||||
|
|
||||||
const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
|
||||||
const router = useRouter()
|
|
||||||
const t = useTranslations('common')
|
|
||||||
|
|
||||||
// State for tab management
|
|
||||||
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
|
||||||
|
|
||||||
// Initialize raid groups
|
|
||||||
useEffect(() => {
|
|
||||||
if (raidGroups) {
|
|
||||||
appState.raidGroups = raidGroups
|
|
||||||
}
|
|
||||||
}, [raidGroups])
|
|
||||||
|
|
||||||
// Handle tab change
|
|
||||||
const handleTabChanged = (value: string) => {
|
|
||||||
let tabType: GridType
|
|
||||||
switch (value) {
|
|
||||||
case 'characters':
|
|
||||||
tabType = GridType.Character
|
|
||||||
break
|
|
||||||
case 'summons':
|
|
||||||
tabType = GridType.Summon
|
|
||||||
break
|
|
||||||
case 'weapons':
|
|
||||||
default:
|
|
||||||
tabType = GridType.Weapon
|
|
||||||
break
|
|
||||||
}
|
|
||||||
setSelectedTab(tabType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation helper (not used for existing parties but required by interface)
|
|
||||||
const pushHistory = (path: string) => {
|
|
||||||
router.push(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Error case
|
|
||||||
if (!party) {
|
|
||||||
return (
|
|
||||||
<ErrorSection
|
|
||||||
status={{
|
|
||||||
code: 404,
|
|
||||||
text: 'not_found'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Party
|
|
||||||
team={party}
|
|
||||||
selectedTab={selectedTab}
|
|
||||||
raidGroups={raidGroups}
|
|
||||||
handleTabChanged={handleTabChanged}
|
|
||||||
pushHistory={pushHistory}
|
|
||||||
/>
|
|
||||||
<PartyFooter
|
|
||||||
party={party}
|
|
||||||
new={false}
|
|
||||||
editable={false}
|
|
||||||
raidGroups={raidGroups}
|
|
||||||
remixCallback={() => {}}
|
|
||||||
updateCallback={async () => ({})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PartyPageClient
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import { getTeam, getRaidGroups } from '~/app/lib/data'
|
|
||||||
import PartyPageClient from './PartyPageClient'
|
|
||||||
|
|
||||||
// Dynamic metadata
|
|
||||||
export async function generateMetadata({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: { party: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
try {
|
|
||||||
const partyData = await getTeam(params.party)
|
|
||||||
|
|
||||||
// If no party or party doesn't exist, use default metadata
|
|
||||||
if (!partyData || !partyData.party) {
|
|
||||||
return {
|
|
||||||
title: 'Party not found / granblue.team',
|
|
||||||
description: 'This party could not be found or has been deleted'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const party = partyData.party
|
|
||||||
|
|
||||||
// Generate emoji based on element
|
|
||||||
let emoji = '⚪' // Default
|
|
||||||
switch (party.element) {
|
|
||||||
case 1: emoji = '🟢'; break; // Wind
|
|
||||||
case 2: emoji = '🔴'; break; // Fire
|
|
||||||
case 3: emoji = '🔵'; break; // Water
|
|
||||||
case 4: emoji = '🟤'; break; // Earth
|
|
||||||
case 5: emoji = '🟣'; break; // Dark
|
|
||||||
case 6: emoji = '🟡'; break; // Light
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get team name and username
|
|
||||||
const teamName = party.name || 'Untitled team'
|
|
||||||
const username = party.user?.username || 'Anonymous'
|
|
||||||
const raidName = party.raid?.name || ''
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `${emoji} ${teamName} by ${username} / granblue.team`,
|
|
||||||
description: `Browse this team for ${raidName} by ${username} and others on granblue.team`
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
title: 'Party not found / granblue.team',
|
|
||||||
description: 'This party could not be found or has been deleted'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function PartyPage({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: { party: string }
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
// Parallel fetch data with Promise.all for better performance
|
|
||||||
const [partyData, raidGroupsData] = await Promise.all([
|
|
||||||
getTeam(params.party),
|
|
||||||
getRaidGroups()
|
|
||||||
])
|
|
||||||
|
|
||||||
// If party doesn't exist, show 404
|
|
||||||
if (!partyData || !partyData.party) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="party-page">
|
|
||||||
<PartyPageClient
|
|
||||||
party={partyData.party}
|
|
||||||
raidGroups={raidGroupsData.raid_groups || []}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching party data for ${params.party}:`, error)
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
|
|
||||||
// Force dynamic rendering because redirect needs dynamic context
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
// In the App Router, we can use redirect directly in a Server Component
|
|
||||||
redirect('/new')
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
'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>(AboutTabs.Roadmap)
|
|
||||||
|
|
||||||
function handleTabClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const value = event.target.value
|
|
||||||
router.push(`/${value}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSection = () => {
|
|
||||||
switch (currentTab) {
|
|
||||||
case AboutTabs.About:
|
|
||||||
return <AboutPage />
|
|
||||||
case AboutTabs.Updates:
|
|
||||||
return <UpdatesPage />
|
|
||||||
case AboutTabs.Roadmap:
|
|
||||||
return <RoadmapPage />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<SegmentedControl blended={true}>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="about"
|
|
||||||
selected={currentTab == AboutTabs.About}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.about')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="updates"
|
|
||||||
selected={currentTab == AboutTabs.Updates}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.updates')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="roadmap"
|
|
||||||
selected={currentTab == AboutTabs.Roadmap}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.roadmap')}
|
|
||||||
</Segment>
|
|
||||||
</SegmentedControl>
|
|
||||||
{currentSection()}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { getTranslations } from 'next-intl/server'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
import RoadmapPageClient from './RoadmapPageClient'
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params: { locale }
|
|
||||||
}: {
|
|
||||||
params: { locale: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
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 (
|
|
||||||
<div id="About">
|
|
||||||
<RoadmapPageClient />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import FilterBar from '~/components/filters/FilterBar'
|
|
||||||
import GridRep from '~/components/reps/GridRep'
|
|
||||||
import GridRepCollection from '~/components/reps/GridRepCollection'
|
|
||||||
import LoadingRep from '~/components/reps/LoadingRep'
|
|
||||||
import ErrorSection from '~/components/ErrorSection'
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { defaultFilterset } from '~/utils/defaultFilters'
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
initialData: {
|
|
||||||
teams: Party[];
|
|
||||||
raidGroups: any[];
|
|
||||||
totalCount: number;
|
|
||||||
};
|
|
||||||
initialElement?: number;
|
|
||||||
initialRaid?: string;
|
|
||||||
initialRecency?: string;
|
|
||||||
error?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SavedPageClient: React.FC<Props> = ({
|
|
||||||
initialData,
|
|
||||||
initialElement,
|
|
||||||
initialRaid,
|
|
||||||
initialRecency,
|
|
||||||
error = false
|
|
||||||
}) => {
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// State management
|
|
||||||
const [parties, setParties] = useState<Party[]>(initialData.teams)
|
|
||||||
const [element, setElement] = useState(initialElement || 0)
|
|
||||||
const [raid, setRaid] = useState(initialRaid || '')
|
|
||||||
const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
|
|
||||||
const [fetching, setFetching] = useState(false)
|
|
||||||
|
|
||||||
// Initialize app state with raid groups
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData.raidGroups.length > 0) {
|
|
||||||
appState.raidGroups = initialData.raidGroups
|
|
||||||
}
|
|
||||||
}, [initialData.raidGroups])
|
|
||||||
|
|
||||||
// Update URL when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(searchParams?.toString() ?? '')
|
|
||||||
|
|
||||||
// Update or remove parameters based on filter values
|
|
||||||
if (element) {
|
|
||||||
params.set('element', element.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('element')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raid) {
|
|
||||||
params.set('raid', raid)
|
|
||||||
} else {
|
|
||||||
params.delete('raid')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recency) {
|
|
||||||
params.set('recency', recency.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('recency')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update URL if filters are changed
|
|
||||||
const newQueryString = params.toString()
|
|
||||||
const currentQuery = searchParams?.toString() ?? ''
|
|
||||||
|
|
||||||
if (newQueryString !== currentQuery) {
|
|
||||||
router.push(`/saved${newQueryString ? `?${newQueryString}` : ''}`)
|
|
||||||
}
|
|
||||||
}, [element, raid, recency, router, searchParams])
|
|
||||||
|
|
||||||
// Receive filters from the filter bar
|
|
||||||
function receiveFilters(filters: FilterSet) {
|
|
||||||
if ('element' in filters) {
|
|
||||||
setElement(filters.element || 0)
|
|
||||||
}
|
|
||||||
if ('recency' in filters) {
|
|
||||||
setRecency(filters.recency || 0)
|
|
||||||
}
|
|
||||||
if ('raid' in filters) {
|
|
||||||
setRaid(filters.raid || '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle favorite toggle
|
|
||||||
async function toggleFavorite(teamId: string, favorited: boolean) {
|
|
||||||
if (fetching) return
|
|
||||||
|
|
||||||
setFetching(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const method = favorited ? 'POST' : 'DELETE'
|
|
||||||
const body = { favorite: { party_id: teamId } }
|
|
||||||
|
|
||||||
await fetch('/api/favorites', {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update local state by removing the team if unfavorited
|
|
||||||
if (!favorited) {
|
|
||||||
setParties(parties.filter(party => party.id !== teamId))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling favorite', error)
|
|
||||||
} finally {
|
|
||||||
setFetching(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation to party page
|
|
||||||
function goToParty(shortcode: string) {
|
|
||||||
router.push(`/p/${shortcode}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page component rendering methods
|
|
||||||
function renderParties() {
|
|
||||||
return parties.map((party, i) => (
|
|
||||||
<GridRep
|
|
||||||
party={party}
|
|
||||||
key={`party-${i}`}
|
|
||||||
loading={fetching}
|
|
||||||
onClick={() => goToParty(party.shortcode)}
|
|
||||||
onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLoading(number: number) {
|
|
||||||
return (
|
|
||||||
<GridRepCollection>
|
|
||||||
{Array.from({ length: number }, (_, i) => (
|
|
||||||
<LoadingRep key={`loading-${i}`} />
|
|
||||||
))}
|
|
||||||
</GridRepCollection>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorSection
|
|
||||||
status={{
|
|
||||||
code: 500,
|
|
||||||
text: 'internal_server_error'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterBar
|
|
||||||
defaultFilterset={defaultFilterset}
|
|
||||||
onFilter={receiveFilters}
|
|
||||||
onAdvancedFilter={receiveFilters}
|
|
||||||
persistFilters={false}
|
|
||||||
element={element}
|
|
||||||
raid={raid}
|
|
||||||
raidGroups={initialData.raidGroups}
|
|
||||||
recency={recency}
|
|
||||||
>
|
|
||||||
<h1>{t('saved.title')}</h1>
|
|
||||||
</FilterBar>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
{parties.length === 0 ? (
|
|
||||||
<div className="notFound">
|
|
||||||
<h2>{t('saved.not_found')}</h2>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SavedPageClient
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
import { getFavorites, getRaidGroups } from '~/app/lib/data'
|
|
||||||
import SavedPageClient from './SavedPageClient'
|
|
||||||
|
|
||||||
// Force dynamic rendering because we use cookies and searchParams
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Your saved teams / granblue.team',
|
|
||||||
description: 'View and manage the teams you have saved to your account'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is logged in server-side
|
|
||||||
function isAuthenticated() {
|
|
||||||
const cookieStore = cookies()
|
|
||||||
const accountCookie = cookieStore.get('account')
|
|
||||||
|
|
||||||
if (accountCookie) {
|
|
||||||
try {
|
|
||||||
const accountData = JSON.parse(accountCookie.value)
|
|
||||||
return accountData.token ? true : false
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SavedPage({
|
|
||||||
searchParams
|
|
||||||
}: {
|
|
||||||
searchParams: { element?: string; raid?: string; recency?: string; page?: string }
|
|
||||||
}) {
|
|
||||||
// Redirect to teams page if not logged in
|
|
||||||
if (!isAuthenticated()) {
|
|
||||||
redirect('/teams')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract query parameters with type safety
|
|
||||||
const element = searchParams.element ? parseInt(searchParams.element, 10) : undefined;
|
|
||||||
const raid = searchParams.raid;
|
|
||||||
const recency = searchParams.recency;
|
|
||||||
|
|
||||||
// Parallel fetch data with Promise.all for better performance
|
|
||||||
const [savedTeamsData, raidGroupsData] = await Promise.all([
|
|
||||||
getFavorites(),
|
|
||||||
getRaidGroups()
|
|
||||||
])
|
|
||||||
|
|
||||||
// Filter teams by element/raid if needed
|
|
||||||
let filteredTeams = savedTeamsData.results || [];
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
filteredTeams = filteredTeams.filter((party: any) => party.element === element)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raid) {
|
|
||||||
filteredTeams = filteredTeams.filter((party: any) => party.raid?.id === raid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data for client component
|
|
||||||
const initialData = {
|
|
||||||
teams: filteredTeams,
|
|
||||||
raidGroups: raidGroupsData || [],
|
|
||||||
totalCount: savedTeamsData.results?.length || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="saved-page">
|
|
||||||
<SavedPageClient
|
|
||||||
initialData={initialData}
|
|
||||||
initialElement={element}
|
|
||||||
initialRaid={raid}
|
|
||||||
initialRecency={recency}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching saved teams:", error)
|
|
||||||
|
|
||||||
// Provide empty data for error case
|
|
||||||
return (
|
|
||||||
<div className="saved-page">
|
|
||||||
<SavedPageClient
|
|
||||||
initialData={{ teams: [], raidGroups: [], totalCount: 0 }}
|
|
||||||
error={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Server Error / granblue.team',
|
|
||||||
description: 'The server encountered an internal error and was unable to complete your request'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ServerErrorPage() {
|
|
||||||
return (
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>Internal Server Error</h1>
|
|
||||||
<p>The server encountered an internal error and was unable to complete your request.</p>
|
|
||||||
<p>Our team has been notified and is working to fix the issue.</p>
|
|
||||||
<div className="error-actions">
|
|
||||||
<Link href="/teams" className="button primary">
|
|
||||||
Browse teams
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/qyZ5hGdPC8"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
className="button secondary"
|
|
||||||
>
|
|
||||||
Report on Discord
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import { useFavorites } from '~/hooks/useFavorites'
|
|
||||||
import { useTeamFilter } from '~/hooks/useTeamFilter'
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
import { defaultFilterset } from '~/utils/defaultFilters'
|
|
||||||
import { CollectionPage } from '~/utils/enums'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import FilterBar from '~/components/filters/FilterBar'
|
|
||||||
import GridRep from '~/components/reps/GridRep'
|
|
||||||
import GridRepCollection from '~/components/reps/GridRepCollection'
|
|
||||||
import LoadingRep from '~/components/reps/LoadingRep'
|
|
||||||
import ErrorSection from '~/components/ErrorSection'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface Pagination {
|
|
||||||
current_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
record_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
initialData: {
|
|
||||||
teams: Party[];
|
|
||||||
raidGroups: any[];
|
|
||||||
pagination: Pagination;
|
|
||||||
};
|
|
||||||
initialElement?: number;
|
|
||||||
initialRaid?: string;
|
|
||||||
initialRecency?: string;
|
|
||||||
error?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TeamsPageClient: React.FC<Props> = ({
|
|
||||||
initialData,
|
|
||||||
initialElement,
|
|
||||||
initialRaid,
|
|
||||||
initialRecency,
|
|
||||||
error = false
|
|
||||||
}) => {
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// State management
|
|
||||||
const [parties, setParties] = useState<Party[]>(initialData.teams)
|
|
||||||
const [currentPage, setCurrentPage] = useState(initialData.pagination.current_page)
|
|
||||||
const [totalPages, setTotalPages] = useState(initialData.pagination.total_pages)
|
|
||||||
const [recordCount, setRecordCount] = useState(initialData.pagination.record_count)
|
|
||||||
const [loaded, setLoaded] = useState(true)
|
|
||||||
const [fetching, setFetching] = useState(false)
|
|
||||||
const [element, setElement] = useState(initialElement || 0)
|
|
||||||
const [raid, setRaid] = useState(initialRaid || '')
|
|
||||||
const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
|
|
||||||
const [advancedFilters, setAdvancedFilters] = useState({})
|
|
||||||
|
|
||||||
const { toggleFavorite } = useFavorites(parties, setParties)
|
|
||||||
|
|
||||||
// Initialize app state with raid groups
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData.raidGroups.length > 0) {
|
|
||||||
appState.raidGroups = initialData.raidGroups
|
|
||||||
}
|
|
||||||
}, [initialData.raidGroups])
|
|
||||||
|
|
||||||
// Update URL when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(searchParams?.toString() ?? '')
|
|
||||||
|
|
||||||
// Update or remove parameters based on filter values
|
|
||||||
if (element) {
|
|
||||||
params.set('element', element.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('element')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raid) {
|
|
||||||
params.set('raid', raid)
|
|
||||||
} else {
|
|
||||||
params.delete('raid')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recency) {
|
|
||||||
params.set('recency', recency.toString())
|
|
||||||
} else {
|
|
||||||
params.delete('recency')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update URL if filters are changed
|
|
||||||
const newQueryString = params.toString()
|
|
||||||
const currentQuery = searchParams?.toString() ?? ''
|
|
||||||
|
|
||||||
if (newQueryString !== currentQuery) {
|
|
||||||
router.push(`/teams${newQueryString ? `?${newQueryString}` : ''}`)
|
|
||||||
}
|
|
||||||
}, [element, raid, recency, router, searchParams])
|
|
||||||
|
|
||||||
// Load more teams when scrolling
|
|
||||||
async function loadMoreTeams() {
|
|
||||||
if (fetching || currentPage >= totalPages) return
|
|
||||||
|
|
||||||
setFetching(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Construct URL for fetching more data
|
|
||||||
const url = new URL('/api/parties', 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 (recency) url.searchParams.set('recency', recency.toString())
|
|
||||||
|
|
||||||
const response = await fetch(url.toString())
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading more teams', error)
|
|
||||||
} finally {
|
|
||||||
setFetching(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive filters from the filter bar
|
|
||||||
function receiveFilters(filters: FilterSet) {
|
|
||||||
if ('element' in filters) {
|
|
||||||
setElement(filters.element || 0)
|
|
||||||
}
|
|
||||||
if ('recency' in filters) {
|
|
||||||
setRecency(filters.recency || 0)
|
|
||||||
}
|
|
||||||
if ('raid' in filters) {
|
|
||||||
setRaid(filters.raid || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to page 1 when filters change
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveAdvancedFilters(filters: FilterSet) {
|
|
||||||
setAdvancedFilters(filters)
|
|
||||||
// Reset to page 1 when filters change
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Navigation
|
|
||||||
function goTo(shortcode: string) {
|
|
||||||
router.push(`/p/${shortcode}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page component rendering methods
|
|
||||||
function renderParties() {
|
|
||||||
return parties.map((party, i) => (
|
|
||||||
<GridRep
|
|
||||||
party={party}
|
|
||||||
key={`party-${i}`}
|
|
||||||
loading={fetching}
|
|
||||||
onClick={() => goTo(party.shortcode)}
|
|
||||||
onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLoading(number: number) {
|
|
||||||
return (
|
|
||||||
<GridRepCollection>
|
|
||||||
{Array.from({ length: number }, (_, i) => (
|
|
||||||
<LoadingRep key={`loading-${i}`} />
|
|
||||||
))}
|
|
||||||
</GridRepCollection>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorSection
|
|
||||||
status={{
|
|
||||||
code: 500,
|
|
||||||
text: 'internal_server_error'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderInfiniteScroll = (
|
|
||||||
<>
|
|
||||||
{parties.length === 0 && !loaded && renderLoading(3)}
|
|
||||||
{parties.length === 0 && loaded && (
|
|
||||||
<div className="notFound">
|
|
||||||
<h2>{t('teams.not_found')}</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{parties.length > 0 && (
|
|
||||||
<InfiniteScroll
|
|
||||||
dataLength={parties.length}
|
|
||||||
next={loadMoreTeams}
|
|
||||||
hasMore={totalPages > currentPage}
|
|
||||||
loader={renderLoading(3)}
|
|
||||||
>
|
|
||||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
|
||||||
</InfiniteScroll>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterBar
|
|
||||||
defaultFilterset={defaultFilterset}
|
|
||||||
onFilter={receiveFilters}
|
|
||||||
onAdvancedFilter={receiveAdvancedFilters}
|
|
||||||
persistFilters={true}
|
|
||||||
element={element}
|
|
||||||
raid={raid}
|
|
||||||
raidGroups={initialData.raidGroups}
|
|
||||||
recency={recency}
|
|
||||||
>
|
|
||||||
<h1>{t('teams.title')}</h1>
|
|
||||||
</FilterBar>
|
|
||||||
|
|
||||||
<section>{renderInfiniteScroll}</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TeamsPageClient
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import React from 'react'
|
|
||||||
import { getTeams as fetchTeams, getRaidGroups } from '~/app/lib/data'
|
|
||||||
import TeamsPageClient from './TeamsPageClient'
|
|
||||||
|
|
||||||
// Force dynamic rendering because we use searchParams
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Discover teams / granblue.team',
|
|
||||||
description: 'Save and discover teams to use in Granblue Fantasy and search by raid, element or recency',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function TeamsPage({
|
|
||||||
searchParams
|
|
||||||
}: {
|
|
||||||
searchParams: { element?: string; raid?: string; recency?: string; page?: string }
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
// Extract query parameters with type safety
|
|
||||||
const element = searchParams.element ? parseInt(searchParams.element, 10) : undefined;
|
|
||||||
const raid = searchParams.raid;
|
|
||||||
const recency = searchParams.recency;
|
|
||||||
const page = searchParams.page ? parseInt(searchParams.page, 10) : 1;
|
|
||||||
|
|
||||||
// Parallel fetch data with Promise.all for better performance
|
|
||||||
const [teamsData, raidGroupsData] = await Promise.all([
|
|
||||||
fetchTeams({ element, raid, recency, page }),
|
|
||||||
getRaidGroups()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Prepare data for client component
|
|
||||||
const initialData = {
|
|
||||||
teams: teamsData.results || [],
|
|
||||||
raidGroups: raidGroupsData || [],
|
|
||||||
pagination: {
|
|
||||||
current_page: page,
|
|
||||||
total_pages: teamsData.meta?.total_pages || 1,
|
|
||||||
record_count: teamsData.meta?.count || 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="teams">
|
|
||||||
{/* Pass server data to client component */}
|
|
||||||
<TeamsPageClient
|
|
||||||
initialData={initialData}
|
|
||||||
initialElement={element}
|
|
||||||
initialRaid={raid}
|
|
||||||
initialRecency={recency}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching teams data:", error);
|
|
||||||
|
|
||||||
// Fallback data for error case
|
|
||||||
return (
|
|
||||||
<div className="teams">
|
|
||||||
<TeamsPageClient
|
|
||||||
initialData={{ teams: [], raidGroups: [], pagination: { current_page: 1, total_pages: 1, record_count: 0 } }}
|
|
||||||
error={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Unauthorized / granblue.team',
|
|
||||||
description: "You don't have permission to perform that action"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UnauthorizedPage() {
|
|
||||||
return (
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>Unauthorized</h1>
|
|
||||||
<p>You don't have permission to perform that action</p>
|
|
||||||
<div className="error-actions">
|
|
||||||
<Link href="/teams" className="button primary">
|
|
||||||
Browse teams
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
'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>(AboutTabs.Updates)
|
|
||||||
|
|
||||||
function handleTabClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const value = event.target.value
|
|
||||||
router.push(`/${value}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSection = () => {
|
|
||||||
switch (currentTab) {
|
|
||||||
case AboutTabs.About:
|
|
||||||
return <AboutPage />
|
|
||||||
case AboutTabs.Updates:
|
|
||||||
return <UpdatesPage />
|
|
||||||
case AboutTabs.Roadmap:
|
|
||||||
return <RoadmapPage />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<SegmentedControl blended={true}>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="about"
|
|
||||||
selected={currentTab == AboutTabs.About}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.about')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="updates"
|
|
||||||
selected={currentTab == AboutTabs.Updates}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.updates')}
|
|
||||||
</Segment>
|
|
||||||
<Segment
|
|
||||||
groupName="about"
|
|
||||||
name="roadmap"
|
|
||||||
selected={currentTab == AboutTabs.Roadmap}
|
|
||||||
onClick={handleTabClicked}
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.roadmap')}
|
|
||||||
</Segment>
|
|
||||||
</SegmentedControl>
|
|
||||||
{currentSection()}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
import { getTranslations } from 'next-intl/server'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid useContext issues during static generation
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
import UpdatesPageClient from './UpdatesPageClient'
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params: { locale }
|
|
||||||
}: {
|
|
||||||
params: { locale: string }
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
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 (
|
|
||||||
<div id="About">
|
|
||||||
<UpdatesPageClient />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
import { login as loginHelper } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Login request schema
|
|
||||||
const LoginSchema = z.object({
|
|
||||||
email: z.string().email('Invalid email format'),
|
|
||||||
password: z.string().min(8, 'Password must be at least 8 characters')
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse and validate request body
|
|
||||||
const body = await request.json()
|
|
||||||
const validatedData = LoginSchema.parse(body)
|
|
||||||
|
|
||||||
// Call login helper with credentials
|
|
||||||
const response = await loginHelper(validatedData)
|
|
||||||
|
|
||||||
// Set cookies based on response
|
|
||||||
if (response.token) {
|
|
||||||
// Calculate expiration (60 days)
|
|
||||||
const expiresAt = new Date()
|
|
||||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
|
||||||
|
|
||||||
// Set account cookie with auth info
|
|
||||||
const accountCookie = {
|
|
||||||
userId: response.user_id,
|
|
||||||
username: response.username,
|
|
||||||
role: response.role,
|
|
||||||
token: response.token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set user cookie with preferences/profile
|
|
||||||
const userCookie = {
|
|
||||||
avatar: {
|
|
||||||
picture: response.avatar.picture,
|
|
||||||
element: response.avatar.element
|
|
||||||
},
|
|
||||||
gender: response.gender,
|
|
||||||
language: response.language,
|
|
||||||
theme: response.theme,
|
|
||||||
bahamut: response.bahamut || false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cookies
|
|
||||||
const cookieStore = cookies()
|
|
||||||
cookieStore.set('account', JSON.stringify(accountCookie), {
|
|
||||||
expires: expiresAt,
|
|
||||||
path: '/',
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict'
|
|
||||||
})
|
|
||||||
|
|
||||||
cookieStore.set('user', JSON.stringify(userCookie), {
|
|
||||||
expires: expiresAt,
|
|
||||||
path: '/',
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return success
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
user: {
|
|
||||||
username: response.username,
|
|
||||||
avatar: response.avatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, something went wrong
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid login response' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For authentication errors
|
|
||||||
if (error && typeof error === 'object' && 'response' in error) {
|
|
||||||
const axiosError = error as any
|
|
||||||
if (axiosError.response?.status === 401) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid email or password' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Login error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Login failed' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Delete cookies
|
|
||||||
const cookieStore = cookies()
|
|
||||||
cookieStore.delete('account')
|
|
||||||
cookieStore.delete('user')
|
|
||||||
|
|
||||||
// Return success
|
|
||||||
return NextResponse.json({ success: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Logout failed' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { postToApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Signup request schema
|
|
||||||
const SignupSchema = z.object({
|
|
||||||
username: z.string()
|
|
||||||
.min(3, 'Username must be at least 3 characters')
|
|
||||||
.max(20, 'Username must be less than 20 characters')
|
|
||||||
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens'),
|
|
||||||
email: z.string().email('Invalid email format'),
|
|
||||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
||||||
password_confirmation: z.string()
|
|
||||||
}).refine(data => data.password === data.password_confirmation, {
|
|
||||||
message: "Passwords don't match",
|
|
||||||
path: ['password_confirmation']
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse and validate request body
|
|
||||||
const body = await request.json()
|
|
||||||
const validatedData = SignupSchema.parse(body)
|
|
||||||
|
|
||||||
// Call signup endpoint
|
|
||||||
const response = await postToApi('/users', {
|
|
||||||
user: {
|
|
||||||
username: validatedData.username,
|
|
||||||
email: validatedData.email,
|
|
||||||
password: validatedData.password,
|
|
||||||
password_confirmation: validatedData.password_confirmation
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return created user info
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
user: {
|
|
||||||
username: response.username,
|
|
||||||
email: response.email
|
|
||||||
}
|
|
||||||
}, { status: 201 })
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle specific API errors
|
|
||||||
if (error && typeof error === 'object' && 'response' in error) {
|
|
||||||
const axiosError = error as any
|
|
||||||
if (axiosError.response?.data?.error) {
|
|
||||||
const apiError = axiosError.response.data.error
|
|
||||||
|
|
||||||
// Username or email already in use
|
|
||||||
if (apiError.includes('username') || apiError.includes('email')) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: apiError },
|
|
||||||
{ status: 409 } // Conflict
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Signup error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Signup failed' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { fetchFromApi, postToApi, deleteFromApi } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Schema for favorite request
|
|
||||||
const FavoriteSchema = z.object({
|
|
||||||
favorite: z.object({
|
|
||||||
party_id: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET handler for fetching user's favorites
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Get saved teams/favorites
|
|
||||||
const data = await fetchFromApi('/parties/favorites');
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching favorites', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch favorites' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for adding a favorite
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
const validatedData = FavoriteSchema.parse(body);
|
|
||||||
|
|
||||||
// Save the favorite
|
|
||||||
const response = await postToApi('/favorites', validatedData);
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error saving favorite', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to save favorite' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE handler for removing a favorite
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
const validatedData = FavoriteSchema.parse(body);
|
|
||||||
|
|
||||||
// Delete the favorite
|
|
||||||
const response = await deleteFromApi('/favorites', validatedData);
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error removing favorite', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to remove favorite' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// GET handler for fetching accessories for a specific job
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = params
|
|
||||||
|
|
||||||
const data = await fetchFromApi(`/jobs/${id}/accessories`)
|
|
||||||
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error fetching accessories for job ${params.id}`, error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch job accessories' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// GET handler for fetching skills for a specific job
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = params
|
|
||||||
|
|
||||||
const data = await fetchFromApi(`/jobs/${id}/skills`)
|
|
||||||
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error fetching skills for job ${params.id}`, error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch job skills' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Force dynamic rendering because we use searchParams
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
// GET handler for fetching all jobs
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse URL parameters
|
|
||||||
const searchParams = request.nextUrl.searchParams
|
|
||||||
const element = searchParams.get('element')
|
|
||||||
|
|
||||||
// Build query parameters
|
|
||||||
const queryParams: Record<string, string> = {}
|
|
||||||
if (element) queryParams.element = element
|
|
||||||
|
|
||||||
// Append query parameters
|
|
||||||
let endpoint = '/jobs'
|
|
||||||
const queryString = new URLSearchParams(queryParams).toString()
|
|
||||||
if (queryString) {
|
|
||||||
endpoint += `?${queryString}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetchFromApi(endpoint)
|
|
||||||
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching jobs', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch jobs' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Force dynamic rendering because fetchFromApi uses cookies
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
// GET handler for fetching all job skills
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi('/jobs/skills')
|
|
||||||
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching job skills', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch job skills' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { postToApi, revalidate } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Force dynamic rendering because postToApi uses cookies
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// POST handler for remixing a party
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { shortcode: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { shortcode } = params;
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Remix the party
|
|
||||||
const response = await postToApi(`/parties/${shortcode}/remix`, body || {});
|
|
||||||
|
|
||||||
// Revalidate the teams page since a new party was created
|
|
||||||
revalidate('/teams');
|
|
||||||
|
|
||||||
if (response.shortcode) {
|
|
||||||
// Revalidate the new party page
|
|
||||||
revalidate(`/p/${response.shortcode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error remixing party with shortcode ${params.shortcode}`, error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to remix party' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { fetchFromApi, putToApi, deleteFromApi, revalidate, PartySchema } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Force dynamic rendering because fetchFromApi uses cookies
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// GET handler for fetching a single party by shortcode
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { shortcode: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { shortcode } = params;
|
|
||||||
|
|
||||||
// Fetch party data
|
|
||||||
const data = await fetchFromApi(`/parties/${shortcode}`);
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error fetching party with shortcode ${params.shortcode}`, error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch party' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update party schema
|
|
||||||
const UpdatePartySchema = PartySchema.extend({
|
|
||||||
id: z.string().optional(),
|
|
||||||
shortcode: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT handler for updating a party
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { shortcode: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { shortcode } = params;
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate the request body
|
|
||||||
const validatedData = UpdatePartySchema.parse(body.party);
|
|
||||||
|
|
||||||
// Update the party
|
|
||||||
const response = await putToApi(`/parties/${shortcode}`, {
|
|
||||||
party: validatedData
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revalidate the party page
|
|
||||||
revalidate(`/p/${shortcode}`);
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to update party' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE handler for deleting a party
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { shortcode: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { shortcode } = params;
|
|
||||||
|
|
||||||
// Delete the party
|
|
||||||
const response = await deleteFromApi(`/parties/${shortcode}`);
|
|
||||||
|
|
||||||
// Revalidate related pages
|
|
||||||
revalidate(`/teams`);
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to delete party' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { fetchFromApi, postToApi, PartySchema } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Force dynamic rendering because we use searchParams and cookies
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// GET handler for fetching parties with filters
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse URL parameters
|
|
||||||
const searchParams = request.nextUrl.searchParams;
|
|
||||||
const element = searchParams.get('element');
|
|
||||||
const raid = searchParams.get('raid');
|
|
||||||
const recency = searchParams.get('recency');
|
|
||||||
const page = searchParams.get('page') || '1';
|
|
||||||
const username = searchParams.get('username');
|
|
||||||
|
|
||||||
// Build query parameters
|
|
||||||
const queryParams: Record<string, string> = {};
|
|
||||||
if (element) queryParams.element = element;
|
|
||||||
if (raid) queryParams.raid_id = raid;
|
|
||||||
if (recency) queryParams.recency = recency;
|
|
||||||
if (page) queryParams.page = page;
|
|
||||||
|
|
||||||
let endpoint = '/parties';
|
|
||||||
|
|
||||||
// If username is provided, fetch that user's parties
|
|
||||||
if (username) {
|
|
||||||
endpoint = `/users/${username}/parties`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append query parameters
|
|
||||||
const queryString = new URLSearchParams(queryParams).toString();
|
|
||||||
if (queryString) {
|
|
||||||
endpoint += `?${queryString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetchFromApi(endpoint);
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching parties', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch parties' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate party data
|
|
||||||
const CreatePartySchema = PartySchema.extend({
|
|
||||||
element: z.number().min(1).max(6),
|
|
||||||
raid_id: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST handler for creating a new party
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate the request body
|
|
||||||
const validatedData = CreatePartySchema.parse(body.party);
|
|
||||||
|
|
||||||
const response = await postToApi('/parties', {
|
|
||||||
party: validatedData
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to create party' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Force dynamic rendering because fetchFromApi uses cookies
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// GET handler for fetching raid groups
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Fetch raid groups
|
|
||||||
const data = await fetchFromApi('/raids/groups');
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching raid groups', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch raid groups' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { postToApi, SearchSchema } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Validate the object type
|
|
||||||
const ObjectTypeSchema = z.enum(['characters', 'weapons', 'summons', 'job_skills']);
|
|
||||||
|
|
||||||
// POST handler for search
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { object: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { object } = params;
|
|
||||||
|
|
||||||
// Validate object type
|
|
||||||
const validObjectType = ObjectTypeSchema.safeParse(object);
|
|
||||||
if (!validObjectType.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Invalid object type: ${object}` },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate search parameters
|
|
||||||
const validatedSearch = SearchSchema.parse(body.search);
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const response = await postToApi(`/search/${object}`, {
|
|
||||||
search: validatedSearch
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Error searching ${params.object}`, error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Search failed' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { postToApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Validate the search request
|
|
||||||
const SearchAllSchema = z.object({
|
|
||||||
search: z.object({
|
|
||||||
query: z.string().min(1, 'Search query is required'),
|
|
||||||
exclude: z.array(z.string()).optional(),
|
|
||||||
locale: z.string().default('en')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST handler for searching across all types
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse and validate request body
|
|
||||||
const body = await request.json()
|
|
||||||
const validatedData = SearchAllSchema.parse(body)
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const response = await postToApi('/search', validatedData)
|
|
||||||
|
|
||||||
return NextResponse.json(response)
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error searching', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Search failed' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// GET handler for fetching user info
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { username: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { username } = params;
|
|
||||||
|
|
||||||
// Fetch user info
|
|
||||||
const data = await fetchFromApi(`/users/info/${username}`);
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error fetching user info for ${params.username}`, error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch user info' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
import { putToApi } from '~/app/lib/api-utils'
|
|
||||||
|
|
||||||
// Settings update schema
|
|
||||||
const SettingsSchema = z.object({
|
|
||||||
picture: z.string().optional(),
|
|
||||||
gender: z.enum(['gran', 'djeeta']).optional(),
|
|
||||||
language: z.enum(['en', 'ja']).optional(),
|
|
||||||
theme: z.enum(['light', 'dark', 'system']).optional(),
|
|
||||||
bahamut: z.boolean().optional()
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Parse and validate request body
|
|
||||||
const body = await request.json()
|
|
||||||
const validatedData = SettingsSchema.parse(body)
|
|
||||||
|
|
||||||
// Get user info from cookie
|
|
||||||
const cookieStore = cookies()
|
|
||||||
const accountCookie = cookieStore.get('account')
|
|
||||||
|
|
||||||
if (!accountCookie) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Authentication required' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse account cookie
|
|
||||||
const accountData = JSON.parse(accountCookie.value)
|
|
||||||
|
|
||||||
// Call API to update settings
|
|
||||||
const response = await putToApi(`/users/${accountData.userId}`, {
|
|
||||||
user: validatedData
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update user cookie with new settings
|
|
||||||
const userCookie = cookieStore.get('user')
|
|
||||||
if (userCookie) {
|
|
||||||
const userData = JSON.parse(userCookie.value)
|
|
||||||
|
|
||||||
// Update user data
|
|
||||||
const updatedUserData = {
|
|
||||||
...userData,
|
|
||||||
avatar: {
|
|
||||||
...userData.avatar,
|
|
||||||
picture: validatedData.picture || userData.avatar.picture
|
|
||||||
},
|
|
||||||
gender: validatedData.gender || userData.gender,
|
|
||||||
language: validatedData.language || userData.language,
|
|
||||||
theme: validatedData.theme || userData.theme,
|
|
||||||
bahamut: validatedData.bahamut !== undefined ? validatedData.bahamut : userData.bahamut
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set updated cookie
|
|
||||||
const expiresAt = new Date()
|
|
||||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
|
||||||
|
|
||||||
cookieStore.set('user', JSON.stringify(updatedUserData), {
|
|
||||||
expires: expiresAt,
|
|
||||||
path: '/',
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return updated user info
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
user: response
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Settings update error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to update settings' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { fetchFromApi } from '~/app/lib/api-utils';
|
|
||||||
|
|
||||||
// Force dynamic rendering because fetchFromApi uses cookies
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// GET handler for fetching version info
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Fetch version info
|
|
||||||
const data = await fetchFromApi('/version');
|
|
||||||
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching version info', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch version info' },
|
|
||||||
{ status: error.response?.status || 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,404 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { deleteCookie } from 'cookies-next'
|
|
||||||
import { useRouter } from '~/i18n/navigation'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import { useLocale } from 'next-intl'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import clonedeep from 'lodash.clonedeep'
|
|
||||||
import { Link } from '~/i18n/navigation'
|
|
||||||
|
|
||||||
import { accountState, initialAccountState } from '~/utils/accountState'
|
|
||||||
import { appState, initialAppState } from '~/utils/appState'
|
|
||||||
|
|
||||||
import Alert from '~/components/common/Alert'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from '~/components/common/DropdownMenuContent'
|
|
||||||
import DropdownMenuGroup from '~/components/common/DropdownMenuGroup'
|
|
||||||
import DropdownMenuLabel from '~/components/common/DropdownMenuLabel'
|
|
||||||
import DropdownMenuItem from '~/components/common/DropdownMenuItem'
|
|
||||||
import LanguageSwitch from '~/components/LanguageSwitch'
|
|
||||||
import LoginModal from '~/components/auth/LoginModal'
|
|
||||||
import SignupModal from '~/components/auth/SignupModal'
|
|
||||||
import AccountModal from '~/components/auth/AccountModal'
|
|
||||||
import Button from '~/components/common/Button'
|
|
||||||
import Tooltip from '~/components/common/Tooltip'
|
|
||||||
|
|
||||||
import BahamutIcon from '~/public/icons/Bahamut.svg'
|
|
||||||
import ChevronIcon from '~/public/icons/Chevron.svg'
|
|
||||||
import MenuIcon from '~/public/icons/Menu.svg'
|
|
||||||
import PlusIcon from '~/public/icons/Add.svg'
|
|
||||||
|
|
||||||
import styles from '~/components/Header/index.module.scss'
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
// Localization
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const locale = useLocale()
|
|
||||||
|
|
||||||
// Router
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// State management
|
|
||||||
const [alertOpen, setAlertOpen] = useState(false)
|
|
||||||
const [loginModalOpen, setLoginModalOpen] = useState(false)
|
|
||||||
const [signupModalOpen, setSignupModalOpen] = useState(false)
|
|
||||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
|
|
||||||
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
|
|
||||||
const [rightMenuOpen, setRightMenuOpen] = useState(false)
|
|
||||||
|
|
||||||
// Methods: Event handlers (Buttons)
|
|
||||||
function handleLeftMenuButtonClicked() {
|
|
||||||
setLeftMenuOpen(!leftMenuOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRightMenuButtonClicked() {
|
|
||||||
setRightMenuOpen(!rightMenuOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Event handlers (Menus)
|
|
||||||
function handleLeftMenuOpenChange(open: boolean) {
|
|
||||||
setLeftMenuOpen(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRightMenuOpenChange(open: boolean) {
|
|
||||||
setRightMenuOpen(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLeftMenu() {
|
|
||||||
setLeftMenuOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeRightMenu() {
|
|
||||||
setRightMenuOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Actions
|
|
||||||
function handleNewTeam(event: React.MouseEvent) {
|
|
||||||
event.preventDefault()
|
|
||||||
newTeam()
|
|
||||||
closeRightMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout() {
|
|
||||||
// Close menu
|
|
||||||
closeRightMenu()
|
|
||||||
|
|
||||||
// Delete cookies
|
|
||||||
deleteCookie('account')
|
|
||||||
deleteCookie('user')
|
|
||||||
|
|
||||||
// Clean state
|
|
||||||
const resetState = clonedeep(initialAccountState)
|
|
||||||
Object.keys(resetState).forEach((key) => {
|
|
||||||
if (key !== 'language') accountState[key] = resetState[key]
|
|
||||||
})
|
|
||||||
|
|
||||||
router.refresh()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function newTeam() {
|
|
||||||
// Clean state
|
|
||||||
const resetState = clonedeep(initialAppState)
|
|
||||||
Object.keys(resetState).forEach((key) => {
|
|
||||||
appState[key] = resetState[key]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Push the new URL
|
|
||||||
router.push('/new')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Rendering
|
|
||||||
const profileImage = () => {
|
|
||||||
const user = accountState.account.user
|
|
||||||
if (accountState.account.authorized && user) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
alt={user.username}
|
|
||||||
className={`profile ${user.avatar.element}`}
|
|
||||||
srcSet={`/profile/${user.avatar.picture}.png,
|
|
||||||
/profile/${user.avatar.picture}@2x.png 2x`}
|
|
||||||
src={`/profile/${user.avatar.picture}.png`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
alt={t('no_user')}
|
|
||||||
className={`profile anonymous`}
|
|
||||||
srcSet={`/profile/npc.png,
|
|
||||||
/profile/npc@2x.png 2x`}
|
|
||||||
src={`/profile/npc.png`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rendering: Buttons
|
|
||||||
const newButton = (
|
|
||||||
<Tooltip content={t('tooltips.new')}>
|
|
||||||
<Button
|
|
||||||
leftAccessoryIcon={<PlusIcon />}
|
|
||||||
className="New"
|
|
||||||
blended={true}
|
|
||||||
text={t('buttons.new')}
|
|
||||||
onClick={newTeam}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rendering: Modals
|
|
||||||
const logoutConfirmationAlert = (
|
|
||||||
<Alert
|
|
||||||
message={t('alert.confirm_logout')}
|
|
||||||
open={alertOpen}
|
|
||||||
primaryActionText="Log out"
|
|
||||||
primaryAction={logout}
|
|
||||||
cancelActionText="Nevermind"
|
|
||||||
cancelAction={() => setAlertOpen(false)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
const settingsModal = (
|
|
||||||
<>
|
|
||||||
{accountState.account.user && (
|
|
||||||
<AccountModal
|
|
||||||
open={settingsModalOpen}
|
|
||||||
username={accountState.account.user.username}
|
|
||||||
picture={accountState.account.user.avatar.picture}
|
|
||||||
gender={accountState.account.user.gender}
|
|
||||||
language={accountState.account.user.language}
|
|
||||||
theme={accountState.account.user.theme}
|
|
||||||
role={accountState.account.user.role}
|
|
||||||
bahamutMode={
|
|
||||||
accountState.account.user.role === 9
|
|
||||||
? accountState.account.user.bahamut
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onOpenChange={setSettingsModalOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const loginModal = (
|
|
||||||
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
|
|
||||||
)
|
|
||||||
|
|
||||||
const signupModal = (
|
|
||||||
<SignupModal open={signupModalOpen} onOpenChange={setSignupModalOpen} />
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rendering: Compositing
|
|
||||||
const authorizedLeftItems = (
|
|
||||||
<>
|
|
||||||
{accountState.account.user && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<Link
|
|
||||||
href={`/${accountState.account.user.username}` || ''}
|
|
||||||
>
|
|
||||||
<span>{t('menu.profile')}</span>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
const leftMenuItems = (
|
|
||||||
<>
|
|
||||||
{accountState.account.authorized &&
|
|
||||||
accountState.account.user &&
|
|
||||||
authorizedLeftItems}
|
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<Link href="/teams">{t('menu.teams')}</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<div>
|
|
||||||
<span>{t('menu.guides')}</span>
|
|
||||||
<i className="tag">{t('coming_soon')}</i>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<a
|
|
||||||
href={locale == 'ja' ? '/ja/about' : '/about'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.about')}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<a
|
|
||||||
href={locale == 'ja' ? '/ja/updates' : '/updates'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.updates')}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
|
||||||
<a
|
|
||||||
href={locale == 'ja' ? '/ja/roadmap' : '/roadmap'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{t('about.segmented_control.roadmap')}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const left = (
|
|
||||||
<section>
|
|
||||||
<div className={styles.dropdownWrapper}>
|
|
||||||
<DropdownMenu
|
|
||||||
open={leftMenuOpen}
|
|
||||||
onOpenChange={handleLeftMenuOpenChange}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
active={leftMenuOpen}
|
|
||||||
blended={true}
|
|
||||||
leftAccessoryIcon={<MenuIcon />}
|
|
||||||
onClick={handleLeftMenuButtonClicked}
|
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="Left">
|
|
||||||
{leftMenuItems}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
|
|
||||||
const authorizedRightItems = (
|
|
||||||
<>
|
|
||||||
{accountState.account.user && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel>
|
|
||||||
{`@${accountState.account.user.username}`}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuItem onClick={closeRightMenu}>
|
|
||||||
<Link
|
|
||||||
href={`/${accountState.account.user.username}` || ''}
|
|
||||||
>
|
|
||||||
<span>{t('menu.profile')}</span>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="MenuItem"
|
|
||||||
onClick={() => setSettingsModalOpen(true)}
|
|
||||||
>
|
|
||||||
<span>{t('menu.settings')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setAlertOpen(true)}
|
|
||||||
destructive={true}
|
|
||||||
>
|
|
||||||
<span>{t('menu.logout')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const unauthorizedRightItems = (
|
|
||||||
<>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem className="language">
|
|
||||||
<span>{t('menu.language')}</span>
|
|
||||||
<LanguageSwitch />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuGroup className="MenuGroup">
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="MenuItem"
|
|
||||||
onClick={() => setLoginModalOpen(true)}
|
|
||||||
>
|
|
||||||
<span>{t('menu.login')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="MenuItem"
|
|
||||||
onClick={() => setSignupModalOpen(true)}
|
|
||||||
>
|
|
||||||
<span>{t('menu.signup')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const rightMenuItems = (
|
|
||||||
<>
|
|
||||||
{accountState.account.authorized && accountState.account.user
|
|
||||||
? authorizedRightItems
|
|
||||||
: unauthorizedRightItems}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const right = (
|
|
||||||
<section>
|
|
||||||
{newButton}
|
|
||||||
<DropdownMenu
|
|
||||||
open={rightMenuOpen}
|
|
||||||
onOpenChange={handleRightMenuOpenChange}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className={classNames({ Active: rightMenuOpen })}
|
|
||||||
leftAccessoryIcon={profileImage()}
|
|
||||||
rightAccessoryIcon={<ChevronIcon />}
|
|
||||||
rightAccessoryClassName="Arrow"
|
|
||||||
onClick={handleRightMenuButtonClicked}
|
|
||||||
blended={true}
|
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="Right">
|
|
||||||
{rightMenuItems}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{accountState.account.user?.bahamut && (
|
|
||||||
<div className={styles.bahamut}>
|
|
||||||
<BahamutIcon />
|
|
||||||
<p>Bahamut Mode is active</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<nav className={styles.header}>
|
|
||||||
{left}
|
|
||||||
{right}
|
|
||||||
{logoutConfirmationAlert}
|
|
||||||
{settingsModal}
|
|
||||||
{loginModal}
|
|
||||||
{signupModal}
|
|
||||||
</nav>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
import { ThemeProvider } from 'next-themes'
|
|
||||||
import { ToastProvider } from '@radix-ui/react-toast'
|
|
||||||
import { TooltipProvider } from '@radix-ui/react-tooltip'
|
|
||||||
|
|
||||||
export default function Providers({ children }: { children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<ThemeProvider>
|
|
||||||
<ToastProvider swipeDirection="right">
|
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
|
||||||
</ToastProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import { add, format } from 'date-fns'
|
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
import UpdateToast from '~/components/toasts/UpdateToast'
|
|
||||||
|
|
||||||
interface UpdateToastClientProps {
|
|
||||||
initialVersion?: AppUpdate | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdateToastClient({ initialVersion }: UpdateToastClientProps) {
|
|
||||||
const pathname = usePathname()
|
|
||||||
const [updateToastOpen, setUpdateToastOpen] = useState(false)
|
|
||||||
const { version } = useSnapshot(appState)
|
|
||||||
|
|
||||||
// Use initialVersion for SSR, then switch to appState version after hydration
|
|
||||||
const effectiveVersion = version?.updated_at ? version : initialVersion
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (effectiveVersion && effectiveVersion.updated_at) {
|
|
||||||
const cookie = getToastCookie()
|
|
||||||
const now = new Date()
|
|
||||||
const updatedAt = new Date(effectiveVersion.updated_at)
|
|
||||||
const validUntil = add(updatedAt, { days: 7 })
|
|
||||||
|
|
||||||
if (now < validUntil && !cookie.seen) setUpdateToastOpen(true)
|
|
||||||
}
|
|
||||||
}, [effectiveVersion?.updated_at])
|
|
||||||
|
|
||||||
function getToastCookie() {
|
|
||||||
if (appState.version && appState.version.updated_at !== '') {
|
|
||||||
const updatedAt = new Date(appState.version.updated_at)
|
|
||||||
const cookieValues = getCookie(
|
|
||||||
`update-${format(updatedAt, 'yyyy-MM-dd')}`
|
|
||||||
)
|
|
||||||
return cookieValues
|
|
||||||
? (JSON.parse(cookieValues as string) as { seen: true })
|
|
||||||
: { seen: false }
|
|
||||||
} else {
|
|
||||||
return { seen: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToastActionClicked() {
|
|
||||||
setUpdateToastOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToastClosed() {
|
|
||||||
setUpdateToastOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = pathname?.replaceAll('/', '') || ''
|
|
||||||
|
|
||||||
// Only render toast if we have valid version data with update_type
|
|
||||||
if (!['about', 'updates', 'roadmap'].includes(path) && effectiveVersion && effectiveVersion.update_type) {
|
|
||||||
return (
|
|
||||||
<UpdateToast
|
|
||||||
open={updateToastOpen}
|
|
||||||
updateType={effectiveVersion.update_type}
|
|
||||||
onActionClicked={handleToastActionClicked}
|
|
||||||
onCloseClicked={handleToastClosed}
|
|
||||||
lastUpdated={effectiveVersion.updated_at}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { appState } from '~/utils/appState'
|
|
||||||
|
|
||||||
interface VersionHydratorProps {
|
|
||||||
version: AppUpdate | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function VersionHydrator({ version }: VersionHydratorProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (version && version.updated_at) {
|
|
||||||
appState.version = version
|
|
||||||
}
|
|
||||||
}, [version])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// Minimal root layout - all content is handled in [locale]/layout.tsx
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
import axios, { AxiosRequestConfig } from "axios";
|
|
||||||
import http from "http";
|
|
||||||
import https from "https";
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
// Base URL from environment variable
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/v1';
|
|
||||||
const oauthUrl = process.env.NEXT_PUBLIC_SIERO_OAUTH_URL || 'https://localhost:3000/oauth';
|
|
||||||
|
|
||||||
// Shared Axios instance with sane defaults for server-side calls
|
|
||||||
const httpClient = axios.create({
|
|
||||||
baseURL: baseUrl,
|
|
||||||
timeout: 15000,
|
|
||||||
// Keep connections alive to reduce socket churn
|
|
||||||
httpAgent: new http.Agent({ keepAlive: true, maxSockets: 50 }),
|
|
||||||
httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 50 }),
|
|
||||||
// Do not throw on HTTP status by default; let callers handle
|
|
||||||
validateStatus: () => true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Utility to get auth token from cookies on the server
|
|
||||||
export function getAuthToken() {
|
|
||||||
const cookieStore = cookies();
|
|
||||||
const accountCookie = cookieStore.get('account');
|
|
||||||
|
|
||||||
if (accountCookie) {
|
|
||||||
try {
|
|
||||||
const accountData = JSON.parse(accountCookie.value);
|
|
||||||
return accountData.token;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse account cookie', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create headers with auth token
|
|
||||||
export function createHeaders() {
|
|
||||||
const token = getAuthToken();
|
|
||||||
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for GET requests
|
|
||||||
export async function fetchFromApi(endpoint: string, config?: AxiosRequestConfig) {
|
|
||||||
const headers = createHeaders();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await httpClient.get(`${endpoint}`, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...(config?.headers || {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API fetch error: ${endpoint}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for POST requests
|
|
||||||
export async function postToApi(endpoint: string, data: any, config?: AxiosRequestConfig) {
|
|
||||||
const headers = createHeaders();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await httpClient.post(`${endpoint}`, data, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...(config?.headers || {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API post error: ${endpoint}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for PUT requests
|
|
||||||
export async function putToApi(endpoint: string, data: any, config?: AxiosRequestConfig) {
|
|
||||||
const headers = createHeaders();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await httpClient.put(`${endpoint}`, data, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...(config?.headers || {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API put error: ${endpoint}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for DELETE requests
|
|
||||||
export async function deleteFromApi(endpoint: string, data?: any, config?: AxiosRequestConfig) {
|
|
||||||
const headers = createHeaders();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await httpClient.delete(`${endpoint}`, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...(config?.headers || {})
|
|
||||||
},
|
|
||||||
data
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API delete error: ${endpoint}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for login endpoint
|
|
||||||
export async function login(credentials: { email: string; password: string }) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${oauthUrl}/token`, credentials);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to revalidate cache for a path
|
|
||||||
export function revalidate(path: string) {
|
|
||||||
try {
|
|
||||||
revalidatePath(path);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to revalidate ${path}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schemas for validation
|
|
||||||
export const UserSchema = z.object({
|
|
||||||
username: z.string().min(3).max(20),
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string().min(8),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PartySchema = z.object({
|
|
||||||
name: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
visibility: z.enum(['public', 'unlisted', 'private']),
|
|
||||||
raid_id: z.string().optional(),
|
|
||||||
element: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SearchSchema = z.object({
|
|
||||||
query: z.string(),
|
|
||||||
filters: z.record(z.array(z.number())).optional(),
|
|
||||||
job: z.string().optional(),
|
|
||||||
locale: z.string().default('en'),
|
|
||||||
page: z.number().default(0),
|
|
||||||
});
|
|
||||||
148
app/lib/data.ts
148
app/lib/data.ts
|
|
@ -1,148 +0,0 @@
|
||||||
import { fetchFromApi } from './api-utils';
|
|
||||||
|
|
||||||
// Server-side data fetching functions
|
|
||||||
// Next.js automatically deduplicates requests within the same render
|
|
||||||
|
|
||||||
// Get teams with optional filters
|
|
||||||
export async function getTeams({
|
|
||||||
element,
|
|
||||||
raid,
|
|
||||||
recency,
|
|
||||||
page = 1,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
element?: number;
|
|
||||||
raid?: string;
|
|
||||||
recency?: string;
|
|
||||||
page?: number;
|
|
||||||
username?: string;
|
|
||||||
}) {
|
|
||||||
const queryParams: Record<string, string> = {};
|
|
||||||
if (element) queryParams.element = element.toString();
|
|
||||||
if (raid) queryParams.raid_id = raid;
|
|
||||||
if (recency) queryParams.recency = recency;
|
|
||||||
if (page) queryParams.page = page.toString();
|
|
||||||
|
|
||||||
let endpoint = '/parties';
|
|
||||||
if (username) {
|
|
||||||
endpoint = `/users/${username}/parties`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = new URLSearchParams(queryParams).toString();
|
|
||||||
if (queryString) endpoint += `?${queryString}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi(endpoint);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch teams', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a single team by shortcode
|
|
||||||
export async function getTeam(shortcode: string) {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi(`/parties/${shortcode}`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch team with shortcode ${shortcode}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user info
|
|
||||||
export async function getUserInfo(username: string) {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi(`/users/info/${username}`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch user info for ${username}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get raid groups
|
|
||||||
export async function getRaidGroups() {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi('/raids/groups');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch raid groups', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get version info
|
|
||||||
export async function getVersion() {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi('/version');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch version info', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user's favorites/saved teams
|
|
||||||
export async function getFavorites() {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi('/parties/favorites');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch favorites', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all jobs
|
|
||||||
export async function getJobs(element?: number) {
|
|
||||||
try {
|
|
||||||
const queryParams: Record<string, string> = {};
|
|
||||||
if (element) queryParams.element = element.toString();
|
|
||||||
|
|
||||||
let endpoint = '/jobs';
|
|
||||||
const queryString = new URLSearchParams(queryParams).toString();
|
|
||||||
if (queryString) endpoint += `?${queryString}`;
|
|
||||||
|
|
||||||
const data = await fetchFromApi(endpoint);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch jobs', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get job by ID
|
|
||||||
export async function getJob(jobId: string) {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi(`/jobs/${jobId}`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch job with ID ${jobId}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get job skills
|
|
||||||
export async function getJobSkills(jobId?: string) {
|
|
||||||
try {
|
|
||||||
const endpoint = jobId ? `/jobs/${jobId}/skills` : '/jobs/skills';
|
|
||||||
const data = await fetchFromApi(endpoint);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch job skills', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get job accessories
|
|
||||||
export async function getJobAccessories(jobId: string) {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi(`/jobs/${jobId}/accessories`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch accessories for job ${jobId}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { Metadata } from 'next'
|
|
||||||
|
|
||||||
// Force dynamic rendering to avoid issues
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Page not found / granblue.team',
|
|
||||||
description: 'The page you were looking for could not be found'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
return (
|
|
||||||
<div className="error-container">
|
|
||||||
<div className="error-content">
|
|
||||||
<h1>404</h1>
|
|
||||||
<h2>Page Not Found</h2>
|
|
||||||
<p>The page you're looking for doesn't exist.</p>
|
|
||||||
<div className="error-actions">
|
|
||||||
<a href="/new" className="button primary">
|
|
||||||
Create a new party
|
|
||||||
</a>
|
|
||||||
<a href="/teams" className="button secondary">
|
|
||||||
Browse teams
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
// Error page styles
|
|
||||||
.error-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: calc(100vh - 60px); // Adjust for header height
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-content {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
.error-digest {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-color-tertiary);
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--primary-color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background-color: var(--background-color-tertiary);
|
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--background-color-quaternary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
import { accountState } from '~utils/accountState'
|
|
||||||
import { setHeaders } from '~utils/userToken'
|
|
||||||
|
|
||||||
interface InitialAuthData {
|
|
||||||
account: {
|
|
||||||
token: string
|
|
||||||
userId: string
|
|
||||||
username: string
|
|
||||||
role: number
|
|
||||||
}
|
|
||||||
user: {
|
|
||||||
avatar: {
|
|
||||||
picture: string
|
|
||||||
element: string
|
|
||||||
}
|
|
||||||
gender: number
|
|
||||||
language: string
|
|
||||||
theme: string
|
|
||||||
bahamut?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccountStateInitializerProps {
|
|
||||||
initialAuthData?: InitialAuthData | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AccountStateInitializer({ initialAuthData }: AccountStateInitializerProps) {
|
|
||||||
const initialized = useRef(false)
|
|
||||||
|
|
||||||
// Initialize synchronously on first render if we have server data
|
|
||||||
if (initialAuthData && !initialized.current) {
|
|
||||||
initialized.current = true
|
|
||||||
const { account: accountData, user: userData } = initialAuthData
|
|
||||||
|
|
||||||
console.log(`Logged in as user "${accountData.username}"`)
|
|
||||||
|
|
||||||
// Set headers for API calls
|
|
||||||
setHeaders()
|
|
||||||
|
|
||||||
// Update account state
|
|
||||||
accountState.account.authorized = true
|
|
||||||
accountState.account.user = {
|
|
||||||
id: accountData.userId,
|
|
||||||
username: accountData.username,
|
|
||||||
role: accountData.role,
|
|
||||||
granblueId: '',
|
|
||||||
avatar: {
|
|
||||||
picture: userData.avatar.picture,
|
|
||||||
element: userData.avatar.element,
|
|
||||||
},
|
|
||||||
gender: userData.gender,
|
|
||||||
language: userData.language,
|
|
||||||
theme: userData.theme,
|
|
||||||
bahamut: userData.bahamut || false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Only run client-side cookie reading if no server data
|
|
||||||
if (initialized.current) return
|
|
||||||
|
|
||||||
const accountCookie = getCookie('account')
|
|
||||||
const userCookie = getCookie('user')
|
|
||||||
|
|
||||||
if (accountCookie && userCookie) {
|
|
||||||
try {
|
|
||||||
const accountData = JSON.parse(accountCookie as string)
|
|
||||||
const userData = JSON.parse(userCookie as string)
|
|
||||||
|
|
||||||
if (accountData && accountData.token) {
|
|
||||||
console.log(`Logged in as user "${accountData.username}"`)
|
|
||||||
|
|
||||||
// Set headers for API calls
|
|
||||||
setHeaders()
|
|
||||||
|
|
||||||
// Update account state
|
|
||||||
accountState.account.authorized = true
|
|
||||||
accountState.account.user = {
|
|
||||||
id: accountData.userId,
|
|
||||||
username: accountData.username,
|
|
||||||
role: accountData.role,
|
|
||||||
granblueId: '',
|
|
||||||
avatar: {
|
|
||||||
picture: userData.avatar.picture,
|
|
||||||
element: userData.avatar.element,
|
|
||||||
},
|
|
||||||
gender: userData.gender,
|
|
||||||
language: userData.language,
|
|
||||||
theme: userData.theme,
|
|
||||||
bahamut: userData.bahamut || false,
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized.current = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing account cookies:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getCookie } from 'cookies-next'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import * as ToggleGroup from '@radix-ui/react-toggle-group'
|
import * as ToggleGroup from '@radix-ui/react-toggle-group'
|
||||||
|
|
@ -13,10 +12,12 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ElementToggle = ({ currentElement, sendValue, ...props }: Props) => {
|
const ElementToggle = ({ currentElement, sendValue, ...props }: Props) => {
|
||||||
// Localization
|
// Router and localization
|
||||||
const locale = (getCookie('NEXT_LOCALE') as string) || 'en'
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// State: Component
|
// State: Component
|
||||||
const [element, setElement] = useState(currentElement)
|
const [element, setElement] = useState(currentElement)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
import { ResponseStatus } from '~types'
|
import { ResponseStatus } from '~types'
|
||||||
|
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
|
|
||||||
const ErrorSection = ({ status }: Props) => {
|
const ErrorSection = ({ status }: Props) => {
|
||||||
// Import translations
|
// Import translations
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
const [statusText, setStatusText] = useState('')
|
const [statusText, setStatusText] = useState('')
|
||||||
|
|
||||||
|
|
|
||||||
226
components/GridRep/index.module.scss
Normal file
226
components/GridRep/index.module.scss
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
.gridRep {
|
||||||
|
aspect-ratio: 3/2;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $card-corner;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit-2x;
|
||||||
|
min-width: 320px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--grid-rep-hover);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weaponGrid {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.weapon {
|
||||||
|
background: var(--unit-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
background: inherit;
|
||||||
|
|
||||||
|
.Grid .Weapon {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .weaponGrid {
|
||||||
|
aspect-ratio: 2/0.95;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 3.36fr; /* left column takes up 1 fraction, right column takes up 3 fractions */
|
||||||
|
grid-gap: $unit; /* add a gap of 8px between grid items */
|
||||||
|
|
||||||
|
.weapon {
|
||||||
|
background: var(--unit-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainhand.weapon {
|
||||||
|
aspect-ratio: 73/153;
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / 2; /* spans one column */
|
||||||
|
height: calc(100% - $unit-fourth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapons {
|
||||||
|
display: grid; /* make the right-images container a grid */
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
3,
|
||||||
|
1fr
|
||||||
|
); /* create 3 columns, each taking up 1 fraction */
|
||||||
|
grid-template-rows: repeat(
|
||||||
|
3,
|
||||||
|
1fr
|
||||||
|
); /* create 3 rows, each taking up 1 fraction */
|
||||||
|
gap: $unit;
|
||||||
|
// column-gap: $unit;
|
||||||
|
// row-gap: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.weapon {
|
||||||
|
aspect-ratio: 280 / 160;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainhand.weapon img[src*='jpg'],
|
||||||
|
.grid.weapon img[src*='jpg'] {
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: $font-regular;
|
||||||
|
font-weight: $bold;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 258px; // Can we not do this?
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: calc($unit / 2);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: calc($unit / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributed,
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-half;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
a.user:hover {
|
||||||
|
color: var(--link-text-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user,
|
||||||
|
.raid,
|
||||||
|
time {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: $font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties {
|
||||||
|
display: flex;
|
||||||
|
font-size: $font-small;
|
||||||
|
gap: $unit-half;
|
||||||
|
|
||||||
|
.raid {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: $unit-half;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullAuto {
|
||||||
|
color: var(--full-auto-label-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
color: var(--extra-purple-light-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoGuard {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--full-auto-label-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid {
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
gap: calc($unit / 2);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img,
|
||||||
|
.no-user {
|
||||||
|
$diameter: 18px;
|
||||||
|
|
||||||
|
border-radius: calc($diameter / 2);
|
||||||
|
height: $diameter;
|
||||||
|
width: $diameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.gran {
|
||||||
|
background-color: #cee7fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.djeeta {
|
||||||
|
background-color: #ffe1fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-user {
|
||||||
|
background: $grey-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
272
components/GridRep/index.tsx
Normal file
272
components/GridRep/index.tsx
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import 'fix-date'
|
||||||
|
|
||||||
|
import { accountState } from '~utils/accountState'
|
||||||
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
|
|
||||||
|
import Button from '~components/common/Button'
|
||||||
|
|
||||||
|
import SaveIcon from '~public/icons/Save.svg'
|
||||||
|
import ShieldIcon from '~public/icons/Shield.svg'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
shortcode: string
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
raid: Raid
|
||||||
|
grid: GridWeapon[]
|
||||||
|
user?: User
|
||||||
|
fullAuto: boolean
|
||||||
|
autoGuard: boolean
|
||||||
|
favorited: boolean
|
||||||
|
createdAt: Date
|
||||||
|
onClick: (shortcode: string) => void
|
||||||
|
onSave?: (partyId: string, favorited: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridRep = (props: Props) => {
|
||||||
|
const numWeapons: number = 9
|
||||||
|
|
||||||
|
const { account } = useSnapshot(accountState)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
const [mainhand, setMainhand] = useState<Weapon>()
|
||||||
|
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||||
|
const [grid, setGrid] = useState<GridArray<GridWeapon>>({})
|
||||||
|
|
||||||
|
const titleClass = classNames({
|
||||||
|
empty: !props.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const raidClass = classNames({
|
||||||
|
[styles.raid]: true,
|
||||||
|
[styles.empty]: !props.raid,
|
||||||
|
})
|
||||||
|
|
||||||
|
const userClass = classNames({
|
||||||
|
[styles.user]: true,
|
||||||
|
[styles.empty]: !props.user,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mainhandClasses = classNames({
|
||||||
|
[styles.weapon]: true,
|
||||||
|
[styles.mainhand]: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const weaponClasses = classNames({
|
||||||
|
[styles.weapon]: true,
|
||||||
|
[styles.grid]: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newWeapons = Array(numWeapons)
|
||||||
|
const gridWeapons = Array(numWeapons)
|
||||||
|
|
||||||
|
let foundMainhand = false
|
||||||
|
for (const [key, value] of Object.entries(props.grid)) {
|
||||||
|
if (value.position == -1) {
|
||||||
|
setMainhand(value.object)
|
||||||
|
foundMainhand = true
|
||||||
|
} else if (!value.mainhand && value.position != null) {
|
||||||
|
newWeapons[value.position] = value.object
|
||||||
|
gridWeapons[value.position] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundMainhand) {
|
||||||
|
setMainhand(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
setWeapons(newWeapons)
|
||||||
|
setGrid(gridWeapons)
|
||||||
|
}, [props.grid])
|
||||||
|
|
||||||
|
function navigate() {
|
||||||
|
props.onClick(props.shortcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMainhandImage() {
|
||||||
|
let url = ''
|
||||||
|
|
||||||
|
if (mainhand) {
|
||||||
|
const weapon = Object.values(props.grid).find(
|
||||||
|
(w) => w && w.object.id === mainhand.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (mainhand.element == 0 && weapon && weapon.element) {
|
||||||
|
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${weapon.element}.jpg`
|
||||||
|
} else {
|
||||||
|
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainhand && props.grid[0] ? (
|
||||||
|
<img alt={mainhand.name[locale]} src={url} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateGridImage(position: number) {
|
||||||
|
let url = ''
|
||||||
|
|
||||||
|
const weapon = weapons[position]
|
||||||
|
const gridWeapon = grid[position]
|
||||||
|
|
||||||
|
if (weapon && gridWeapon) {
|
||||||
|
if (weapon.element == 0 && gridWeapon.element) {
|
||||||
|
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
|
||||||
|
} else {
|
||||||
|
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return weapons[position] ? (
|
||||||
|
<img alt={weapons[position]?.name[locale]} src={url} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSaveData() {
|
||||||
|
if (props.onSave) props.onSave(props.id, props.favorited)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userImage = () => {
|
||||||
|
if (props.user && props.user.avatar) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={props.user.avatar.picture}
|
||||||
|
className={`profile ${props.user.avatar.element}`}
|
||||||
|
srcSet={`/profile/${props.user.avatar.picture}.png,
|
||||||
|
/profile/${props.user.avatar.picture}@2x.png 2x`}
|
||||||
|
src={`/profile/${props.user.avatar.picture}.png`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={t('no_user')}
|
||||||
|
className={`profile anonymous`}
|
||||||
|
srcSet={`/profile/npc.png,
|
||||||
|
/profile/npc@2x.png 2x`}
|
||||||
|
src={`/profile/npc.png`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribution = () => (
|
||||||
|
<span className={userClass}>
|
||||||
|
{userImage()}
|
||||||
|
{props.user ? props.user.username : t('no_user')}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
function fullAutoString() {
|
||||||
|
const fullAutoElement = (
|
||||||
|
<span className={styles.fullAuto}>
|
||||||
|
{` · ${t('party.details.labels.full_auto')}`}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
const autoGuardElement = (
|
||||||
|
<span className={styles.autoGuard}>
|
||||||
|
<ShieldIcon />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.auto}>
|
||||||
|
{fullAutoElement}
|
||||||
|
{props.autoGuard ? autoGuardElement : ''}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsWithUsername = (
|
||||||
|
<div className={styles.details}>
|
||||||
|
<div className={styles.top}>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<h2 className={titleClass}>
|
||||||
|
{props.name ? props.name : t('no_title')}
|
||||||
|
</h2>
|
||||||
|
<div className={styles.properties}>
|
||||||
|
<span className={raidClass}>
|
||||||
|
{props.raid ? props.raid.name[locale] : t('no_raid')}
|
||||||
|
</span>
|
||||||
|
{props.fullAuto && (
|
||||||
|
<span className={styles.fullAuto}>
|
||||||
|
{` · ${t('party.details.labels.full_auto')}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{props.raid && props.raid.group.extra && (
|
||||||
|
<span className={styles.extra}>{` · EX`}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{account.authorized &&
|
||||||
|
((props.user && account.user && account.user.id !== props.user.id) ||
|
||||||
|
!props.user) ? (
|
||||||
|
<Link href="#">
|
||||||
|
<Button
|
||||||
|
className={classNames({
|
||||||
|
save: true,
|
||||||
|
saved: props.favorited,
|
||||||
|
})}
|
||||||
|
leftAccessoryIcon={<SaveIcon className="stroke" />}
|
||||||
|
active={props.favorited}
|
||||||
|
bound={true}
|
||||||
|
size="small"
|
||||||
|
onClick={sendSaveData}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.attributed}>
|
||||||
|
{attribution()}
|
||||||
|
|
||||||
|
<time
|
||||||
|
className={styles.lastUpdated}
|
||||||
|
dateTime={props.createdAt.toISOString()}
|
||||||
|
>
|
||||||
|
{formatTimeAgo(props.createdAt, locale)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/p/${props.shortcode}`}>
|
||||||
|
<a className={styles.gridRep}>
|
||||||
|
{detailsWithUsername}
|
||||||
|
<div className={styles.weaponGrid}>
|
||||||
|
<div className={mainhandClasses}>{generateMainhandImage()}</div>
|
||||||
|
|
||||||
|
<ul className={styles.weapons}>
|
||||||
|
{Array.from(Array(numWeapons)).map((x, i) => {
|
||||||
|
return (
|
||||||
|
<li key={`${props.shortcode}-${i}`} className={weaponClasses}>
|
||||||
|
{generateGridImage(i)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GridRep
|
||||||
|
|
@ -1,31 +1,3 @@
|
||||||
.bahamut {
|
|
||||||
$negative-margin: $unit * -2;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
background: #2b4683;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
gap: $unit;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: $bold;
|
|
||||||
padding: $unit-2x;
|
|
||||||
margin-top: $negative-margin;
|
|
||||||
margin-left: $negative-margin;
|
|
||||||
margin-right: $negative-margin;
|
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.2em;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
'use client'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { deleteCookie, getCookie } from 'cookies-next'
|
import { deleteCookie } from 'cookies-next'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useRouter } from 'next/router'
|
||||||
import { useRouter } from '~/i18n/navigation'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
@ -28,7 +26,6 @@ import AccountModal from '~components/auth/AccountModal'
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
import Tooltip from '~components/common/Tooltip'
|
import Tooltip from '~components/common/Tooltip'
|
||||||
|
|
||||||
import BahamutIcon from '~public/icons/Bahamut.svg'
|
|
||||||
import ChevronIcon from '~public/icons/Chevron.svg'
|
import ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
import MenuIcon from '~public/icons/Menu.svg'
|
import MenuIcon from '~public/icons/Menu.svg'
|
||||||
import PlusIcon from '~public/icons/Add.svg'
|
import PlusIcon from '~public/icons/Add.svg'
|
||||||
|
|
@ -37,14 +34,12 @@ import styles from './index.module.scss'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
// Localization
|
// Localization
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Router
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
// Locale
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
const locale = (getCookie('NEXT_LOCALE') as string) || 'en'
|
|
||||||
|
|
||||||
// Subscribe to account state changes
|
|
||||||
const accountSnap = useSnapshot(accountState)
|
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [alertOpen, setAlertOpen] = useState(false)
|
const [alertOpen, setAlertOpen] = useState(false)
|
||||||
|
|
@ -101,7 +96,7 @@ const Header = () => {
|
||||||
if (key !== 'language') accountState[key] = resetState[key]
|
if (key !== 'language') accountState[key] = resetState[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
router.refresh()
|
router.reload()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,13 +108,13 @@ const Header = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Push the root URL
|
// Push the root URL
|
||||||
router.push('/new')
|
router.push('/new', undefined, { shallow: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Rendering
|
// Methods: Rendering
|
||||||
const profileImage = () => {
|
const profileImage = () => {
|
||||||
const user = accountSnap.account.user
|
const user = accountState.account.user
|
||||||
if (accountSnap.account.authorized && user) {
|
if (accountState.account.authorized && user) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
alt={user.username}
|
alt={user.username}
|
||||||
|
|
@ -132,7 +127,7 @@ const Header = () => {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
alt={t('header.anonymous')}
|
alt={t('no_user')}
|
||||||
className={`profile anonymous`}
|
className={`profile anonymous`}
|
||||||
srcSet={`/profile/npc.png,
|
srcSet={`/profile/npc.png,
|
||||||
/profile/npc@2x.png 2x`}
|
/profile/npc@2x.png 2x`}
|
||||||
|
|
@ -169,20 +164,14 @@ const Header = () => {
|
||||||
|
|
||||||
const settingsModal = (
|
const settingsModal = (
|
||||||
<>
|
<>
|
||||||
{accountSnap.account.user && (
|
{accountState.account.user && (
|
||||||
<AccountModal
|
<AccountModal
|
||||||
open={settingsModalOpen}
|
open={settingsModalOpen}
|
||||||
username={accountSnap.account.user.username}
|
username={accountState.account.user.username}
|
||||||
picture={accountSnap.account.user.avatar.picture}
|
picture={accountState.account.user.avatar.picture}
|
||||||
gender={accountSnap.account.user.gender}
|
gender={accountState.account.user.gender}
|
||||||
language={accountSnap.account.user.language}
|
language={accountState.account.user.language}
|
||||||
theme={accountSnap.account.user.theme}
|
theme={accountState.account.user.theme}
|
||||||
role={accountSnap.account.user.role}
|
|
||||||
bahamutMode={
|
|
||||||
accountSnap.account.user.role === 9
|
|
||||||
? accountSnap.account.user.bahamut
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onOpenChange={setSettingsModalOpen}
|
onOpenChange={setSettingsModalOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -200,12 +189,13 @@ const Header = () => {
|
||||||
// Rendering: Compositing
|
// Rendering: Compositing
|
||||||
const authorizedLeftItems = (
|
const authorizedLeftItems = (
|
||||||
<>
|
<>
|
||||||
{accountSnap.account.user && (
|
{accountState.account.user && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={closeLeftMenu}>
|
<DropdownMenuItem onClick={closeLeftMenu}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${accountSnap.account.user.username}` || ''}
|
href={`/${accountState.account.user.username}` || ''}
|
||||||
|
passHref
|
||||||
>
|
>
|
||||||
<span>{t('menu.profile')}</span>
|
<span>{t('menu.profile')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -220,8 +210,8 @@ const Header = () => {
|
||||||
)
|
)
|
||||||
const leftMenuItems = (
|
const leftMenuItems = (
|
||||||
<>
|
<>
|
||||||
{accountSnap.account.authorized &&
|
{accountState.account.authorized &&
|
||||||
accountSnap.account.user &&
|
accountState.account.user &&
|
||||||
authorizedLeftItems}
|
authorizedLeftItems}
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
|
@ -292,15 +282,16 @@ const Header = () => {
|
||||||
|
|
||||||
const authorizedRightItems = (
|
const authorizedRightItems = (
|
||||||
<>
|
<>
|
||||||
{accountSnap.account.user && (
|
{accountState.account.user && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{`@${accountSnap.account.user.username}`}
|
{`@${accountState.account.user.username}`}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuItem onClick={closeRightMenu}>
|
<DropdownMenuItem onClick={closeRightMenu}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${accountSnap.account.user.username}` || ''}
|
href={`/${accountState.account.user.username}` || ''}
|
||||||
|
passHref
|
||||||
>
|
>
|
||||||
<span>{t('menu.profile')}</span>
|
<span>{t('menu.profile')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -353,7 +344,7 @@ const Header = () => {
|
||||||
|
|
||||||
const rightMenuItems = (
|
const rightMenuItems = (
|
||||||
<>
|
<>
|
||||||
{accountSnap.account.authorized && accountSnap.account.user
|
{accountState.account.authorized && accountState.account.user
|
||||||
? authorizedRightItems
|
? authorizedRightItems
|
||||||
: unauthorizedRightItems}
|
: unauthorizedRightItems}
|
||||||
</>
|
</>
|
||||||
|
|
@ -384,22 +375,14 @@ const Header = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<nav className={styles.header}>
|
||||||
{accountSnap.account.user?.bahamut && (
|
{left}
|
||||||
<div className={styles.bahamut}>
|
{right}
|
||||||
<BahamutIcon />
|
{logoutConfirmationAlert}
|
||||||
<p>Bahamut Mode is active</p>
|
{settingsModal}
|
||||||
</div>
|
{loginModal}
|
||||||
)}
|
{signupModal}
|
||||||
<nav className={styles.header}>
|
</nav>
|
||||||
{left}
|
|
||||||
{right}
|
|
||||||
{logoutConfirmationAlert}
|
|
||||||
{settingsModal}
|
|
||||||
{loginModal}
|
|
||||||
{signupModal}
|
|
||||||
</nav>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,19 +41,17 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
gap: $unit * 2;
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
.icons {
|
.icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-grow: 0;
|
flex-grow: 1;
|
||||||
gap: $unit-half;
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
.proficiencies {
|
.UncapIndicator {
|
||||||
display: flex;
|
min-width: 100px;
|
||||||
gap: $unit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
'use client'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
|
|
||||||
import UncapIndicator from '~components/uncap/UncapIndicator'
|
import UncapIndicator from '~components/uncap/UncapIndicator'
|
||||||
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
|
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
gridObject: GridCharacter | GridSummon | GridWeapon
|
gridObject: GridCharacter | GridSummon | GridWeapon
|
||||||
|
|
@ -29,7 +27,9 @@ const Proficiency = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
||||||
const locale = (getCookie('NEXT_LOCALE') as string) || 'en'
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
const overlay = () => {
|
const overlay = () => {
|
||||||
if (type === 'character') {
|
if (type === 'character') {
|
||||||
|
|
@ -76,7 +76,7 @@ const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
||||||
) {
|
) {
|
||||||
suffix = '_02'
|
suffix = '_02'
|
||||||
} else if (
|
} else if (
|
||||||
gridSummon.object.uncap.transcendence &&
|
gridSummon.object.uncap.xlb &&
|
||||||
gridSummon.transcendence_step > 0
|
gridSummon.transcendence_step > 0
|
||||||
) {
|
) {
|
||||||
suffix = '_03'
|
suffix = '_03'
|
||||||
|
|
@ -107,61 +107,6 @@ const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const summonProficiency = (
|
|
||||||
<div className={styles.icons}>
|
|
||||||
<WeaponLabelIcon labelType={Element[object.element]} size="small" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const weaponProficiency = (
|
|
||||||
<div className={styles.icons}>
|
|
||||||
<WeaponLabelIcon labelType={Element[object.element]} size="small" />
|
|
||||||
{'proficiency' in object && !Array.isArray(object.proficiency) && (
|
|
||||||
<WeaponLabelIcon
|
|
||||||
labelType={Proficiency[object.proficiency]}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const characterProficiency = (
|
|
||||||
<div
|
|
||||||
className={classNames({
|
|
||||||
[styles.icons]: true,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<WeaponLabelIcon labelType={Element[object.element]} size="small" />
|
|
||||||
|
|
||||||
{'proficiency' in object && Array.isArray(object.proficiency) && (
|
|
||||||
<WeaponLabelIcon
|
|
||||||
labelType={Proficiency[object.proficiency[0]]}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{'proficiency' in object &&
|
|
||||||
Array.isArray(object.proficiency) &&
|
|
||||||
object.proficiency.length > 1 && (
|
|
||||||
<WeaponLabelIcon
|
|
||||||
labelType={Proficiency[object.proficiency[1]]}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
function proficiency() {
|
|
||||||
switch (type) {
|
|
||||||
case 'character':
|
|
||||||
return characterProficiency
|
|
||||||
case 'summon':
|
|
||||||
return summonProficiency
|
|
||||||
case 'weapon':
|
|
||||||
return weaponProficiency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.root}>
|
<header className={styles.root}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
|
|
@ -172,9 +117,21 @@ const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.subInfo}>
|
<div className={styles.subInfo}>
|
||||||
{proficiency()}
|
<div className={styles.icons}>
|
||||||
|
<WeaponLabelIcon labelType={Element[object.element]} />
|
||||||
|
{'proficiency' in object && Array.isArray(object.proficiency) && (
|
||||||
|
<WeaponLabelIcon labelType={Proficiency[object.proficiency[0]]} />
|
||||||
|
)}
|
||||||
|
{'proficiency' in object && !Array.isArray(object.proficiency) && (
|
||||||
|
<WeaponLabelIcon labelType={Proficiency[object.proficiency]} />
|
||||||
|
)}
|
||||||
|
{'proficiency' in object &&
|
||||||
|
Array.isArray(object.proficiency) &&
|
||||||
|
object.proficiency.length > 1 && (
|
||||||
|
<WeaponLabelIcon labelType={Proficiency[object.proficiency[1]]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<UncapIndicator
|
<UncapIndicator
|
||||||
className="hovercard"
|
|
||||||
type={type}
|
type={type}
|
||||||
ulb={object.uncap.ulb || false}
|
ulb={object.uncap.ulb || false}
|
||||||
flb={object.uncap.flb || false}
|
flb={object.uncap.flb || false}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
'use client'
|
|
||||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import { setCookie } from 'cookies-next'
|
import { setCookie } from 'cookies-next'
|
||||||
import { retrieveLocaleCookies } from '~utils/retrieveCookies'
|
import { retrieveLocaleCookies } from '~utils/retrieveCookies'
|
||||||
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
||||||
|
|
@ -16,7 +14,6 @@ export const LanguageSwitch = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
) {
|
) {
|
||||||
// Router and locale data
|
// Router and locale data
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
|
||||||
const localeData = retrieveLocaleCookies()
|
const localeData = retrieveLocaleCookies()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
@ -33,7 +30,7 @@ export const LanguageSwitch = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
expiresAt.setDate(expiresAt.getDate() + 120)
|
expiresAt.setDate(expiresAt.getDate() + 120)
|
||||||
|
|
||||||
setCookie('NEXT_LOCALE', language, { path: '/', expires: expiresAt })
|
setCookie('NEXT_LOCALE', language, { path: '/', expires: expiresAt })
|
||||||
router.refresh()
|
router.push(router.asPath, undefined, { locale: language })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
'use client'
|
|
||||||
import { PropsWithChildren, useEffect, useState } from 'react'
|
import { PropsWithChildren, useEffect, useState } from 'react'
|
||||||
import { usePathname } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { add, format } from 'date-fns'
|
import { add, format } from 'date-fns'
|
||||||
import { getCookie } from 'cookies-next'
|
import { getCookie } from 'cookies-next'
|
||||||
|
|
||||||
|
|
@ -12,7 +11,7 @@ import UpdateToast from '~components/toasts/UpdateToast'
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
const Layout = ({ children }: PropsWithChildren<Props>) => {
|
const Layout = ({ children }: PropsWithChildren<Props>) => {
|
||||||
const pathname = usePathname()
|
const router = useRouter()
|
||||||
const [updateToastOpen, setUpdateToastOpen] = useState(false)
|
const [updateToastOpen, setUpdateToastOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -49,7 +48,7 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateToast = () => {
|
const updateToast = () => {
|
||||||
const path = pathname?.replaceAll('/', '') || ''
|
const path = router.asPath.replaceAll('/', '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!['about', 'updates', 'roadmap'].includes(path) &&
|
!['about', 'updates', 'roadmap'].includes(path) &&
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
'use client'
|
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { getCookie } from 'cookies-next'
|
import { useRouter } from 'next/router'
|
||||||
import { SuggestionProps } from '@tiptap/suggestion'
|
import { SuggestionProps } from '@tiptap/suggestion'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
type Props = Pick<SuggestionProps, 'items' | 'command' | 'query'>
|
type Props = Pick<SuggestionProps, 'items' | 'command'>
|
||||||
|
|
||||||
export type MentionRef = {
|
export type MentionRef = {
|
||||||
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
||||||
|
|
@ -35,9 +34,10 @@ interface MentionProps extends SuggestionProps {
|
||||||
|
|
||||||
export const MentionList = forwardRef<MentionRef, Props>(
|
export const MentionList = forwardRef<MentionRef, Props>(
|
||||||
({ items, ...props }: Props, forwardedRef) => {
|
({ items, ...props }: Props, forwardedRef) => {
|
||||||
const locale = (getCookie('NEXT_LOCALE') as string) || 'en'
|
const router = useRouter()
|
||||||
|
const locale = router.locale || 'en'
|
||||||
|
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
|
||||||
|
|
@ -113,9 +113,7 @@ export const MentionList = forwardRef<MentionRef, Props>(
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.noResult}>
|
<div className={styles.noResult}>
|
||||||
{props.query.length < 3
|
{t('search.errors.no_results_generic')}
|
||||||
? t('search.errors.type')
|
|
||||||
: t('search.errors.no_results_generic')}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: string
|
page: string
|
||||||
|
|
@ -8,7 +8,7 @@ interface Props {
|
||||||
|
|
||||||
const AboutHead = ({ page }: Props) => {
|
const AboutHead = ({ page }: Props) => {
|
||||||
// Import translations
|
// Import translations
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [currentPage, setCurrentPage] = useState('about')
|
const [currentPage, setCurrentPage] = useState('about')
|
||||||
|
|
@ -26,7 +26,7 @@ const AboutHead = ({ page }: Props) => {
|
||||||
name="description"
|
name="description"
|
||||||
content={t(`page.descriptions.${currentPage}`)}
|
content={t(`page.descriptions.${currentPage}`)}
|
||||||
/>
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/x-icon" href="/images/favicon.png" />
|
<link rel="icon" type="image/x-icon" href="/images/favicon.png" />
|
||||||
|
|
||||||
{/* OpenGraph */}
|
{/* OpenGraph */}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import LinkItem from '../LinkItem'
|
import LinkItem from '../LinkItem'
|
||||||
|
|
@ -12,8 +12,8 @@ import styles from './index.module.scss'
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
const AboutPage: React.FC<Props> = (props: Props) => {
|
const AboutPage: React.FC<Props> = (props: Props) => {
|
||||||
const common = useTranslations('common')
|
const { t: common } = useTranslation('common')
|
||||||
const about = useTranslations('about')
|
const { t: about } = useTranslation('about')
|
||||||
|
|
||||||
const classes = classNames(styles.about, 'PageContent')
|
const classes = classNames(styles.about, 'PageContent')
|
||||||
|
|
||||||
|
|
@ -22,17 +22,17 @@ const AboutPage: React.FC<Props> = (props: Props) => {
|
||||||
<h1>{common('about.segmented_control.about')}</h1>
|
<h1>{common('about.segmented_control.about')}</h1>
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
{about.rich('about.subtitle', {
|
<Trans i18nKey="about:about.subtitle">
|
||||||
gameLink: (chunks) => (
|
Granblue.team is a tool to save and share team compositions for{' '}
|
||||||
<a
|
<a
|
||||||
href="https://game.granbluefantasy.jp"
|
href="https://game.granbluefantasy.jp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{chunks}
|
Granblue Fantasy
|
||||||
</a>
|
</a>
|
||||||
)
|
, a social RPG from Cygames.
|
||||||
})}
|
</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
<p>{about('about.explanation.0')}</p>
|
<p>{about('about.explanation.0')}</p>
|
||||||
<p>{about('about.explanation.1')}</p>
|
<p>{about('about.explanation.1')}</p>
|
||||||
|
|
@ -54,52 +54,53 @@ const AboutPage: React.FC<Props> = (props: Props) => {
|
||||||
<section>
|
<section>
|
||||||
<h2>{about('about.credits.title')}</h2>
|
<h2>{about('about.credits.title')}</h2>
|
||||||
<p>
|
<p>
|
||||||
{about.rich('about.credits.maintainer', {
|
<Trans i18nKey="about:about.credits.maintainer">
|
||||||
link: (chunks) => (
|
Granblue.team was built and is maintained by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/jedmund"
|
href="https://twitter.com/jedmund"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{chunks}
|
@jedmund
|
||||||
</a>
|
</a>
|
||||||
)
|
.
|
||||||
})}
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{about.rich('about.credits.assistance', {
|
<Trans i18nKey="about:about.credits.assistance">
|
||||||
link1: (chunks) => (
|
Many thanks to{' '}
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/lalalalinna"
|
href="https://twitter.com/lalalalinna"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{chunks}
|
@lalalalinna
|
||||||
</a>
|
</a>{' '}
|
||||||
),
|
and{' '}
|
||||||
link2: (chunks) => (
|
<a
|
||||||
<a
|
href="https://twitter.com/tarngerine"
|
||||||
href="https://twitter.com/tarngerine"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noreferrer"
|
||||||
rel="noreferrer"
|
>
|
||||||
>
|
@tarngerine
|
||||||
{chunks}
|
</a>
|
||||||
</a>
|
, who both provided a lot of help and advice as I was ramping up.
|
||||||
)
|
</Trans>
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{about.rich('about.credits.support', {
|
<Trans i18nKey="about:about.credits.support">
|
||||||
link: (chunks) => (
|
Many thanks also go to everyone in{' '}
|
||||||
<a
|
<a
|
||||||
href="https://game.granbluefantasy.jp/#guild/detail/1190185"
|
href="https://game.granbluefantasy.jp/#guild/detail/1190185"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{chunks}
|
Fireplace
|
||||||
</a>
|
</a>{' '}
|
||||||
)
|
and the granblue-tools Discord for all of their help with with bug
|
||||||
})}
|
testing, feature requests, and moral support. (P.S. We're
|
||||||
|
recruiting!)
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -125,17 +126,17 @@ const AboutPage: React.FC<Props> = (props: Props) => {
|
||||||
<section>
|
<section>
|
||||||
<h2>{about('about.license.title')}</h2>
|
<h2>{about('about.license.title')}</h2>
|
||||||
<p>
|
<p>
|
||||||
{about.rich('about.license.license', {
|
<Trans i18nKey="about:about.license.license">
|
||||||
link: (chunks) => (
|
This app is licensed under{' '}
|
||||||
<a
|
<a
|
||||||
href="https://choosealicense.com/licenses/agpl-3.0/"
|
href="https://choosealicense.com/licenses/agpl-3.0/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{chunks}
|
GNU AGPLv3
|
||||||
</a>
|
</a>
|
||||||
)
|
.
|
||||||
})}
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<p>{about('about.license.explanation')}</p>
|
<p>{about('about.license.explanation')}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
'use client'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getCookie } from 'cookies-next'
|
import api from '~utils/api'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
type: 'character' | 'summon' | 'weapon' | 'raid' | 'job'
|
type: 'character' | 'summon' | 'weapon'
|
||||||
image?: '01' | '02' | '03' | '04'
|
image?: '01' | '02' | '03' | '04'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,49 +19,50 @@ const defaultProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangelogUnit = ({ id, type, image }: Props) => {
|
const ChangelogUnit = ({ id, type, image }: Props) => {
|
||||||
// Locale
|
// Router
|
||||||
const locale = (getCookie('NEXT_LOCALE') as string) || 'en'
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [item, setItem] = useState<Character | Weapon | Summon>()
|
const [item, setItem] = useState<Character | Weapon | Summon>()
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItem()
|
fetch()
|
||||||
}, [id, type])
|
}, [])
|
||||||
|
|
||||||
async function fetchItem() {
|
async function fetch() {
|
||||||
try {
|
switch (type) {
|
||||||
let endpoint = ''
|
case 'character':
|
||||||
|
const character = await fetchCharacter()
|
||||||
|
setItem(character.data)
|
||||||
|
break
|
||||||
|
|
||||||
switch (type) {
|
case 'weapon':
|
||||||
case 'character':
|
const weapon = await fetchWeapon()
|
||||||
endpoint = `/api/characters/${id}`
|
setItem(weapon.data)
|
||||||
break
|
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)
|
case 'summon':
|
||||||
|
const summon = await fetchSummon()
|
||||||
if (response.ok) {
|
setItem(summon.data)
|
||||||
const data = await response.json()
|
break
|
||||||
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 })
|
||||||
|
}
|
||||||
|
|
||||||
const imageUrl = () => {
|
const imageUrl = () => {
|
||||||
let src = ''
|
let src = ''
|
||||||
|
|
||||||
|
|
@ -70,20 +71,10 @@ const ChangelogUnit = ({ id, type, image }: Props) => {
|
||||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${id}_${image}.jpg`
|
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${id}_${image}.jpg`
|
||||||
break
|
break
|
||||||
case 'weapon':
|
case 'weapon':
|
||||||
src =
|
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}.jpg`
|
||||||
image === '03'
|
|
||||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}_${image}.jpg`
|
|
||||||
: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}.jpg`
|
|
||||||
break
|
break
|
||||||
case 'summon':
|
case 'summon':
|
||||||
src =
|
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${id}.jpg`
|
||||||
image === '04'
|
|
||||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${id}_${image}.jpg`
|
|
||||||
: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${id}.jpg`
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'raid':
|
|
||||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/raids/${id}.png`
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@
|
||||||
|
|
||||||
.characters,
|
.characters,
|
||||||
.weapons,
|
.weapons,
|
||||||
.summons,
|
.summons {
|
||||||
.raids {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
gap: $unit;
|
gap: $unit;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import ChangelogUnit from '~components/about/ChangelogUnit'
|
import ChangelogUnit from '~components/about/ChangelogUnit'
|
||||||
|
|
@ -17,9 +17,6 @@ interface Props {
|
||||||
event: string
|
event: string
|
||||||
newItems?: UpdateObject
|
newItems?: UpdateObject
|
||||||
uncappedItems?: UpdateObject
|
uncappedItems?: UpdateObject
|
||||||
transcendedItems?: UpdateObject
|
|
||||||
awakenedItems?: string[]
|
|
||||||
raidItems?: string[]
|
|
||||||
numNotes: number
|
numNotes: number
|
||||||
}
|
}
|
||||||
const ContentUpdate = ({
|
const ContentUpdate = ({
|
||||||
|
|
@ -28,12 +25,9 @@ const ContentUpdate = ({
|
||||||
event,
|
event,
|
||||||
newItems,
|
newItems,
|
||||||
uncappedItems,
|
uncappedItems,
|
||||||
transcendedItems,
|
|
||||||
awakenedItems,
|
|
||||||
raidItems,
|
|
||||||
numNotes,
|
numNotes,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const updates = useTranslations('updates')
|
const { t: updates } = useTranslation('updates')
|
||||||
|
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
|
|
||||||
|
|
@ -106,94 +100,6 @@ const ContentUpdate = ({
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
|
|
||||||
function transcendItemElements(key: 'character' | 'weapon' | 'summon') {
|
|
||||||
let elements: React.ReactNode[] = []
|
|
||||||
if (transcendedItems && transcendedItems[key]) {
|
|
||||||
const items = transcendedItems[key]
|
|
||||||
elements = items
|
|
||||||
? items.map((id) => {
|
|
||||||
return key === 'character' || key === 'summon' ? (
|
|
||||||
<ChangelogUnit id={id} type={key} key={id} image="04" />
|
|
||||||
) : (
|
|
||||||
<ChangelogUnit id={id} type={key} key={id} image="03" />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
return elements
|
|
||||||
}
|
|
||||||
|
|
||||||
function transcendItemSection(key: 'character' | 'weapon' | 'summon') {
|
|
||||||
let section: React.ReactNode = ''
|
|
||||||
|
|
||||||
if (transcendedItems && transcendedItems[key]) {
|
|
||||||
const items = transcendedItems[key]
|
|
||||||
section =
|
|
||||||
items && items.length > 0 ? (
|
|
||||||
<section className={styles[`${key}s`]}>
|
|
||||||
<h4>{updates(`labels.transcends.${key}s`)}</h4>
|
|
||||||
<div className={styles.items}>{transcendItemElements(key)}</div>
|
|
||||||
</section>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return section
|
|
||||||
}
|
|
||||||
|
|
||||||
function newRaidSection() {
|
|
||||||
let section: React.ReactNode = ''
|
|
||||||
|
|
||||||
if (raidItems) {
|
|
||||||
section = raidItems && raidItems.length > 0 && (
|
|
||||||
<section className={styles['raids']}>
|
|
||||||
<h4>{updates(`labels.raids`)}</h4>
|
|
||||||
<div className={styles.items}>{raidItemElements()}</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return section
|
|
||||||
}
|
|
||||||
|
|
||||||
function awakenedItemElements() {
|
|
||||||
let elements: React.ReactNode[] = []
|
|
||||||
if (awakenedItems) {
|
|
||||||
elements = awakenedItems.map((id) => {
|
|
||||||
return <ChangelogUnit id={id} type="weapon" key={id} />
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return elements
|
|
||||||
}
|
|
||||||
|
|
||||||
function awakenedItemSection() {
|
|
||||||
let section: React.ReactNode = ''
|
|
||||||
|
|
||||||
if (awakenedItems && awakenedItems.length > 0) {
|
|
||||||
section = (
|
|
||||||
<section className={styles['weapons']}>
|
|
||||||
<h4>{updates(`labels.awakened.weapons`)}</h4>
|
|
||||||
<div className={styles.items}>{awakenedItemElements()}</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return section
|
|
||||||
}
|
|
||||||
|
|
||||||
function raidItemElements() {
|
|
||||||
let elements: React.ReactNode[] = []
|
|
||||||
|
|
||||||
if (raidItems) {
|
|
||||||
elements = raidItems.map((id) => {
|
|
||||||
return <ChangelogUnit id={id} type="raid" key={id} />
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
@ -212,15 +118,10 @@ const ContentUpdate = ({
|
||||||
<div className={styles.contents}>
|
<div className={styles.contents}>
|
||||||
{newItemSection('character')}
|
{newItemSection('character')}
|
||||||
{uncapItemSection('character')}
|
{uncapItemSection('character')}
|
||||||
{transcendItemSection('character')}
|
|
||||||
{newItemSection('weapon')}
|
{newItemSection('weapon')}
|
||||||
{uncapItemSection('weapon')}
|
{uncapItemSection('weapon')}
|
||||||
{transcendItemSection('weapon')}
|
|
||||||
{newItemSection('summon')}
|
{newItemSection('summon')}
|
||||||
{uncapItemSection('summon')}
|
{uncapItemSection('summon')}
|
||||||
{transcendItemSection('summon')}
|
|
||||||
{awakenedItemSection()}
|
|
||||||
{newRaidSection()}
|
|
||||||
</div>
|
</div>
|
||||||
{numNotes > 0 ? (
|
{numNotes > 0 ? (
|
||||||
<div className={styles.notes}>
|
<div className={styles.notes}>
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.constrained.update {
|
|
||||||
max-width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.github:hover .left .icon svg {
|
&.github:hover .left .icon svg {
|
||||||
fill: var(--text-primary);
|
fill: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ComponentProps } from 'react'
|
import { ComponentProps } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import ShareIcon from '~public/icons/Share.svg'
|
import ShareIcon from '~public/icons/Share.svg'
|
||||||
|
|
@ -20,13 +21,15 @@ const LinkItem = ({ icon, title, link, className, ...props }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<a href={link} target="_blank" rel="noreferrer">
|
<Link href={link}>
|
||||||
<div className={styles.left}>
|
<a href={link} target="_blank" rel="noreferrer">
|
||||||
<i className={styles.icon}>{icon}</i>
|
<div className={styles.left}>
|
||||||
<h3>{title}</h3>
|
<i className={styles.icon}>{icon}</i>
|
||||||
</div>
|
<h3>{title}</h3>
|
||||||
<ShareIcon className={styles.shareIcon} />
|
</div>
|
||||||
</a>
|
<ShareIcon className={styles.shareIcon} />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import LinkItem from '~components/about/LinkItem'
|
import LinkItem from '~components/about/LinkItem'
|
||||||
|
|
@ -13,8 +11,8 @@ import styles from './index.module.scss'
|
||||||
const ROADMAP_ITEMS = 6
|
const ROADMAP_ITEMS = 6
|
||||||
|
|
||||||
const RoadmapPage = () => {
|
const RoadmapPage = () => {
|
||||||
const common = useTranslations('common')
|
const { t: common } = useTranslation('common')
|
||||||
const about = useTranslations('about')
|
const { t: about } = useTranslation('about')
|
||||||
|
|
||||||
const classes = classNames(styles.roadmap, 'PageContent')
|
const classes = classNames(styles.roadmap, 'PageContent')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,123 @@
|
||||||
.updates {
|
.updates {
|
||||||
.top {
|
padding-bottom: $unit-12x;
|
||||||
|
|
||||||
|
.version {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yearSelector {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
|
|
||||||
.yearButton {
|
&.content {
|
||||||
background: none;
|
.header h3 {
|
||||||
border: none;
|
color: var(--accent-yellow);
|
||||||
font-size: $font-medium;
|
}
|
||||||
font-weight: $bold;
|
}
|
||||||
font-variant-numeric: oldstyle-nums;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&.active {
|
.contents {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-4x;
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: $unit-2x;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: $bold;
|
||||||
|
margin-top: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: var(--dialog-bg);
|
||||||
|
border-radius: $input-corner;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: var(--button-bg);
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: $bold;
|
||||||
|
letter-spacing: 0.02rem;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: baseline;
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-half;
|
||||||
|
margin-bottom: $unit-2x;
|
||||||
|
|
||||||
|
h3 {
|
||||||
color: var(--accent-blue);
|
color: var(--accent-blue);
|
||||||
|
font-weight: $medium;
|
||||||
|
font-size: $font-large;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
time {
|
||||||
color: var(--accent-blue);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: inside;
|
||||||
|
gap: $unit-half;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Contents {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-4x;
|
||||||
|
|
||||||
|
&.Bare {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: inside;
|
||||||
|
gap: $unit-half;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-2x;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Bugs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
list-style-type: disc;
|
||||||
|
gap: $unit-half;
|
||||||
|
padding-left: $unit-2x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,377 @@
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import ContentUpdate2022 from '../updates/ContentUpdate2022'
|
import ContentUpdate from '~components/about/ContentUpdate'
|
||||||
import ContentUpdate2023 from '../updates/ContentUpdate2023'
|
|
||||||
import ContentUpdate2024 from '../updates/ContentUpdate2024'
|
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
const UpdatesPage = () => {
|
const UpdatesPage = () => {
|
||||||
const common = useTranslations('common')
|
const { t: common } = useTranslation('common')
|
||||||
const updates = useTranslations('updates')
|
const { t: updates } = useTranslation('updates')
|
||||||
|
|
||||||
const classes = classNames(styles.updates, 'PageContent')
|
const classes = classNames(styles.updates, 'PageContent')
|
||||||
|
|
||||||
// Default to most recent year with content (2024)
|
const versionUpdates = {
|
||||||
const [activeYear, setActiveYear] = useState(2024)
|
'1.0.0': 5,
|
||||||
const getYearButtonClass = (year: number) =>
|
'1.0.1': 4,
|
||||||
classNames({
|
'1.1.0': {
|
||||||
[styles.yearButton]: true,
|
updates: 10,
|
||||||
[styles.active]: activeYear === year,
|
bugs: 4,
|
||||||
})
|
images: [
|
||||||
|
'remix',
|
||||||
|
'unauth',
|
||||||
|
'transcendence',
|
||||||
|
'accessories',
|
||||||
|
'mastery',
|
||||||
|
'mechanics',
|
||||||
|
'rare',
|
||||||
|
'urls',
|
||||||
|
'nav',
|
||||||
|
'toasts',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'202302U2': {
|
||||||
|
updates: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Render the component based on the active year
|
function image(
|
||||||
const renderContentUpdate = () => {
|
alt: string,
|
||||||
switch (activeYear) {
|
url: string,
|
||||||
case 2022:
|
filename: string,
|
||||||
return <ContentUpdate2022 />
|
extension: string
|
||||||
case 2023:
|
) {
|
||||||
return <ContentUpdate2023 />
|
const fallback = `${url}/${filename}.${extension}`
|
||||||
case 2024:
|
|
||||||
return <ContentUpdate2024 />
|
let set = []
|
||||||
default:
|
for (let i = 1; i < 3; i++) {
|
||||||
return <div>{updates('noUpdates')}</div>
|
if (i === 1) set.push(fallback)
|
||||||
|
else set.push(`${url}/${filename}@${i}x.${extension} ${i}x`)
|
||||||
}
|
}
|
||||||
|
const sizes = set.join(', ')
|
||||||
|
|
||||||
|
return <img alt={alt} src={fallback} srcSet={sizes} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className={styles.top}>
|
<h1>{common('about.segmented_control.updates')}</h1>
|
||||||
<h1>{common('about.segmented_control.updates')}</h1>
|
<ContentUpdate
|
||||||
<div className={styles.yearSelector}>
|
version="2023-06L"
|
||||||
<button
|
dateString="2023/06/29"
|
||||||
className={getYearButtonClass(2024)}
|
event="events.legfest"
|
||||||
onClick={() => setActiveYear(2024)}
|
newItems={{
|
||||||
>
|
character: ['3040468000', '3040469000'],
|
||||||
2024
|
weapon: ['1040421900', '1040712600', '1040516000', '1030305700'],
|
||||||
</button>
|
}}
|
||||||
<button
|
/>
|
||||||
className={getYearButtonClass(2023)}
|
<ContentUpdate
|
||||||
onClick={() => setActiveYear(2023)}
|
version="2023-06F"
|
||||||
>
|
dateString="2023/06/19"
|
||||||
2023
|
event="events.flash"
|
||||||
</button>
|
newItems={{
|
||||||
<button
|
character: ['3040466000', '3040467000'],
|
||||||
className={getYearButtonClass(2022)}
|
weapon: ['1040915300', '1040815700'],
|
||||||
onClick={() => setActiveYear(2022)}
|
}}
|
||||||
>
|
/>
|
||||||
2022
|
<ContentUpdate
|
||||||
</button>
|
version="2023-06U1"
|
||||||
|
dateString="2023/06/07"
|
||||||
|
event="events.uncap"
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040169000', '3040163000'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-05L"
|
||||||
|
dateString="2023/05/31"
|
||||||
|
event="events.legfest"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040464000', '3040465000'],
|
||||||
|
weapon: ['1040116900', '1040218400', '1040712500', '1030804400'],
|
||||||
|
}}
|
||||||
|
numNotes={1}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-05F"
|
||||||
|
dateString="2023/05/20"
|
||||||
|
event="events.flash"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040463000', '3040462000'],
|
||||||
|
weapon: ['1040421800', '1040024600'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-05U"
|
||||||
|
dateString="2023/05/18"
|
||||||
|
event="events.content"
|
||||||
|
newItems={{
|
||||||
|
weapon: ['1040712400'],
|
||||||
|
}}
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040073000'],
|
||||||
|
}}
|
||||||
|
numNotes={1}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-04L"
|
||||||
|
dateString="2023/04/30"
|
||||||
|
event="events.legfest"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040460000', '3040461000'],
|
||||||
|
weapon: ['1040815500', '1040815600', '1040421700', '1030208100'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-04U"
|
||||||
|
dateString="2023/04/01"
|
||||||
|
event="events.content"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040457000'],
|
||||||
|
summon: ['2040419000'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-03L"
|
||||||
|
dateString="2023/03/31"
|
||||||
|
event="events.legfest"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040456000', '3040455000'],
|
||||||
|
weapon: ['1040316100', '1040617500'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-03U2"
|
||||||
|
dateString="2023/03/30"
|
||||||
|
event="events.content"
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040164000', '3040160000'],
|
||||||
|
}}
|
||||||
|
newItems={{
|
||||||
|
weapon: [
|
||||||
|
'1040815100',
|
||||||
|
'1040815200',
|
||||||
|
'1040815300',
|
||||||
|
'1040815400',
|
||||||
|
'1040815000',
|
||||||
|
'1040024400',
|
||||||
|
'1030609400',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
numNotes={1}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-03U"
|
||||||
|
dateString="2023/03/22"
|
||||||
|
event="events.content"
|
||||||
|
newItems={{
|
||||||
|
weapon: ['1040024300'],
|
||||||
|
}}
|
||||||
|
uncappedItems={{
|
||||||
|
weapon: [
|
||||||
|
'1040217600',
|
||||||
|
'1040312800',
|
||||||
|
'1040023200',
|
||||||
|
'1040217800',
|
||||||
|
'1040420800',
|
||||||
|
'1040213900',
|
||||||
|
'1040116200',
|
||||||
|
'1040216500',
|
||||||
|
'1040616700',
|
||||||
|
'1040420700',
|
||||||
|
'1040913000',
|
||||||
|
'1040419000',
|
||||||
|
],
|
||||||
|
summon: [
|
||||||
|
'2040398000',
|
||||||
|
'2040413000',
|
||||||
|
'2040401000',
|
||||||
|
'2040406000',
|
||||||
|
'2040418000',
|
||||||
|
'2040409000',
|
||||||
|
'2040056000',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
numNotes={2}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-03F"
|
||||||
|
dateString="2023/03/16"
|
||||||
|
event="events.flash"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040451000', '3040452000', '3040453000', '3040454000'],
|
||||||
|
weapon: ['1040914600', '1040116800', '1040515900', '1040712300'],
|
||||||
|
}}
|
||||||
|
numNotes={7}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-02L"
|
||||||
|
dateString="2023/02/27"
|
||||||
|
event="events.legfest"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040450000', '3040449000'],
|
||||||
|
weapon: ['1040421600', '1040617300', '1040712200'],
|
||||||
|
summon: ['2040418000'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-02F"
|
||||||
|
dateString="2023/02/14"
|
||||||
|
event="events.flash"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040447000', '3040448000'],
|
||||||
|
weapon: ['1040617200', '1040421500'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-02-U3"
|
||||||
|
dateString="2023/02/12"
|
||||||
|
event="events.uncap"
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040173000'],
|
||||||
|
weapon: ['1040606800', '1040606900', '1040607000', '1040509500'],
|
||||||
|
summon: ['2040288000'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-02-U2"
|
||||||
|
dateString="2023/02/06"
|
||||||
|
event="events.uncap"
|
||||||
|
newItems={{
|
||||||
|
weapon: ['1040016100'],
|
||||||
|
}}
|
||||||
|
numNotes={versionUpdates['202302U2'].updates}
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040252000'],
|
||||||
|
weapon: ['1040617100', '1040016100'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<section className={styles.version} data-version="1.1">
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3>1.1.0</h3>
|
||||||
|
<time>2023/02/06</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={styles.contents}>
|
||||||
{renderContentUpdate()}
|
<section>
|
||||||
|
<h2>{updates('labels.features')}</h2>
|
||||||
|
<ul className={styles.features}>
|
||||||
|
{[...Array(versionUpdates['1.1.0'].updates)].map((e, i) => (
|
||||||
|
<li key={`1.1.0-update-${i}`}>
|
||||||
|
{image(
|
||||||
|
updates(`versions.1.1.0.features.${i}.title`),
|
||||||
|
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/updates`,
|
||||||
|
versionUpdates['1.1.0'].images[i],
|
||||||
|
'jpg'
|
||||||
|
)}
|
||||||
|
<h3>{updates(`versions.1.1.0.features.${i}.title`)}</h3>
|
||||||
|
<p>{updates(`versions.1.1.0.features.${i}.blurb`)}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Bug fixes</h2>
|
||||||
|
<ul className={styles.bugs}>
|
||||||
|
{[...Array(versionUpdates['1.1.0'].bugs)].map((e, i) => (
|
||||||
|
<li key={`1.1.0-bugfix-${i}`}>
|
||||||
|
{updates(`versions.1.1.0.bugs.${i}`)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-02-U1"
|
||||||
|
dateString="2023/02/01"
|
||||||
|
event="events.uncap"
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040136000', '3040219000'],
|
||||||
|
weapon: ['1040412800', '1040511300'],
|
||||||
|
summon: ['2040234000', '2040331000'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-01F"
|
||||||
|
dateString="2023/01/31"
|
||||||
|
event={'events.legfest'}
|
||||||
|
newItems={{
|
||||||
|
character: ['3040445000', '3040446000'],
|
||||||
|
weapon: ['1040116700', '1040421400', '1040316000', '1030208000'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-01F"
|
||||||
|
dateString="2023/01/19"
|
||||||
|
event="events.flash"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040444000', '3040443000'],
|
||||||
|
weapon: ['1040218300', '1040116600'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2023-01U"
|
||||||
|
dateString="2023/01/06"
|
||||||
|
event="events.uncap"
|
||||||
|
uncappedItems={{
|
||||||
|
character: ['3040196000'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<section className={styles.version} data-version="1.0">
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3>1.0.1</h3>
|
||||||
|
<time>2023/01/08</time>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{[...Array(versionUpdates['1.0.1'])].map((e, i) => (
|
||||||
|
<li key={`1.0.1-update-${i}`}>
|
||||||
|
{updates(`versions.1.0.1.features.${i}`)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2022-12L"
|
||||||
|
dateString="2022/12/26"
|
||||||
|
event="events.legfest"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040440000', '3040441000', '3040442000'],
|
||||||
|
weapon: ['1040315900', '1040914500', '1040218200'],
|
||||||
|
summon: ['2040417000'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<ContentUpdate
|
||||||
|
version="2022-12F2"
|
||||||
|
dateString="2022/12/26"
|
||||||
|
event="events.flash"
|
||||||
|
newItems={{
|
||||||
|
character: ['3040438000', '3040439000'],
|
||||||
|
weapon: ['1040024200', '1040116500'],
|
||||||
|
}}
|
||||||
|
numNotes={0}
|
||||||
|
/>
|
||||||
|
<section className={styles.version} data-version="1.0">
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3>1.0.0</h3>
|
||||||
|
<time>2022/12/26</time>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{[...Array(versionUpdates['1.0.0'])].map((e, i) => (
|
||||||
|
<li key={`1.0.0-update-${i}`}>
|
||||||
|
{updates(`versions.1.0.0.features.${i}`)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
.updates {
|
|
||||||
padding-bottom: $unit-12x;
|
|
||||||
|
|
||||||
.version {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.content {
|
|
||||||
.header h3 {
|
|
||||||
color: var(--accent-yellow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bugs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
list-style-type: disc;
|
|
||||||
gap: $unit-half;
|
|
||||||
padding-left: $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contents {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-4x;
|
|
||||||
|
|
||||||
.foreword {
|
|
||||||
margin-top: $unit-4x;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: $font-regular;
|
|
||||||
line-height: 1.32;
|
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-weight: $bold;
|
|
||||||
margin-top: $unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
background: var(--dialog-bg);
|
|
||||||
border-radius: $input-corner;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: var(--button-bg);
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: $bold;
|
|
||||||
letter-spacing: 0.02rem;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
align-items: baseline;
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-half;
|
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: var(--accent-blue);
|
|
||||||
font-weight: $medium;
|
|
||||||
font-size: $font-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
time {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: $font-small;
|
|
||||||
font-weight: $medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--text-primary);
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
gap: $unit-half;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Contents {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-4x;
|
|
||||||
|
|
||||||
&.Bare {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--text-primary);
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
gap: $unit-half;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import ContentUpdate from '~components/about/ContentUpdate'
|
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
|
||||||
|
|
||||||
const ContentUpdate2022 = () => {
|
|
||||||
const updates = useTranslations('updates')
|
|
||||||
|
|
||||||
const versionUpdates = {
|
|
||||||
'1.0.0': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2022-12L"
|
|
||||||
dateString="2022/12/26"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040440000', '3040441000', '3040442000'],
|
|
||||||
weapon: ['1040315900', '1040914500', '1040218200'],
|
|
||||||
summon: ['2040417000'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2022-12F2"
|
|
||||||
dateString="2022/12/26"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040438000', '3040439000'],
|
|
||||||
weapon: ['1040024200', '1040116500'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<section className={styles.version} data-version="1.0">
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h3>1.0.0</h3>
|
|
||||||
<time>2022/12/26</time>
|
|
||||||
</div>
|
|
||||||
<ul className={styles.list}>
|
|
||||||
{[...Array(versionUpdates['1.0.0'])].map((e, i) => (
|
|
||||||
<li key={`1.0.0-update-${i}`}>
|
|
||||||
{updates(`versions.v1_0_0.features.${i}`)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContentUpdate2022
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
.updates {
|
|
||||||
padding-bottom: $unit-12x;
|
|
||||||
|
|
||||||
.version {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.content {
|
|
||||||
.header h3 {
|
|
||||||
color: var(--accent-yellow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bugs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
list-style-type: disc;
|
|
||||||
gap: $unit-half;
|
|
||||||
padding-left: $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contents {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-4x;
|
|
||||||
|
|
||||||
.foreword {
|
|
||||||
margin-top: $unit-4x;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: $font-regular;
|
|
||||||
line-height: 1.32;
|
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-weight: $bold;
|
|
||||||
margin-top: $unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
background: var(--dialog-bg);
|
|
||||||
border-radius: $input-corner;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: var(--button-bg);
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: $bold;
|
|
||||||
letter-spacing: 0.02rem;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
align-items: baseline;
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-half;
|
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: var(--accent-blue);
|
|
||||||
font-weight: $medium;
|
|
||||||
font-size: $font-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
time {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: $font-small;
|
|
||||||
font-weight: $medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--text-primary);
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
gap: $unit-half;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Contents {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-4x;
|
|
||||||
|
|
||||||
&.Bare {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--text-primary);
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
gap: $unit-half;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,683 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import ContentUpdate from '~components/about/ContentUpdate'
|
|
||||||
import LinkItem from '../../LinkItem'
|
|
||||||
import DiscordIcon from '~public/icons/discord.svg'
|
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
|
||||||
|
|
||||||
const ContentUpdate2023 = () => {
|
|
||||||
const updates = useTranslations('updates')
|
|
||||||
|
|
||||||
const versionUpdates = {
|
|
||||||
'1.0.1': 4,
|
|
||||||
'1.1.0': {
|
|
||||||
updates: 10,
|
|
||||||
bugs: 4,
|
|
||||||
images: [
|
|
||||||
'remix',
|
|
||||||
'unauth',
|
|
||||||
'transcendence',
|
|
||||||
'accessories',
|
|
||||||
'mastery',
|
|
||||||
'mechanics',
|
|
||||||
'rare',
|
|
||||||
'urls',
|
|
||||||
'nav',
|
|
||||||
'toasts',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'1.2.0': {
|
|
||||||
updates: 10,
|
|
||||||
bugs: 0,
|
|
||||||
images: [
|
|
||||||
'party-peek',
|
|
||||||
'party-redesign',
|
|
||||||
'visibility',
|
|
||||||
'rich-text',
|
|
||||||
'mentions',
|
|
||||||
'include-exclude',
|
|
||||||
'raid-search',
|
|
||||||
'search-views',
|
|
||||||
'quick-summon',
|
|
||||||
'grand-awakening',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'202302U2': {
|
|
||||||
updates: 1,
|
|
||||||
},
|
|
||||||
'1.2.1': {
|
|
||||||
bugs: 5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function image(
|
|
||||||
alt: string,
|
|
||||||
url: string,
|
|
||||||
filename: string,
|
|
||||||
extension: string
|
|
||||||
) {
|
|
||||||
const fallback = `${url}/${filename}.${extension}`
|
|
||||||
|
|
||||||
let set = []
|
|
||||||
for (let i = 1; i < 3; i++) {
|
|
||||||
if (i === 1) set.push(fallback)
|
|
||||||
else set.push(`${url}/${filename}@${i}x.${extension} ${i}x`)
|
|
||||||
}
|
|
||||||
const sizes = set.join(', ')
|
|
||||||
|
|
||||||
return <img alt={alt} src={fallback} srcSet={sizes} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-12L"
|
|
||||||
dateString="2023/12/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040119000', '1040618200', '1040317000'],
|
|
||||||
character: ['3040502000', '3040501000', '3040503000'],
|
|
||||||
summon: ['2040425000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-12F2"
|
|
||||||
dateString="2023/12/28"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040218900', '1040618100', '1040025500', '1030305900'],
|
|
||||||
character: ['3040499000', '3040500000'],
|
|
||||||
summon: ['2040427000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-12U2"
|
|
||||||
dateString="2023/12/19"
|
|
||||||
event="events.content"
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040815100',
|
|
||||||
'1040815200',
|
|
||||||
'1040815300',
|
|
||||||
'1040815400',
|
|
||||||
'1040815000',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={2}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-12F"
|
|
||||||
dateString="2023/12/17"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040218800', '1040816200'],
|
|
||||||
character: ['3040498000', '3040497000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: ['1040416500', '1040215000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-12U"
|
|
||||||
dateString="2023/12/07"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040118000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-11L"
|
|
||||||
dateString="2023/11/30"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040516700', '1040713100', '1040117900', '1030609500'],
|
|
||||||
character: ['3040496000', '3040495000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-11F"
|
|
||||||
dateString="2023/11/17"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040117800', '1040516600', '1040025300'],
|
|
||||||
character: ['3040492000', '3040493000', '3040494000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-11U2"
|
|
||||||
dateString="2023/11/14"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040212000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-11U"
|
|
||||||
dateString="2023/11/09"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040025200',
|
|
||||||
'1040316800',
|
|
||||||
'1040316900',
|
|
||||||
'1040025100',
|
|
||||||
'1040712900',
|
|
||||||
'1040713000',
|
|
||||||
'1040915900',
|
|
||||||
'1040617900',
|
|
||||||
'1040618000',
|
|
||||||
'1040117700',
|
|
||||||
'1040316600',
|
|
||||||
'1040316700',
|
|
||||||
'1040422300',
|
|
||||||
'1040816000',
|
|
||||||
'1040816100',
|
|
||||||
'1040916000',
|
|
||||||
'1040117500',
|
|
||||||
'1040117600',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-10L"
|
|
||||||
dateString="2023/10/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040915800', '1040117400', '1040915700', '1030804500'],
|
|
||||||
character: ['3040490000', '3040491000'],
|
|
||||||
summon: ['2040424000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-10U"
|
|
||||||
dateString="2023/10/23"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040422200',
|
|
||||||
'1040815900',
|
|
||||||
'1040316500',
|
|
||||||
'1040712800',
|
|
||||||
'1040516500',
|
|
||||||
'1040915600',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-10F"
|
|
||||||
dateString="2023/10/18"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040516400', '1040422100', '1040316400'],
|
|
||||||
character: ['3040487000', '3040488000', '3040489000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-10U"
|
|
||||||
dateString="2023/10/16"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040109000', '3040168000', '3040162000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-09L"
|
|
||||||
dateString="2023/09/30"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040915500', '1040617800', '1040117300', '1030406500'],
|
|
||||||
character: ['3040485000', '3040484000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-09F"
|
|
||||||
dateString="2023/09/15"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040117200', '1040024900'],
|
|
||||||
character: ['3040486000', '3040483000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040064000'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-09U"
|
|
||||||
dateString="2023/09/07"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040117000', '1040516300'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<section className={styles.version} data-version="1.2.1">
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h3>1.2.1</h3>
|
|
||||||
<time>2023/09/01</time>
|
|
||||||
</div>
|
|
||||||
<h2>Bug fixes</h2>
|
|
||||||
<ul className={styles.bugs}>
|
|
||||||
{[...Array(versionUpdates['1.2.1'].bugs)].map((e, i) => (
|
|
||||||
<li key={`1.2.1-bugfix-${i}`}>
|
|
||||||
{updates(`versions.v1_2_1.bugs.${i}`)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-08L"
|
|
||||||
dateString="2023/08/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040481000', '3040482000'],
|
|
||||||
weapon: ['1040218700', '1040617700', '1040712700', '1030406400'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<section className={styles.version} data-version="1.2">
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h3>1.2.0</h3>
|
|
||||||
<time>2023/08/25</time>
|
|
||||||
</div>
|
|
||||||
<div className={styles.contents}>
|
|
||||||
<section>
|
|
||||||
<h2>{updates('labels.features')}</h2>
|
|
||||||
<ul className={styles.features}>
|
|
||||||
{[...Array(versionUpdates['1.2.0'].updates)].map((e, i) => (
|
|
||||||
<li key={`1.2.0-update-${i}`}>
|
|
||||||
{image(
|
|
||||||
updates(`versions.v1_2_0.features.${i}.title`),
|
|
||||||
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/updates`,
|
|
||||||
versionUpdates['1.2.0'].images[i],
|
|
||||||
'jpg'
|
|
||||||
)}
|
|
||||||
<h3>{updates(`versions.v1_2_0.features.${i}.title`)}</h3>
|
|
||||||
<p>{updates(`versions.v1_2_0.features.${i}.blurb`)}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className={styles.foreword}>
|
|
||||||
<h2>Developer notes</h2>
|
|
||||||
{updates('versions.v1_2_0.notes')
|
|
||||||
.split('\n')
|
|
||||||
.map((item, i) => (
|
|
||||||
<p key={`note-${i}`}>{item}</p>
|
|
||||||
))}
|
|
||||||
<LinkItem
|
|
||||||
className="discord constrained update"
|
|
||||||
title="granblue-tools"
|
|
||||||
link="https://discord.gg/qyZ5hGdPC8"
|
|
||||||
icon={<DiscordIcon />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/* <section>
|
|
||||||
<h2>Bug fixes</h2>
|
|
||||||
<ul className={styles.bugs}>
|
|
||||||
{[...Array(versionUpdates['1.2.0'].bugs)].map((e, i) => (
|
|
||||||
<li key={`1.2.0-bugfix-${i}`}>
|
|
||||||
{updates(`versions.v1_2_0.bugs.${i}`)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section> */}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-08U"
|
|
||||||
dateString="2023/08/22"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
summon: ['2040185000', '2040225000', '2040205000', '2040261000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-08F"
|
|
||||||
dateString="2023/08/16"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040478000', '3040479000', '3040480000'],
|
|
||||||
weapon: ['1040915400', '1040024800', '1040422000'],
|
|
||||||
summon: ['2040423000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040161000', '3040165000'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-08U"
|
|
||||||
dateString="2023/08/11"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040476000', '3040477000'],
|
|
||||||
weapon: ['1040117100'],
|
|
||||||
summon: ['2040422000', '2040421000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-07L"
|
|
||||||
dateString="2023/07/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040472000', '3040474000', '3040475000', '3040473000'],
|
|
||||||
weapon: [
|
|
||||||
'1040815800',
|
|
||||||
'1040024700',
|
|
||||||
'1040516200',
|
|
||||||
'1040218600',
|
|
||||||
'1040617600',
|
|
||||||
'1030305800',
|
|
||||||
],
|
|
||||||
summon: ['2040420000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-07F"
|
|
||||||
dateString="2023/07/15"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040470000', '3040471000'],
|
|
||||||
weapon: ['1040316300', '1040516100'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-07U"
|
|
||||||
dateString="2023/07/08"
|
|
||||||
event="events.uncap"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040218500'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040102000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-06L"
|
|
||||||
dateString="2023/06/29"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040468000', '3040469000'],
|
|
||||||
weapon: ['1040421900', '1040712600', '1040516000', '1030305700'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-06F"
|
|
||||||
dateString="2023/06/19"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040466000', '3040467000'],
|
|
||||||
weapon: ['1040915300', '1040815700'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-06U1"
|
|
||||||
dateString="2023/06/07"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040169000', '3040163000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-05L"
|
|
||||||
dateString="2023/05/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040464000', '3040465000'],
|
|
||||||
weapon: ['1040116900', '1040218400', '1040712500', '1030804400'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-05F"
|
|
||||||
dateString="2023/05/20"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040463000', '3040462000'],
|
|
||||||
weapon: ['1040421800', '1040024600'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-05U"
|
|
||||||
dateString="2023/05/18"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040712400'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040073000'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-04L"
|
|
||||||
dateString="2023/04/30"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040460000', '3040461000'],
|
|
||||||
weapon: ['1040815500', '1040815600', '1040421700', '1030208100'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-04U"
|
|
||||||
dateString="2023/04/01"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040457000'],
|
|
||||||
summon: ['2040419000'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-03L"
|
|
||||||
dateString="2023/03/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040456000', '3040455000'],
|
|
||||||
weapon: ['1040316100', '1040617500'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-03U2"
|
|
||||||
dateString="2023/03/30"
|
|
||||||
event="events.content"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040164000', '3040160000'],
|
|
||||||
}}
|
|
||||||
newItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040815100',
|
|
||||||
'1040815200',
|
|
||||||
'1040815300',
|
|
||||||
'1040815400',
|
|
||||||
'1040815000',
|
|
||||||
'1040024400',
|
|
||||||
'1030609400',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-03U"
|
|
||||||
dateString="2023/03/22"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040024300'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040217600',
|
|
||||||
'1040312800',
|
|
||||||
'1040023200',
|
|
||||||
'1040217800',
|
|
||||||
'1040420800',
|
|
||||||
'1040213900',
|
|
||||||
'1040116200',
|
|
||||||
'1040216500',
|
|
||||||
'1040616700',
|
|
||||||
'1040420700',
|
|
||||||
'1040913000',
|
|
||||||
'1040419000',
|
|
||||||
],
|
|
||||||
summon: [
|
|
||||||
'2040398000',
|
|
||||||
'2040413000',
|
|
||||||
'2040401000',
|
|
||||||
'2040406000',
|
|
||||||
'2040418000',
|
|
||||||
'2040409000',
|
|
||||||
'2040056000',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={2}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-03F"
|
|
||||||
dateString="2023/03/16"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040451000', '3040452000', '3040453000', '3040454000'],
|
|
||||||
weapon: ['1040914600', '1040116800', '1040515900', '1040712300'],
|
|
||||||
}}
|
|
||||||
numNotes={7}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-02L"
|
|
||||||
dateString="2023/02/27"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040450000', '3040449000'],
|
|
||||||
weapon: ['1040421600', '1040617300', '1040712200'],
|
|
||||||
summon: ['2040418000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-02F"
|
|
||||||
dateString="2023/02/14"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040447000', '3040448000'],
|
|
||||||
weapon: ['1040617200', '1040421500'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-02-U3"
|
|
||||||
dateString="2023/02/12"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040173000'],
|
|
||||||
weapon: ['1040606800', '1040606900', '1040607000', '1040509500'],
|
|
||||||
summon: ['2040288000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-02-U2"
|
|
||||||
dateString="2023/02/06"
|
|
||||||
event="events.uncap"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040016100'],
|
|
||||||
}}
|
|
||||||
numNotes={versionUpdates['202302U2'].updates}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040252000'],
|
|
||||||
weapon: ['1040617100', '1040016100'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<section className={styles.version} data-version="1.1">
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h3>1.1.0</h3>
|
|
||||||
<time>2023/02/06</time>
|
|
||||||
</div>
|
|
||||||
<div className={styles.contents}>
|
|
||||||
<section>
|
|
||||||
<h2>{updates('labels.features')}</h2>
|
|
||||||
<ul className={styles.features}>
|
|
||||||
{[...Array(versionUpdates['1.1.0'].updates)].map((e, i) => (
|
|
||||||
<li key={`1.1.0-update-${i}`}>
|
|
||||||
{image(
|
|
||||||
updates(`versions.v1_1_0.features.${i}.title`),
|
|
||||||
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/updates`,
|
|
||||||
versionUpdates['1.1.0'].images[i],
|
|
||||||
'jpg'
|
|
||||||
)}
|
|
||||||
<h3>{updates(`versions.v1_1_0.features.${i}.title`)}</h3>
|
|
||||||
<p>{updates(`versions.v1_1_0.features.${i}.blurb`)}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Bug fixes</h2>
|
|
||||||
<ul className={styles.bugs}>
|
|
||||||
{[...Array(versionUpdates['1.1.0'].bugs)].map((e, i) => (
|
|
||||||
<li key={`1.1.0-bugfix-${i}`}>
|
|
||||||
{updates(`versions.v1_1_0.bugs.${i}`)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-02-U1"
|
|
||||||
dateString="2023/02/01"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040136000', '3040219000'],
|
|
||||||
weapon: ['1040412800', '1040511300'],
|
|
||||||
summon: ['2040234000', '2040331000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-01F"
|
|
||||||
dateString="2023/01/31"
|
|
||||||
event={'events.legfest'}
|
|
||||||
newItems={{
|
|
||||||
character: ['3040445000', '3040446000'],
|
|
||||||
weapon: ['1040116700', '1040421400', '1040316000', '1030208000'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-01F"
|
|
||||||
dateString="2023/01/19"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040444000', '3040443000'],
|
|
||||||
weapon: ['1040218300', '1040116600'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2023-01U"
|
|
||||||
dateString="2023/01/06"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040196000'],
|
|
||||||
}}
|
|
||||||
numNotes={0}
|
|
||||||
/>
|
|
||||||
<section className={styles.version} data-version="1.0">
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h3>1.0.1</h3>
|
|
||||||
<time>2023/01/08</time>
|
|
||||||
</div>
|
|
||||||
<ul className={styles.list}>
|
|
||||||
{[...Array(versionUpdates['1.0.1'])].map((e, i) => (
|
|
||||||
<li key={`1.0.1-update-${i}`}>
|
|
||||||
{updates(`versions.v1_0_1.features.${i}`)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContentUpdate2023
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import ContentUpdate from '~components/about/ContentUpdate'
|
|
||||||
|
|
||||||
const ContentUpdate2024 = () => {
|
|
||||||
const updates = useTranslations('updates')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-05U1"
|
|
||||||
dateString="2024/05/02"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040026100',
|
|
||||||
'1040317400',
|
|
||||||
'1040423100',
|
|
||||||
'1040119500',
|
|
||||||
'1040618800',
|
|
||||||
'1040916300',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
transcendedItems={{
|
|
||||||
summon: ['2040034000', '2040046000'],
|
|
||||||
}}
|
|
||||||
numNotes={4}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-04L"
|
|
||||||
dateString="2024/04/30"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040529000', '3040530000'],
|
|
||||||
weapon: ['1040219200', '1040119400', '1040618700', '1030109000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-04U2"
|
|
||||||
dateString="2024/04/21"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040525000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: ['1040313200'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-04F"
|
|
||||||
dateString="2024/04/15"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040523000', '3040524000'],
|
|
||||||
weapon: ['1040119300', '1040423000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-04U1"
|
|
||||||
dateString="2024/04/07"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040522000'],
|
|
||||||
weapon: ['1040618600'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-03L"
|
|
||||||
dateString="2024/03/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040520000', '3040521000'],
|
|
||||||
weapon: ['1040026000', '1040422900', '1040422800', '1030704700'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-03U3"
|
|
||||||
dateString="2024/03/25"
|
|
||||||
event="events.content"
|
|
||||||
transcendedItems={{
|
|
||||||
summon: ['2040020000', '2040047000'],
|
|
||||||
}}
|
|
||||||
newItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040119200',
|
|
||||||
'1040516800',
|
|
||||||
'1040713400',
|
|
||||||
'1040219100',
|
|
||||||
'1040516900',
|
|
||||||
'1040916200',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
numNotes={3}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-03F"
|
|
||||||
dateString="2024/03/19"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040317200', '1040422600', '1040422700'],
|
|
||||||
character: ['3040517000', '3040518000', '3040519000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-03U2"
|
|
||||||
dateString="2024/03/15"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040713300'],
|
|
||||||
character: ['3040516000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: ['1040614500'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-03U"
|
|
||||||
dateString="2024/03/10"
|
|
||||||
event="events.content"
|
|
||||||
transcendedItems={{
|
|
||||||
summon: [
|
|
||||||
'2040094000',
|
|
||||||
'2040100000',
|
|
||||||
'2040098000',
|
|
||||||
'2040084000',
|
|
||||||
'2040090000',
|
|
||||||
'2040080000',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040516200',
|
|
||||||
'1040915300',
|
|
||||||
'1040116500',
|
|
||||||
'1040815800',
|
|
||||||
'1040710900',
|
|
||||||
'1040024700',
|
|
||||||
'1040712600',
|
|
||||||
'1040116100',
|
|
||||||
'1040712300',
|
|
||||||
'1040806000',
|
|
||||||
'1040515900',
|
|
||||||
'1040616800',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
awakenedItems={[
|
|
||||||
'1040906400',
|
|
||||||
'1040708700',
|
|
||||||
'1040212700',
|
|
||||||
'1040910000',
|
|
||||||
'1040014300',
|
|
||||||
'1040207000',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-02L"
|
|
||||||
dateString="2024/02/29"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040515000', '3040513000', '3040514000'],
|
|
||||||
weapon: [
|
|
||||||
'1040025900',
|
|
||||||
'1040618500',
|
|
||||||
'1040119100',
|
|
||||||
'1040025800',
|
|
||||||
'1030010200',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-02U"
|
|
||||||
dateString="2024/02/20"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040618400'],
|
|
||||||
}}
|
|
||||||
raidItems={['dark-rapture-zero']}
|
|
||||||
numNotes={3}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-02F"
|
|
||||||
dateString="2024/02/14"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040512000', '3040511000'],
|
|
||||||
weapon: ['1040713200', '1040816400'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01U"
|
|
||||||
dateString="2024/02/06"
|
|
||||||
event="events.uncap"
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040190000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01L"
|
|
||||||
dateString="2024/01/31"
|
|
||||||
event="events.legfest"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040509000', '3040510000'],
|
|
||||||
weapon: ['1040025700', '1040422500', '1040317100', '1030406600'],
|
|
||||||
}}
|
|
||||||
numNotes={1}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01U3"
|
|
||||||
dateString="2024/01/18"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040506000'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040313000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01F"
|
|
||||||
dateString="2024/01/15"
|
|
||||||
event="events.flash"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040508000', '3040507000'],
|
|
||||||
weapon: ['1040422400', '1040219000'],
|
|
||||||
}}
|
|
||||||
transcendedItems={{
|
|
||||||
weapon: [
|
|
||||||
'1040212600',
|
|
||||||
'1040212500',
|
|
||||||
'1040310700',
|
|
||||||
'1040310600',
|
|
||||||
'1040415100',
|
|
||||||
'1040415000',
|
|
||||||
'1040809500',
|
|
||||||
'1040809400',
|
|
||||||
'1040911100',
|
|
||||||
'1040911000',
|
|
||||||
'1040017100',
|
|
||||||
'1040017000',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01U2"
|
|
||||||
dateString="2024/01/12"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
character: ['3040504000', '3040505000'],
|
|
||||||
weapon: ['1040618300'],
|
|
||||||
summon: ['2040426000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ContentUpdate
|
|
||||||
version="2024-01U"
|
|
||||||
dateString="2024/01/05"
|
|
||||||
event="events.content"
|
|
||||||
newItems={{
|
|
||||||
weapon: ['1040025400', '1040816300'],
|
|
||||||
}}
|
|
||||||
uncappedItems={{
|
|
||||||
character: ['3040167000', '3040166000'],
|
|
||||||
}}
|
|
||||||
numNotes={2}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContentUpdate2024
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getCookie, setCookie } from 'cookies-next'
|
import { getCookie, setCookie } from 'cookies-next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
import { Dialog } from '~components/common/Dialog'
|
import { Dialog } from '~components/common/Dialog'
|
||||||
|
|
@ -20,7 +18,6 @@ import { accountState } from '~utils/accountState'
|
||||||
import { pictureData } from '~utils/pictureData'
|
import { pictureData } from '~utils/pictureData'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
import SwitchTableField from '~components/common/SwitchTableField'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -30,19 +27,18 @@ interface Props {
|
||||||
language?: string
|
language?: string
|
||||||
theme?: string
|
theme?: string
|
||||||
private?: boolean
|
private?: boolean
|
||||||
role?: number
|
|
||||||
bahamutMode?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
function AccountModal(props: Props, forwardedRef) {
|
function AccountModal(props: Props, forwardedRef) {
|
||||||
// Localization
|
// Localization
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// In App Router, locale is handled via cookies
|
const locale =
|
||||||
const currentLocale = getCookie('NEXT_LOCALE') as string || 'en'
|
router.locale && ['en', 'ja'].includes(router.locale)
|
||||||
const locale = ['en', 'ja'].includes(currentLocale) ? currentLocale : 'en'
|
? router.locale
|
||||||
|
: 'en'
|
||||||
|
|
||||||
// useEffect only runs on the client, so now we can safely show the UI
|
// useEffect only runs on the client, so now we can safely show the UI
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
@ -57,7 +53,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
const [language, setLanguage] = useState(props.language || '')
|
const [language, setLanguage] = useState(props.language || '')
|
||||||
const [gender, setGender] = useState(props.gender || 0)
|
const [gender, setGender] = useState(props.gender || 0)
|
||||||
const [theme, setTheme] = useState(props.theme || 'system')
|
const [theme, setTheme] = useState(props.theme || 'system')
|
||||||
const [bahamutMode, setBahamutMode] = useState(props.bahamutMode || false)
|
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
const [pictureOpen, setPictureOpen] = useState(false)
|
const [pictureOpen, setPictureOpen] = useState(false)
|
||||||
|
|
@ -140,7 +135,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
language: user.language,
|
language: user.language,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
bahamut: bahamutMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = new Date()
|
const expiresAt = new Date()
|
||||||
|
|
@ -151,7 +145,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
granblueId: '',
|
granblueId: '',
|
||||||
role: user.role,
|
|
||||||
avatar: {
|
avatar: {
|
||||||
picture: user.avatar.picture,
|
picture: user.avatar.picture,
|
||||||
element: user.avatar.element,
|
element: user.avatar.element,
|
||||||
|
|
@ -159,13 +152,11 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
language: user.language,
|
language: user.language,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
bahamut: bahamutMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
if (props.onOpenChange) props.onOpenChange(false)
|
if (props.onOpenChange) props.onOpenChange(false)
|
||||||
changeLanguage(router, user.language)
|
changeLanguage(router, user.language)
|
||||||
if (props.bahamutMode != bahamutMode) router.refresh()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,15 +265,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
</SelectTableField>
|
</SelectTableField>
|
||||||
)
|
)
|
||||||
|
|
||||||
const adminField = () => (
|
|
||||||
<SwitchTableField
|
|
||||||
name="admin"
|
|
||||||
label={t('modals.settings.labels.admin')}
|
|
||||||
value={props.bahamutMode}
|
|
||||||
onValueChange={(value: boolean) => setBahamutMode(value)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -295,8 +277,8 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="Account"
|
className="Account"
|
||||||
headerRef={headerRef}
|
headerref={headerRef}
|
||||||
footerRef={footerRef}
|
footerref={footerRef}
|
||||||
onOpenAutoFocus={(event: Event) => {}}
|
onOpenAutoFocus={(event: Event) => {}}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
>
|
>
|
||||||
|
|
@ -311,7 +293,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
{genderField()}
|
{genderField()}
|
||||||
{languageField()}
|
{languageField()}
|
||||||
{themeField()}
|
{themeField()}
|
||||||
{props.role === 9 && adminField()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { setCookie } from 'cookies-next'
|
import { setCookie } from 'cookies-next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'react-i18next'
|
||||||
import axios, { AxiosError, AxiosResponse } from 'axios'
|
import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
|
@ -19,7 +17,6 @@ import DialogFooter from '~components/common/DialogFooter'
|
||||||
import DialogContent from '~components/common/DialogContent'
|
import DialogContent from '~components/common/DialogContent'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
import { userAgent } from 'next/server'
|
|
||||||
|
|
||||||
interface ErrorMap {
|
interface ErrorMap {
|
||||||
[index: string]: string
|
[index: string]: string
|
||||||
|
|
@ -37,7 +34,7 @@ interface Props {
|
||||||
|
|
||||||
const LoginModal = (props: Props) => {
|
const LoginModal = (props: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// Set up form states and error handling
|
// Set up form states and error handling
|
||||||
const [formValid, setFormValid] = useState(false)
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
|
@ -113,9 +110,9 @@ const LoginModal = (props: Props) => {
|
||||||
.login(body)
|
.login(body)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
storeCookieInfo(response)
|
storeCookieInfo(response)
|
||||||
return response.data.user.username
|
return response.data.user.id
|
||||||
})
|
})
|
||||||
.then((username) => fetchUserInfo(username))
|
.then((id) => fetchUserInfo(id))
|
||||||
.then((infoResponse) => storeUserInfo(infoResponse))
|
.then((infoResponse) => storeUserInfo(infoResponse))
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
|
|
@ -133,8 +130,8 @@ const LoginModal = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchUserInfo(username: string) {
|
function fetchUserInfo(id: string) {
|
||||||
return api.userInfo(username)
|
return api.userInfo(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeCookieInfo(response: AxiosResponse) {
|
function storeCookieInfo(response: AxiosResponse) {
|
||||||
|
|
@ -143,7 +140,6 @@ const LoginModal = (props: Props) => {
|
||||||
const cookieObj: AccountCookie = {
|
const cookieObj: AccountCookie = {
|
||||||
userId: resp.user.id,
|
userId: resp.user.id,
|
||||||
username: resp.user.username,
|
username: resp.user.username,
|
||||||
role: resp.user.role,
|
|
||||||
token: resp.access_token,
|
token: resp.access_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +169,6 @@ const LoginModal = (props: Props) => {
|
||||||
language: user.language,
|
language: user.language,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
bahamut: false,
|
|
||||||
},
|
},
|
||||||
{ path: '/', expires: expiresAt }
|
{ path: '/', expires: expiresAt }
|
||||||
)
|
)
|
||||||
|
|
@ -183,7 +178,6 @@ const LoginModal = (props: Props) => {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
granblueId: '',
|
granblueId: '',
|
||||||
role: user.role,
|
|
||||||
avatar: {
|
avatar: {
|
||||||
picture: user.avatar.picture,
|
picture: user.avatar.picture,
|
||||||
element: user.avatar.element,
|
element: user.avatar.element,
|
||||||
|
|
@ -191,7 +185,6 @@ const LoginModal = (props: Props) => {
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
language: user.language,
|
language: user.language,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
bahamut: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Authorizing account...')
|
console.log('Authorizing account...')
|
||||||
|
|
@ -225,7 +218,7 @@ const LoginModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="login"
|
className="login"
|
||||||
footerRef={footerRef}
|
footerref={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { setCookie, getCookie } from 'cookies-next'
|
import { setCookie } from 'cookies-next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
|
@ -37,7 +35,7 @@ const emailRegex =
|
||||||
|
|
||||||
const SignupModal = (props: Props) => {
|
const SignupModal = (props: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// Set up form states and error handling
|
// Set up form states and error handling
|
||||||
const [formValid, setFormValid] = useState(false)
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
|
@ -72,16 +70,13 @@ const SignupModal = (props: Props) => {
|
||||||
function register(event: React.FormEvent) {
|
function register(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
// In App Router, locale is typically handled via cookies or headers
|
|
||||||
const currentLocale = getCookie('NEXT_LOCALE') as string || 'en'
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
user: {
|
user: {
|
||||||
username: usernameInput.current?.value,
|
username: usernameInput.current?.value,
|
||||||
email: emailInput.current?.value,
|
email: emailInput.current?.value,
|
||||||
password: passwordInput.current?.value,
|
password: passwordInput.current?.value,
|
||||||
password_confirmation: passwordConfirmationInput.current?.value,
|
password_confirmation: passwordConfirmationInput.current?.value,
|
||||||
language: currentLocale,
|
language: router.locale,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,9 +85,9 @@ const SignupModal = (props: Props) => {
|
||||||
.create(body)
|
.create(body)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
storeCookieInfo(response)
|
storeCookieInfo(response)
|
||||||
return response.data.username
|
return response.data.id
|
||||||
})
|
})
|
||||||
.then((username) => fetchUserInfo(username))
|
.then((id) => fetchUserInfo(id))
|
||||||
.then((infoResponse) => storeUserInfo(infoResponse))
|
.then((infoResponse) => storeUserInfo(infoResponse))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +97,6 @@ const SignupModal = (props: Props) => {
|
||||||
const cookieObj: AccountCookie = {
|
const cookieObj: AccountCookie = {
|
||||||
userId: resp.id,
|
userId: resp.id,
|
||||||
username: resp.username,
|
username: resp.username,
|
||||||
role: resp.role,
|
|
||||||
token: resp.token,
|
token: resp.token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,8 +108,8 @@ const SignupModal = (props: Props) => {
|
||||||
setHeaders()
|
setHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchUserInfo(username: string) {
|
function fetchUserInfo(id: string) {
|
||||||
return api.userInfo(username)
|
return api.userInfo(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeUserInfo(response: AxiosResponse) {
|
function storeUserInfo(response: AxiosResponse) {
|
||||||
|
|
@ -136,7 +130,6 @@ const SignupModal = (props: Props) => {
|
||||||
language: user.language,
|
language: user.language,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
bahamut: false,
|
|
||||||
},
|
},
|
||||||
{ path: '/', expires: expiresAt }
|
{ path: '/', expires: expiresAt }
|
||||||
)
|
)
|
||||||
|
|
@ -146,7 +139,6 @@ const SignupModal = (props: Props) => {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
granblueId: '',
|
granblueId: '',
|
||||||
role: user.role,
|
|
||||||
avatar: {
|
avatar: {
|
||||||
picture: user.avatar.picture,
|
picture: user.avatar.picture,
|
||||||
element: user.avatar.element,
|
element: user.avatar.element,
|
||||||
|
|
@ -154,7 +146,6 @@ const SignupModal = (props: Props) => {
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
language: user.language,
|
language: user.language,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
bahamut: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Authorizing account...')
|
console.log('Authorizing account...')
|
||||||
|
|
@ -307,7 +298,7 @@ const SignupModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="signup"
|
className="signup"
|
||||||
footerRef={footerRef}
|
footerref={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
|
|
||||||
import { Dialog } from '~components/common/Dialog'
|
import { Dialog } from '~components/common/Dialog'
|
||||||
import DialogContent from '~components/common/DialogContent'
|
import DialogContent from '~components/common/DialogContent'
|
||||||
|
|
@ -27,12 +24,9 @@ interface Props {
|
||||||
const CharacterConflictModal = (props: Props) => {
|
const CharacterConflictModal = (props: Props) => {
|
||||||
// Localization
|
// Localization
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const { t } = useTranslation('common')
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const routerLocale = getCookie('NEXT_LOCALE')
|
|
||||||
const locale =
|
const locale =
|
||||||
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
@ -83,15 +77,13 @@ const CharacterConflictModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="conflict"
|
className="conflict"
|
||||||
footerRef={footerRef}
|
footerref={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={close}
|
onEscapeKeyDown={close}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<p>
|
<p>
|
||||||
{t.rich('modals.conflict.character', {
|
<Trans i18nKey="modals.conflict.character"></Trans>
|
||||||
strong: (chunks) => <strong>{chunks}</strong>
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.diagram}>
|
<div className={styles.diagram}>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { getCookie } from 'cookies-next'
|
import { getCookie } from 'cookies-next'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import { AxiosError, AxiosResponse } from 'axios'
|
import { AxiosError, AxiosResponse } from 'axios'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
@ -33,7 +33,7 @@ const CharacterGrid = (props: Props) => {
|
||||||
const numCharacters: number = 5
|
const numCharacters: number = 5
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// Cookies
|
// Cookies
|
||||||
const cookie = getCookie('account')
|
const cookie = getCookie('account')
|
||||||
|
|
@ -100,19 +100,13 @@ const CharacterGrid = (props: Props) => {
|
||||||
if (!party.id) {
|
if (!party.id) {
|
||||||
props.createParty().then((team) => {
|
props.createParty().then((team) => {
|
||||||
saveCharacter(team.id, character, position)
|
saveCharacter(team.id, character, position)
|
||||||
.then((response) => {
|
.then((response) => storeGridCharacter(response.data))
|
||||||
const data = response.data['grid_character']
|
|
||||||
storeGridCharacter(data)
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error))
|
.catch((error) => console.error(error))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (props.editable)
|
if (props.editable)
|
||||||
saveCharacter(party.id, character, position)
|
saveCharacter(party.id, character, position)
|
||||||
.then((response) => {
|
.then((response) => handleCharacterResponse(response.data))
|
||||||
const data = response.data['grid_character']
|
|
||||||
handleCharacterResponse(data)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const axiosError = error as AxiosError
|
const axiosError = error as AxiosError
|
||||||
const response = axiosError.response
|
const response = axiosError.response
|
||||||
|
|
@ -132,7 +126,6 @@ const CharacterGrid = (props: Props) => {
|
||||||
setPosition(data.position)
|
setPosition(data.position)
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
} else {
|
} else {
|
||||||
console.log(data)
|
|
||||||
storeGridCharacter(data)
|
storeGridCharacter(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -508,9 +501,7 @@ const CharacterGrid = (props: Props) => {
|
||||||
<Alert
|
<Alert
|
||||||
open={errorAlertOpen}
|
open={errorAlertOpen}
|
||||||
title={axiosError ? `${axiosError.status}` : 'Error'}
|
title={axiosError ? `${axiosError.status}` : 'Error'}
|
||||||
message={axiosError?.statusText && axiosError.statusText !== 'undefined'
|
message={t(`errors.${axiosError?.statusText.toLowerCase()}`)}
|
||||||
? t(`errors.${axiosError.statusText.toLowerCase()}`)
|
|
||||||
: t('errors.internal_server_error.description')}
|
|
||||||
cancelAction={() => setErrorAlertOpen(false)}
|
cancelAction={() => setErrorAlertOpen(false)}
|
||||||
cancelActionText={t('buttons.confirm')}
|
cancelActionText={t('buttons.confirm')}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Hovercard,
|
Hovercard,
|
||||||
|
|
@ -32,12 +29,9 @@ interface Props {
|
||||||
|
|
||||||
const CharacterHovercard = (props: Props) => {
|
const CharacterHovercard = (props: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const { t } = useTranslation('common')
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const t = useTranslations('common')
|
|
||||||
const routerLocale = getCookie('NEXT_LOCALE')
|
|
||||||
const locale =
|
const locale =
|
||||||
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||||
const tintElement = Element[props.gridCharacter.object.element]
|
const tintElement = Element[props.gridCharacter.object.element]
|
||||||
|
|
@ -71,11 +65,7 @@ const CharacterHovercard = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const overMasterySection = () => {
|
const overMasterySection = () => {
|
||||||
if (
|
if (props.gridCharacter && props.gridCharacter.over_mastery) {
|
||||||
props.gridCharacter &&
|
|
||||||
props.gridCharacter.over_mastery &&
|
|
||||||
props.gridCharacter.over_mastery.length > 0
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.mastery}>
|
<section className={styles.mastery}>
|
||||||
<h5 className={tintElement}>
|
<h5 className={tintElement}>
|
||||||
|
|
@ -83,13 +73,13 @@ const CharacterHovercard = (props: Props) => {
|
||||||
</h5>
|
</h5>
|
||||||
<ul>
|
<ul>
|
||||||
{[...Array(4)].map((e, i) => {
|
{[...Array(4)].map((e, i) => {
|
||||||
|
const ringIndex = i + 1
|
||||||
const ringStat: ExtendedMastery =
|
const ringStat: ExtendedMastery =
|
||||||
props.gridCharacter.over_mastery[i]
|
props.gridCharacter.over_mastery[i]
|
||||||
|
|
||||||
if (ringStat && ringStat.modifier && ringStat.modifier > 0) {
|
if (ringStat && ringStat.modifier && ringStat.modifier > 0) {
|
||||||
if (i === 0 || i === 1) {
|
if (ringIndex === 1 || ringIndex === 2) {
|
||||||
return masteryElement(overMastery.a, ringStat)
|
return masteryElement(overMastery.a, ringStat)
|
||||||
} else if (i === 2) {
|
} else if (ringIndex === 3) {
|
||||||
return masteryElement(overMastery.b, ringStat)
|
return masteryElement(overMastery.b, ringStat)
|
||||||
} else {
|
} else {
|
||||||
return masteryElement(overMastery.c, ringStat)
|
return masteryElement(overMastery.c, ringStat)
|
||||||
|
|
@ -105,9 +95,8 @@ const CharacterHovercard = (props: Props) => {
|
||||||
const aetherialMasterySection = () => {
|
const aetherialMasterySection = () => {
|
||||||
if (
|
if (
|
||||||
props.gridCharacter &&
|
props.gridCharacter &&
|
||||||
props.gridCharacter.over_mastery &&
|
|
||||||
props.gridCharacter.aetherial_mastery &&
|
props.gridCharacter.aetherial_mastery &&
|
||||||
props.gridCharacter.aetherial_mastery?.modifier > 0
|
props.gridCharacter.aetherial_mastery.modifier > 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<section className={styles.mastery}>
|
<section className={styles.mastery}>
|
||||||
|
|
@ -146,8 +135,9 @@ const CharacterHovercard = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const awakeningSection = () => {
|
const awakeningSection = () => {
|
||||||
if (props.gridCharacter.awakening) {
|
const gridAwakening = props.gridCharacter.awakening
|
||||||
const gridAwakening = props.gridCharacter.awakening
|
|
||||||
|
if (gridAwakening) {
|
||||||
return (
|
return (
|
||||||
<section className={styles.awakening}>
|
<section className={styles.awakening}>
|
||||||
<h5 className={tintElement}>
|
<h5 className={tintElement}>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
// Core dependencies
|
// Core dependencies
|
||||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
|
|
||||||
// UI dependencies
|
// UI dependencies
|
||||||
|
|
@ -47,13 +44,6 @@ interface Props {
|
||||||
updateCharacter: (object: GridCharacterObject) => Promise<any>
|
updateCharacter: (object: GridCharacterObject) => Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AWAKENING_MAP: { [key: string]: string } = {
|
|
||||||
'character-balanced': 'b1847c82-ece0-4d7a-8af1-c7868d90f34a',
|
|
||||||
'character-atk': '6e233877-8cda-4c8f-a091-3db6f68749e2',
|
|
||||||
'character-def': 'c95441de-f949-4a62-b02b-101aa2e0a638',
|
|
||||||
'character-multi': 'e36b0573-79c3-4dd2-9524-c95def4bbb1a',
|
|
||||||
}
|
|
||||||
|
|
||||||
const CharacterModal = ({
|
const CharacterModal = ({
|
||||||
gridCharacter,
|
gridCharacter,
|
||||||
children,
|
children,
|
||||||
|
|
@ -63,12 +53,9 @@ const CharacterModal = ({
|
||||||
}: PropsWithChildren<Props>) => {
|
}: PropsWithChildren<Props>) => {
|
||||||
// Router and localization
|
// Router and localization
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const routerLocale = getCookie('NEXT_LOCALE')
|
|
||||||
const locale =
|
const locale =
|
||||||
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// State: Component
|
// State: Component
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
@ -77,13 +64,16 @@ const CharacterModal = ({
|
||||||
|
|
||||||
// State: Data
|
// State: Data
|
||||||
const [perpetuity, setPerpetuity] = useState(false)
|
const [perpetuity, setPerpetuity] = useState(false)
|
||||||
const [rings, setRings] = useState<CharacterOverMastery>([])
|
const [rings, setRings] = useState<CharacterOverMastery>({
|
||||||
|
1: { ...emptyExtendedMastery, modifier: 1 },
|
||||||
|
2: { ...emptyExtendedMastery, modifier: 2 },
|
||||||
|
3: emptyExtendedMastery,
|
||||||
|
4: emptyExtendedMastery,
|
||||||
|
})
|
||||||
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
|
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
|
||||||
const [awakening, setAwakening] = useState<Awakening>()
|
const [awakening, setAwakening] = useState<Awakening>()
|
||||||
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
||||||
const [transcendenceStep, setTranscendenceStep] = useState(
|
const [transcendenceStep, setTranscendenceStep] = useState(0)
|
||||||
gridCharacter.transcendence_step
|
|
||||||
)
|
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const headerRef = React.createRef<HTMLDivElement>()
|
const headerRef = React.createRef<HTMLDivElement>()
|
||||||
|
|
@ -102,36 +92,46 @@ const CharacterModal = ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gridCharacter.awakening) {
|
setAwakening(gridCharacter.awakening.type)
|
||||||
setAwakening(gridCharacter.awakening.type)
|
setAwakeningLevel(gridCharacter.awakening.level)
|
||||||
setAwakeningLevel(gridCharacter.awakening.level)
|
|
||||||
}
|
|
||||||
setPerpetuity(gridCharacter.perpetuity)
|
setPerpetuity(gridCharacter.perpetuity)
|
||||||
}, [gridCharacter])
|
}, [gridCharacter])
|
||||||
|
|
||||||
// Prepare the GridWeaponObject to send to the server
|
// Prepare the GridWeaponObject to send to the server
|
||||||
function prepareObject(): GridCharacterObject {
|
function prepareObject() {
|
||||||
return {
|
let object: GridCharacterObject = {
|
||||||
character: {
|
character: {
|
||||||
rings: rings, // your local rings array
|
ring1: {
|
||||||
|
modifier: rings[1].modifier,
|
||||||
|
strength: rings[1].strength,
|
||||||
|
},
|
||||||
|
ring2: {
|
||||||
|
modifier: rings[2].modifier,
|
||||||
|
strength: rings[2].strength,
|
||||||
|
},
|
||||||
|
ring3: {
|
||||||
|
modifier: rings[3].modifier,
|
||||||
|
strength: rings[3].strength,
|
||||||
|
},
|
||||||
|
ring4: {
|
||||||
|
modifier: rings[4].modifier,
|
||||||
|
strength: rings[4].strength,
|
||||||
|
},
|
||||||
earring: {
|
earring: {
|
||||||
modifier: earring.modifier,
|
modifier: earring.modifier,
|
||||||
strength:
|
strength: earring.strength,
|
||||||
earring.modifier && earring.modifier > 0 ? earring.strength : 0,
|
|
||||||
},
|
},
|
||||||
// Only include awakening if one is set.
|
|
||||||
...(awakening
|
|
||||||
? {
|
|
||||||
awakening: {
|
|
||||||
id: awakening.id,
|
|
||||||
level: awakeningLevel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
transcendence_step: transcendenceStep,
|
transcendence_step: transcendenceStep,
|
||||||
perpetuity: perpetuity,
|
perpetuity: perpetuity,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (awakening) {
|
||||||
|
object.character.awakening_id = awakening.id
|
||||||
|
object.character.awakening_level = awakeningLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Modification checking
|
// Methods: Modification checking
|
||||||
|
|
@ -150,12 +150,12 @@ const CharacterModal = ({
|
||||||
|
|
||||||
function ringsChanged() {
|
function ringsChanged() {
|
||||||
// Create an empty ExtendedMastery object
|
// Create an empty ExtendedMastery object
|
||||||
const emptyRingset: CharacterOverMastery = [
|
const emptyRingset: CharacterOverMastery = {
|
||||||
{ ...emptyExtendedMastery, modifier: 1 },
|
1: { ...emptyExtendedMastery, modifier: 1 },
|
||||||
{ ...emptyExtendedMastery, modifier: 2 },
|
2: { ...emptyExtendedMastery, modifier: 2 },
|
||||||
emptyExtendedMastery,
|
3: emptyExtendedMastery,
|
||||||
emptyExtendedMastery,
|
4: emptyExtendedMastery,
|
||||||
]
|
}
|
||||||
|
|
||||||
// Check if the current ringset is empty on the current GridCharacter and our local state
|
// Check if the current ringset is empty on the current GridCharacter and our local state
|
||||||
const isEmptyRingset =
|
const isEmptyRingset =
|
||||||
|
|
@ -193,8 +193,8 @@ const CharacterModal = ({
|
||||||
function awakeningChanged() {
|
function awakeningChanged() {
|
||||||
// Check if the awakening in local state is different from the one on the current GridCharacter
|
// Check if the awakening in local state is different from the one on the current GridCharacter
|
||||||
const awakeningChanged =
|
const awakeningChanged =
|
||||||
!isEqual(gridCharacter.awakening?.type, awakening) ||
|
!isEqual(gridCharacter.awakening.type, awakening) ||
|
||||||
gridCharacter.awakening?.level !== awakeningLevel
|
gridCharacter.awakening.level !== awakeningLevel
|
||||||
|
|
||||||
// Return true if the awakening has been modified and is not empty
|
// Return true if the awakening has been modified and is not empty
|
||||||
return awakeningChanged
|
return awakeningChanged
|
||||||
|
|
@ -225,26 +225,8 @@ const CharacterModal = ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiveAwakeningValues(slug: string, level: number) {
|
function receiveAwakeningValues(id: string, level: number) {
|
||||||
const mappedId = AWAKENING_MAP[slug] || null
|
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
|
||||||
const existingAwakening = gridCharacter.object.awakenings.find(
|
|
||||||
(a) => a.slug === slug
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingAwakening && mappedId) {
|
|
||||||
setAwakening({
|
|
||||||
...existingAwakening,
|
|
||||||
id: mappedId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setAwakening({
|
|
||||||
id: mappedId || '',
|
|
||||||
slug,
|
|
||||||
name: { en: '', jp: '' },
|
|
||||||
order: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setAwakeningLevel(level)
|
setAwakeningLevel(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,18 +269,21 @@ const CharacterModal = ({
|
||||||
const confirmationAlert = (
|
const confirmationAlert = (
|
||||||
<Alert
|
<Alert
|
||||||
message={
|
message={
|
||||||
<>
|
<span>
|
||||||
{t.rich('alert.unsaved_changes.object', {
|
<Trans i18nKey="alerts.unsaved_changes.object">
|
||||||
objectName: gridCharacter.object.name[locale],
|
You will lose all changes to{' '}
|
||||||
strong: (chunks) => <strong>{chunks}</strong>,
|
<strong>{{ objectName: gridCharacter.object.name[locale] }}</strong>{' '}
|
||||||
br: () => <br />
|
if you continue.
|
||||||
})}
|
<br />
|
||||||
</>
|
<br />
|
||||||
|
Are you sure you want to continue without saving?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
open={alertOpen}
|
open={alertOpen}
|
||||||
primaryActionText={t('alert.unsaved_changes.buttons.confirm')}
|
primaryActionText="Close"
|
||||||
primaryAction={close}
|
primaryAction={close}
|
||||||
cancelActionText={t('alert.unsaved_changes.buttons.cancel')}
|
cancelActionText="Nevermind"
|
||||||
cancelAction={() => setAlertOpen(false)}
|
cancelAction={() => setAlertOpen(false)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -320,13 +305,13 @@ const CharacterModal = ({
|
||||||
object="earring"
|
object="earring"
|
||||||
dataSet={elementalizeAetherialMastery(gridCharacter)}
|
dataSet={elementalizeAetherialMastery(gridCharacter)}
|
||||||
selectValue={
|
selectValue={
|
||||||
gridCharacter.over_mastery && gridCharacter.aetherial_mastery
|
gridCharacter.aetherial_mastery
|
||||||
? gridCharacter.aetherial_mastery?.modifier
|
? gridCharacter.aetherial_mastery.modifier
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
inputValue={
|
inputValue={
|
||||||
gridCharacter.over_mastery && gridCharacter.aetherial_mastery
|
gridCharacter.aetherial_mastery
|
||||||
? gridCharacter.aetherial_mastery?.strength
|
? gridCharacter.aetherial_mastery.strength
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
sendValidity={receiveValidity}
|
sendValidity={receiveValidity}
|
||||||
|
|
@ -369,8 +354,8 @@ const CharacterModal = ({
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="character"
|
className="character"
|
||||||
headerRef={headerRef}
|
headerref={headerRef}
|
||||||
footerRef={footerRef}
|
footerref={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={() => {}}
|
onEscapeKeyDown={() => {}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
|
|
||||||
import UncapIndicator from '~components/uncap/UncapIndicator'
|
import UncapIndicator from '~components/uncap/UncapIndicator'
|
||||||
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
|
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
|
||||||
|
|
@ -18,11 +15,8 @@ const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||||
|
|
||||||
const CharacterResult = (props: Props) => {
|
const CharacterResult = (props: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const routerLocale = getCookie('NEXT_LOCALE')
|
|
||||||
const locale =
|
const locale =
|
||||||
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
const character = props.data
|
const character = props.data
|
||||||
|
|
||||||
|
|
@ -45,8 +39,6 @@ const CharacterResult = (props: Props) => {
|
||||||
type="character"
|
type="character"
|
||||||
flb={character.uncap.flb}
|
flb={character.uncap.flb}
|
||||||
ulb={character.uncap.ulb}
|
ulb={character.uncap.ulb}
|
||||||
transcendence={character.uncap.ulb}
|
|
||||||
transcendenceStage={5}
|
|
||||||
special={character.special}
|
special={character.special}
|
||||||
/>
|
/>
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslation } from 'next-i18next'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
import SearchFilter from '~components/search/SearchFilter'
|
import SearchFilter from '~components/search/SearchFilter'
|
||||||
|
|
@ -19,7 +19,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CharacterSearchFilterBar = (props: Props) => {
|
const CharacterSearchFilterBar = (props: Props) => {
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
const [rarityMenu, setRarityMenu] = useState(false)
|
const [rarityMenu, setRarityMenu] = useState(false)
|
||||||
const [elementMenu, setElementMenu] = useState(false)
|
const [elementMenu, setElementMenu] = useState(false)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
import { useRouter } from 'next/router'
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useTranslations } from 'next-intl'
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
@ -58,13 +55,10 @@ const CharacterUnit = ({
|
||||||
updateTranscendence,
|
updateTranscendence,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
// Translations and locale
|
// Translations and locale
|
||||||
const t = useTranslations('common')
|
const { t } = useTranslation('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const routerLocale = getCookie('NEXT_LOCALE')
|
|
||||||
const locale =
|
const locale =
|
||||||
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
// State snapshot
|
// State snapshot
|
||||||
const { party, grid } = useSnapshot(appState)
|
const { party, grid } = useSnapshot(appState)
|
||||||
|
|
@ -154,12 +148,12 @@ const CharacterUnit = ({
|
||||||
let character = cloneDeep(gridCharacter)
|
let character = cloneDeep(gridCharacter)
|
||||||
|
|
||||||
if (character.over_mastery) {
|
if (character.over_mastery) {
|
||||||
const overMastery: CharacterOverMastery = [
|
const overMastery: CharacterOverMastery = {
|
||||||
gridCharacter.over_mastery[0],
|
1: gridCharacter.over_mastery[0],
|
||||||
gridCharacter.over_mastery[1],
|
2: gridCharacter.over_mastery[1],
|
||||||
gridCharacter.over_mastery[2],
|
3: gridCharacter.over_mastery[2],
|
||||||
gridCharacter.over_mastery[3],
|
4: gridCharacter.over_mastery[3],
|
||||||
]
|
}
|
||||||
|
|
||||||
character.over_mastery = overMastery
|
character.over_mastery = overMastery
|
||||||
}
|
}
|
||||||
|
|
@ -268,12 +262,11 @@ const CharacterUnit = ({
|
||||||
cancelAction={() => setAlertOpen(false)}
|
cancelAction={() => setAlertOpen(false)}
|
||||||
cancelActionText={t('buttons.cancel')}
|
cancelActionText={t('buttons.cancel')}
|
||||||
message={
|
message={
|
||||||
<>
|
<Trans i18nKey="modals.characters.messages.remove">
|
||||||
{t.rich('modals.characters.messages.remove', {
|
Are you sure you want to remove{' '}
|
||||||
character: gridCharacter?.object.name[locale] || '',
|
<strong>{{ character: gridCharacter?.object.name[locale] }}</strong>{' '}
|
||||||
strong: (chunks) => <strong>{chunks}</strong>
|
from your team?
|
||||||
})}
|
</Trans>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue