Merge pull request #32 from jedmund/class

Add support for selecting Jobs for teams
This commit is contained in:
Justin Edmund 2022-04-10 13:33:10 -07:00 committed by GitHub
commit 4ae8f829df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 390 additions and 23 deletions

1
.gitignore vendored
View file

@ -50,6 +50,7 @@ dist/
public/images/weapon* public/images/weapon*
public/images/summon* public/images/summon*
public/images/chara* public/images/chara*
public/images/jobs
# Typescript v1 declaration files # Typescript v1 declaration files
typings/ typings/

View file

@ -36,16 +36,19 @@ const AccountModal = () => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [picture, setPicture] = useState('') const [picture, setPicture] = useState('')
const [language, setLanguage] = useState('') const [language, setLanguage] = useState('')
const [gender, setGender] = useState(0)
const [privateProfile, setPrivateProfile] = useState(false) const [privateProfile, setPrivateProfile] = useState(false)
// Refs // Refs
const pictureSelect = React.createRef<HTMLSelectElement>() const pictureSelect = React.createRef<HTMLSelectElement>()
const languageSelect = React.createRef<HTMLSelectElement>() const languageSelect = React.createRef<HTMLSelectElement>()
const genderSelect = React.createRef<HTMLSelectElement>()
const privateSelect = React.createRef<HTMLInputElement>() const privateSelect = React.createRef<HTMLInputElement>()
useEffect(() => { useEffect(() => {
if (cookies.user) setPicture(cookies.user.picture) if (cookies.user) setPicture(cookies.user.picture)
if (cookies.user) setLanguage(cookies.user.language) if (cookies.user) setLanguage(cookies.user.language)
if (cookies.user) setGender(cookies.user.gender)
}, [cookies]) }, [cookies])
const pictureOptions = ( const pictureOptions = (
@ -66,6 +69,11 @@ const AccountModal = () => {
setLanguage(languageSelect.current.value) setLanguage(languageSelect.current.value)
} }
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (genderSelect.current)
setGender(parseInt(genderSelect.current.value))
}
function handlePrivateChange(checked: boolean) { function handlePrivateChange(checked: boolean) {
setPrivateProfile(checked) setPrivateProfile(checked)
} }
@ -78,6 +86,7 @@ const AccountModal = () => {
picture: picture, picture: picture,
element: pictureData.find(i => i.filename === picture)?.element, element: pictureData.find(i => i.filename === picture)?.element,
language: language, language: language,
gender: gender,
private: privateProfile private: privateProfile
} }
} }
@ -89,7 +98,8 @@ const AccountModal = () => {
const cookieObj = { const cookieObj = {
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
language: user.language, gender: user.gender,
language: user.language
} }
setCookies('user', cookieObj, { path: '/'}) setCookies('user', cookieObj, { path: '/'})
@ -98,7 +108,8 @@ const AccountModal = () => {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element element: user.picture.element,
gender: user.gender
} }
setOpen(false) setOpen(false)
@ -157,6 +168,16 @@ const AccountModal = () => {
{pictureOptions} {pictureOptions}
</select> </select>
</div> </div>
<div className="field">
<div className="left">
<label>{t('modals.settings.labels.gender')}</label>
</div>
<select name="gender" onChange={handleGenderChange} value={gender} ref={genderSelect}>
<option key="gran" value="0">{t('modals.settings.gender.gran')}</option>
<option key="djeeta" value="1">{t('modals.settings.gender.djeeta')}</option>
</select>
</div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t('modals.settings.labels.language')}</label> <label>{t('modals.settings.labels.language')}</label>

View file

@ -1,6 +1,9 @@
#CharacterGrid { #CharacterGrid {
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
margin: auto;
max-width: 761px;
} }
#grid_characters { #grid_characters {

View file

@ -6,6 +6,7 @@ import { useSnapshot } from 'valtio'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import JobSection from '~components/JobSection'
import CharacterUnit from '~components/CharacterUnit' import CharacterUnit from '~components/CharacterUnit'
import api from '~utils/api' import api from '~utils/api'
@ -227,22 +228,25 @@ const CharacterGrid = (props: Props) => {
// Render: JSX components // Render: JSX components
return ( return (
<div id="CharacterGrid"> <div>
<ul id="grid_characters"> <div id="CharacterGrid">
{Array.from(Array(numCharacters)).map((x, i) => { <JobSection />
return ( <ul id="grid_characters">
<li key={`grid_unit_${i}`} > {Array.from(Array(numCharacters)).map((x, i) => {
<CharacterUnit return (
gridCharacter={grid.characters[i]} <li key={`grid_unit_${i}`} >
editable={party.editable} <CharacterUnit
position={i} gridCharacter={grid.characters[i]}
updateObject={receiveCharacterFromSearch} editable={party.editable}
updateUncap={initiateUncapUpdate} position={i}
/> updateObject={receiveCharacterFromSearch}
</li> updateUncap={initiateUncapUpdate}
) />
})} </li>
</ul> )
})}
</ul>
</div>
</div> </div>
) )
} }

View file

View file

@ -0,0 +1,97 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import api from '~utils/api'
import { appState } from '~utils/appState'
import { jobGroups } from '~utils/jobGroups'
import './index.scss'
// Props
interface Props {
currentJob?: string
onChange?: (job?: Job) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
}
type GroupedJob = { [key: string]: Job[] }
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
// Set up router for locale
const router = useRouter()
const locale = router.locale || 'en'
// Set up local states for storing jobs
const [currentJob, setCurrentJob] = useState<Job>()
const [jobs, setJobs] = useState<Job[]>()
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
// Organize jobs into groups on mount
const organizeJobs = useCallback((jobs: Job[]) => {
const jobGroups = jobs.map(job => job.row).filter((value, index, self) => self.indexOf(value) === index)
let groupedJobs: GroupedJob = {}
jobGroups.forEach(group => {
groupedJobs[group] = jobs.filter(job => job.row === group)
})
setJobs(jobs)
setSortedJobs(groupedJobs)
appState.jobs = jobs
}, [])
// Fetch all jobs on mount
useEffect(() => {
api.endpoints.jobs.getAll()
.then(response => organizeJobs(response.data))
}, [organizeJobs])
// Set current job on mount
useEffect(() => {
if (jobs && props.currentJob) {
const job = jobs.find(job => job.id === props.currentJob)
setCurrentJob(job)
}
}, [jobs, props.currentJob])
// Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (jobs) {
const job = jobs.find(job => job.id === event.target.value)
if (props.onChange) props.onChange(job)
setCurrentJob(job)
}
}
// Render JSX for each job option, sorted into optgroups
function renderJobGroup(group: string) {
const options = sortedJobs && sortedJobs[group].length > 0 &&
sortedJobs[group].sort((a, b) => a.order - b.order).map((item, i) => {
return (
<option key={i} value={item.id}>{item.name[locale]}</option>
)
})
const groupName = jobGroups.find(g => g.slug === group)?.name[locale]
return (
<optgroup key={group} label={groupName}>
{options}
</optgroup>
)
}
return (
<select
key={currentJob?.id}
value={currentJob?.id}
onBlur={props.onBlur}
onChange={handleChange}
ref={ref}>
<option key="no-job" value={-1}>No class</option>
{ (sortedJobs) ? Object.keys(sortedJobs).map(x => renderJobGroup(x)) : '' }
</select>
)
})
export default JobDropdown

View file

@ -0,0 +1,44 @@
#Job {
display: flex;
margin-bottom: $unit * 3;
select {
flex-grow: 1;
width: auto;
}
.JobImage {
$height: 249px;
$width: 447px;
background: url('/images/background_a.jpg');
background-size: 500px 281px;
border-radius: $unit;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
display: block;
flex-grow: 2;
height: $height;
margin-right: $unit * 3;
max-height: $height;
max-width: $width;
overflow: hidden;
position: relative;
width: $width;
transition: box-shadow 0.15s ease-in-out;
img {
position: relative;
top: $unit * -4;
left: 50%;
transform: translateX(-50%);
width: 100%;
z-index: 2;
}
.Overlay {
background: rgba(255, 255, 255, 0.12);
position: absolute;
z-index: 1;
}
}
}

View file

@ -0,0 +1,64 @@
import React, { useEffect, useState } from 'react'
import { useSnapshot } from 'valtio'
import JobDropdown from '~components/JobDropdown'
import { appState } from '~utils/appState'
import './index.scss'
// Props
interface Props {}
const JobSection = (props: Props) => {
const [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState('')
const { party } = useSnapshot(appState)
useEffect(() => {
// Set current job based on ID
setJob(party.job)
}, [])
useEffect(() => {
generateImageUrl()
})
useEffect(() => {
if (job) appState.party.job = job
}, [job])
function receiveJob(job?: Job) {
setJob(job)
}
function generateImageUrl() {
let imgSrc = ""
if (job) {
const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
const gender = (party.user && party.user.gender == 1) ? 'b' : 'a'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
}
setImageUrl(imgSrc)
}
// Render: JSX components
return (
<section id="Job">
<div className="JobImage">
<img src={imageUrl} />
<div className="Overlay" />
</div>
<JobDropdown
currentJob={ (party.job) ? party.job.id : undefined}
onChange={receiveJob}
/>
</section>
)
}
export default JobSection

View file

@ -132,6 +132,7 @@ const LoginModal = (props: Props) => {
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
language: user.language, language: user.language,
gender: user.gender
} }
setCookies('user', cookieObj, { path: '/' }) setCookies('user', cookieObj, { path: '/' })
@ -140,7 +141,8 @@ const LoginModal = (props: Props) => {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element element: user.picture.element,
gender: user.gender
} }
accountState.account.authorized = true accountState.account.authorized = true

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import { subscribeKey } from 'valtio/utils'
import PartySegmentedControl from '~components/PartySegmentedControl' import PartySegmentedControl from '~components/PartySegmentedControl'
import PartyDetails from '~components/PartyDetails' import PartyDetails from '~components/PartyDetails'
@ -38,6 +39,9 @@ const Party = (props: Props) => {
// Set up states // Set up states
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
const jobState = party.job
const [job, setJob] = useState<Job>()
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon) const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
// Reset state on first load // Reset state on first load
@ -46,6 +50,14 @@ const Party = (props: Props) => {
appState.grid = resetState.grid appState.grid = resetState.grid
}, []) }, [])
useEffect(() => {
setJob(jobState)
}, [jobState])
useEffect(() => {
jobChanged()
}, [job])
// Methods: Creating a new party // Methods: Creating a new party
async function createParty(extra: boolean = false) { async function createParty(extra: boolean = false) {
let body = { let body = {
@ -69,6 +81,14 @@ const Party = (props: Props) => {
} }
} }
function jobChanged() {
if (party.id) {
api.endpoints.parties.update(party.id, {
'party': { 'job_id': (job) ? job.id : '' }
}, headers)
}
}
function updateDetails(name?: string, description?: string, raid?: Raid) { function updateDetails(name?: string, description?: string, raid?: Raid) {
if (appState.party.name !== name || if (appState.party.name !== name ||
appState.party.description !== description || appState.party.description !== description ||
@ -145,6 +165,7 @@ const Party = (props: Props) => {
appState.party.name = response.data.party.name appState.party.name = response.data.party.name
appState.party.description = response.data.party.description appState.party.description = response.data.party.description
appState.party.raid = response.data.party.raid appState.party.raid = response.data.party.raid
appState.party.job = response.data.party.job
}, []) }, [])
const handleError = useCallback((error: any) => { const handleError = useCallback((error: any) => {

View file

@ -100,6 +100,7 @@ const SignupModal = (props: Props) => {
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
language: user.language, language: user.language,
gender: user.gender
} }
// TODO: Set language // TODO: Set language
@ -109,7 +110,8 @@ const SignupModal = (props: Props) => {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element element: user.picture.element,
gender: user.gender
} }
accountState.account.authorized = true accountState.account.authorized = true

View file

@ -25,7 +25,8 @@ const emptyUser = {
picture: '', picture: '',
element: '' element: ''
}, },
private: false private: false,
gender: 0
} }
const ProfileRoute: React.FC = () => { const ProfileRoute: React.FC = () => {
@ -123,7 +124,8 @@ const ProfileRoute: React.FC = () => {
username: response.data.user.username, username: response.data.user.username,
granblueId: response.data.user.granblue_id, granblueId: response.data.user.granblue_id,
picture: response.data.user.picture, picture: response.data.user.picture,
private: response.data.user.private private: response.data.user.private,
gender: response.data.user.gender
}) })
setTotalPages(response.data.parties.total_pages) setTotalPages(response.data.parties.total_pages)

View file

@ -21,7 +21,8 @@ function MyApp({ Component, pageProps }: AppProps) {
id: cookies.account.user_id, id: cookies.account.user_id,
username: cookies.account.username, username: cookies.account.username,
picture: '', picture: '',
element: '' element: '',
gender: 0
} }
} else { } else {
console.log(`You are not currently logged in.`) console.log(`You are not currently logged in.`)

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

View file

@ -136,11 +136,16 @@
"labels": { "labels": {
"picture": "Picture", "picture": "Picture",
"language": "Language", "language": "Language",
"gender": "Main Character",
"private": "Private" "private": "Private"
}, },
"descriptions": { "descriptions": {
"private": "Hide your profile and prevent your grids from showing up in collections" "private": "Hide your profile and prevent your grids from showing up in collections"
}, },
"gender": {
"gran": "Gran",
"djeeta": "Djeeta"
},
"language": { "language": {
"english": "English", "english": "English",
"japanese": "Japanese" "japanese": "Japanese"

View file

@ -136,11 +136,16 @@
"labels": { "labels": {
"picture": "プロフィール画像", "picture": "プロフィール画像",
"language": "言語", "language": "言語",
"gender": "主人公",
"private": "プライベート" "private": "プライベート"
}, },
"descriptions": { "descriptions": {
"private": "プロフィールを隠し、編成をコレクションに表示されないようにします" "private": "プロフィールを隠し、編成をコレクションに表示されないようにします"
}, },
"gender": {
"gran": "グラン",
"djeeta": "ジータ"
},
"language": { "language": {
"english": "英語", "english": "英語",
"japanese": "日本語" "japanese": "日本語"

15
types/Job.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
interface Job {
id: string
row: string
ml: boolean
order: number
name: {
[key: string]: string
en: string
ja: string
}
proficiency: {
proficiency1: number
proficiency2: number
}
}

1
types/User.d.ts vendored
View file

@ -6,5 +6,6 @@ interface User {
picture: string picture: string
element: string element: string
} }
gender: number
private: boolean private: boolean
} }

View file

@ -10,6 +10,7 @@ interface AccountState {
username: string username: string
picture: string picture: string
element: string element: string
gender: number
} | undefined } | undefined
} }
} }

View file

@ -116,6 +116,7 @@ api.createEntity( { name: 'grid_weapons' })
api.createEntity( { name: 'characters' }) api.createEntity( { name: 'characters' })
api.createEntity( { name: 'weapons' }) api.createEntity( { name: 'weapons' })
api.createEntity( { name: 'summons' }) api.createEntity( { name: 'summons' })
api.createEntity( { name: 'jobs' })
api.createEntity( { name: 'raids' }) api.createEntity( { name: 'raids' })
api.createEntity( { name: 'weapon_keys' }) api.createEntity( { name: 'weapon_keys' })
api.createEntity( { name: 'favorites' }) api.createEntity( { name: 'favorites' })

View file

@ -1,5 +1,20 @@
import { proxy } from "valtio"; import { proxy } from "valtio";
const emptyJob: Job = {
id: "-1",
row: "",
ml: false,
order: 0,
name: {
en: "",
ja: ""
},
proficiency: {
proficiency1: 0,
proficiency2: 0
}
}
interface AppState { interface AppState {
[key: string]: any [key: string]: any
@ -9,6 +24,7 @@ interface AppState {
detailsVisible: boolean, detailsVisible: boolean,
name: string | undefined, name: string | undefined,
description: string | undefined, description: string | undefined,
job: Job,
raid: Raid | undefined, raid: Raid | undefined,
element: number, element: number,
extra: boolean, extra: boolean,
@ -46,6 +62,7 @@ export const initialAppState: AppState = {
detailsVisible: false, detailsVisible: false,
name: undefined, name: undefined,
description: undefined, description: undefined,
job: emptyJob,
raid: undefined, raid: undefined,
element: 0, element: 0,
extra: false, extra: false,

60
utils/jobGroups.tsx Normal file
View file

@ -0,0 +1,60 @@
interface JobGroup {
slug: string
name: {
[key: string]: string
en: string
ja: string
}
}
export const jobGroups: JobGroup[] = [
{
slug: "1",
name: {
en: 'Row I',
ja: 'Class I'
}
},
{
slug: "2",
name: {
en: 'Row II',
ja: 'Class II'
}
},
{
slug: "3",
name: {
en: 'Row III',
ja: 'Class III'
}
},
{
slug: "4",
name: {
en: 'Row IV',
ja: 'Class IV'
}
},
{
slug: "5",
name: {
en: 'Row V',
ja: 'Class V'
}
},
{
slug: "ex1",
name: {
en: 'Extra I',
ja: 'EXTRA I'
}
},
{
slug: "ex2",
name: {
en: 'Extra II',
ja: 'EXTRA II'
}
},
]