Merge pull request #32 from jedmund/class
Add support for selecting Jobs for teams
This commit is contained in:
commit
4ae8f829df
22 changed files with 390 additions and 23 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -50,6 +50,7 @@ dist/
|
|||
public/images/weapon*
|
||||
public/images/summon*
|
||||
public/images/chara*
|
||||
public/images/jobs
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
|
|
|||
|
|
@ -36,16 +36,19 @@ const AccountModal = () => {
|
|||
const [open, setOpen] = useState(false)
|
||||
const [picture, setPicture] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const [gender, setGender] = useState(0)
|
||||
const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
// Refs
|
||||
const pictureSelect = React.createRef<HTMLSelectElement>()
|
||||
const languageSelect = React.createRef<HTMLSelectElement>()
|
||||
const genderSelect = React.createRef<HTMLSelectElement>()
|
||||
const privateSelect = React.createRef<HTMLInputElement>()
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies.user) setPicture(cookies.user.picture)
|
||||
if (cookies.user) setLanguage(cookies.user.language)
|
||||
if (cookies.user) setGender(cookies.user.gender)
|
||||
}, [cookies])
|
||||
|
||||
const pictureOptions = (
|
||||
|
|
@ -66,6 +69,11 @@ const AccountModal = () => {
|
|||
setLanguage(languageSelect.current.value)
|
||||
}
|
||||
|
||||
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (genderSelect.current)
|
||||
setGender(parseInt(genderSelect.current.value))
|
||||
}
|
||||
|
||||
function handlePrivateChange(checked: boolean) {
|
||||
setPrivateProfile(checked)
|
||||
}
|
||||
|
|
@ -78,6 +86,7 @@ const AccountModal = () => {
|
|||
picture: picture,
|
||||
element: pictureData.find(i => i.filename === picture)?.element,
|
||||
language: language,
|
||||
gender: gender,
|
||||
private: privateProfile
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +98,8 @@ const AccountModal = () => {
|
|||
const cookieObj = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender,
|
||||
language: user.language
|
||||
}
|
||||
|
||||
setCookies('user', cookieObj, { path: '/'})
|
||||
|
|
@ -98,7 +108,8 @@ const AccountModal = () => {
|
|||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
|
|
@ -157,6 +168,16 @@ const AccountModal = () => {
|
|||
{pictureOptions}
|
||||
</select>
|
||||
</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="left">
|
||||
<label>{t('modals.settings.labels.language')}</label>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#CharacterGrid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
max-width: 761px;
|
||||
}
|
||||
|
||||
#grid_characters {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useSnapshot } from 'valtio'
|
|||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import JobSection from '~components/JobSection'
|
||||
import CharacterUnit from '~components/CharacterUnit'
|
||||
|
||||
import api from '~utils/api'
|
||||
|
|
@ -227,22 +228,25 @@ const CharacterGrid = (props: Props) => {
|
|||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<div id="CharacterGrid">
|
||||
<ul id="grid_characters">
|
||||
{Array.from(Array(numCharacters)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<CharacterUnit
|
||||
gridCharacter={grid.characters[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
updateObject={receiveCharacterFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection />
|
||||
<ul id="grid_characters">
|
||||
{Array.from(Array(numCharacters)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<CharacterUnit
|
||||
gridCharacter={grid.characters[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
updateObject={receiveCharacterFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
0
components/JobDropdown/index.scss
Normal file
0
components/JobDropdown/index.scss
Normal file
97
components/JobDropdown/index.tsx
Normal file
97
components/JobDropdown/index.tsx
Normal 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
|
||||
44
components/JobSection/index.scss
Normal file
44
components/JobSection/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
components/JobSection/index.tsx
Normal file
64
components/JobSection/index.tsx
Normal 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
|
||||
|
|
@ -132,6 +132,7 @@ const LoginModal = (props: Props) => {
|
|||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
setCookies('user', cookieObj, { path: '/' })
|
||||
|
|
@ -140,7 +141,8 @@ const LoginModal = (props: Props) => {
|
|||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
accountState.account.authorized = true
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||
import PartyDetails from '~components/PartyDetails'
|
||||
|
|
@ -38,6 +39,9 @@ const Party = (props: Props) => {
|
|||
|
||||
// Set up states
|
||||
const { party } = useSnapshot(appState)
|
||||
const jobState = party.job
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
||||
|
||||
// Reset state on first load
|
||||
|
|
@ -46,6 +50,14 @@ const Party = (props: Props) => {
|
|||
appState.grid = resetState.grid
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setJob(jobState)
|
||||
}, [jobState])
|
||||
|
||||
useEffect(() => {
|
||||
jobChanged()
|
||||
}, [job])
|
||||
|
||||
// Methods: Creating a new party
|
||||
async function createParty(extra: boolean = false) {
|
||||
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) {
|
||||
if (appState.party.name !== name ||
|
||||
appState.party.description !== description ||
|
||||
|
|
@ -145,6 +165,7 @@ const Party = (props: Props) => {
|
|||
appState.party.name = response.data.party.name
|
||||
appState.party.description = response.data.party.description
|
||||
appState.party.raid = response.data.party.raid
|
||||
appState.party.job = response.data.party.job
|
||||
}, [])
|
||||
|
||||
const handleError = useCallback((error: any) => {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ const SignupModal = (props: Props) => {
|
|||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
// TODO: Set language
|
||||
|
|
@ -109,7 +110,8 @@ const SignupModal = (props: Props) => {
|
|||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
accountState.account.authorized = true
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ const emptyUser = {
|
|||
picture: '',
|
||||
element: ''
|
||||
},
|
||||
private: false
|
||||
private: false,
|
||||
gender: 0
|
||||
}
|
||||
|
||||
const ProfileRoute: React.FC = () => {
|
||||
|
|
@ -123,7 +124,8 @@ const ProfileRoute: React.FC = () => {
|
|||
username: response.data.user.username,
|
||||
granblueId: response.data.user.granblue_id,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
id: cookies.account.user_id,
|
||||
username: cookies.account.username,
|
||||
picture: '',
|
||||
element: ''
|
||||
element: '',
|
||||
gender: 0
|
||||
}
|
||||
} else {
|
||||
console.log(`You are not currently logged in.`)
|
||||
|
|
|
|||
BIN
public/images/background_a.jpg
Normal file
BIN
public/images/background_a.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 773 KiB |
|
|
@ -136,11 +136,16 @@
|
|||
"labels": {
|
||||
"picture": "Picture",
|
||||
"language": "Language",
|
||||
"gender": "Main Character",
|
||||
"private": "Private"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "Hide your profile and prevent your grids from showing up in collections"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "Gran",
|
||||
"djeeta": "Djeeta"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"japanese": "Japanese"
|
||||
|
|
|
|||
|
|
@ -136,11 +136,16 @@
|
|||
"labels": {
|
||||
"picture": "プロフィール画像",
|
||||
"language": "言語",
|
||||
"gender": "主人公",
|
||||
"private": "プライベート"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "プロフィールを隠し、編成をコレクションに表示されないようにします"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "グラン",
|
||||
"djeeta": "ジータ"
|
||||
},
|
||||
"language": {
|
||||
"english": "英語",
|
||||
"japanese": "日本語"
|
||||
|
|
|
|||
15
types/Job.d.ts
vendored
Normal file
15
types/Job.d.ts
vendored
Normal 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
1
types/User.d.ts
vendored
|
|
@ -6,5 +6,6 @@ interface User {
|
|||
picture: string
|
||||
element: string
|
||||
}
|
||||
gender: number
|
||||
private: boolean
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ interface AccountState {
|
|||
username: string
|
||||
picture: string
|
||||
element: string
|
||||
gender: number
|
||||
} | undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ api.createEntity( { name: 'grid_weapons' })
|
|||
api.createEntity( { name: 'characters' })
|
||||
api.createEntity( { name: 'weapons' })
|
||||
api.createEntity( { name: 'summons' })
|
||||
api.createEntity( { name: 'jobs' })
|
||||
api.createEntity( { name: 'raids' })
|
||||
api.createEntity( { name: 'weapon_keys' })
|
||||
api.createEntity( { name: 'favorites' })
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import { proxy } from "valtio";
|
||||
|
||||
const emptyJob: Job = {
|
||||
id: "-1",
|
||||
row: "",
|
||||
ml: false,
|
||||
order: 0,
|
||||
name: {
|
||||
en: "",
|
||||
ja: ""
|
||||
},
|
||||
proficiency: {
|
||||
proficiency1: 0,
|
||||
proficiency2: 0
|
||||
}
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
[key: string]: any
|
||||
|
||||
|
|
@ -9,6 +24,7 @@ interface AppState {
|
|||
detailsVisible: boolean,
|
||||
name: string | undefined,
|
||||
description: string | undefined,
|
||||
job: Job,
|
||||
raid: Raid | undefined,
|
||||
element: number,
|
||||
extra: boolean,
|
||||
|
|
@ -46,6 +62,7 @@ export const initialAppState: AppState = {
|
|||
detailsVisible: false,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
job: emptyJob,
|
||||
raid: undefined,
|
||||
element: 0,
|
||||
extra: false,
|
||||
|
|
|
|||
60
utils/jobGroups.tsx
Normal file
60
utils/jobGroups.tsx
Normal 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'
|
||||
}
|
||||
},
|
||||
]
|
||||
Loading…
Reference in a new issue