diff --git a/.gitignore b/.gitignore index c7376d35..e6fbad6e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ dist/ public/images/weapon* public/images/summon* public/images/chara* +public/images/jobs # Typescript v1 declaration files typings/ diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index a5156276..9dc64075 100644 --- a/components/AccountModal/index.tsx +++ b/components/AccountModal/index.tsx @@ -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() const languageSelect = React.createRef() + const genderSelect = React.createRef() const privateSelect = React.createRef() 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) { + 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} +
+
+ +
+ + +
diff --git a/components/CharacterGrid/index.scss b/components/CharacterGrid/index.scss index 79725c38..01e6ada2 100644 --- a/components/CharacterGrid/index.scss +++ b/components/CharacterGrid/index.scss @@ -1,6 +1,9 @@ #CharacterGrid { display: flex; + flex-direction: column; justify-content: center; + margin: auto; + max-width: 761px; } #grid_characters { diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index 10daad5c..d4d31ae0 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -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 ( -
-
    - {Array.from(Array(numCharacters)).map((x, i) => { - return ( -
  • - -
  • - ) - })} -
+
+
+ +
    + {Array.from(Array(numCharacters)).map((x, i) => { + return ( +
  • + +
  • + ) + })} +
+
) } diff --git a/components/JobDropdown/index.scss b/components/JobDropdown/index.scss new file mode 100644 index 00000000..e69de29b diff --git a/components/JobDropdown/index.tsx b/components/JobDropdown/index.tsx new file mode 100644 index 00000000..b0a6019f --- /dev/null +++ b/components/JobDropdown/index.tsx @@ -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) => void +} + +type GroupedJob = { [key: string]: Job[] } + +const JobDropdown = React.forwardRef(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() + const [jobs, setJobs] = useState() + const [sortedJobs, setSortedJobs] = useState() + + // 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) { + 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 ( + + ) + }) + + const groupName = jobGroups.find(g => g.slug === group)?.name[locale] + + return ( + + {options} + + ) + } + + return ( + + ) +}) + +export default JobDropdown diff --git a/components/JobSection/index.scss b/components/JobSection/index.scss new file mode 100644 index 00000000..bf1e710c --- /dev/null +++ b/components/JobSection/index.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/components/JobSection/index.tsx b/components/JobSection/index.tsx new file mode 100644 index 00000000..464f261f --- /dev/null +++ b/components/JobSection/index.tsx @@ -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() + 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 ( +
+
+ +
+
+ +
+ ) +} + +export default JobSection \ No newline at end of file diff --git a/components/LoginModal/index.tsx b/components/LoginModal/index.tsx index 831e35d5..8d83ec13 100644 --- a/components/LoginModal/index.tsx +++ b/components/LoginModal/index.tsx @@ -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 diff --git a/components/Party/index.tsx b/components/Party/index.tsx index 2dfb7ace..eb5a0695 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -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() const [currentTab, setCurrentTab] = useState(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) => { diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index c74ca550..516c6900 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -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 diff --git a/pages/[username].tsx b/pages/[username].tsx index 9ee9d695..a2f6f7cc 100644 --- a/pages/[username].tsx +++ b/pages/[username].tsx @@ -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) diff --git a/pages/_app.tsx b/pages/_app.tsx index a5d4e39c..c7dd8537 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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.`) diff --git a/public/images/background_a.jpg b/public/images/background_a.jpg new file mode 100644 index 00000000..855dbb84 Binary files /dev/null and b/public/images/background_a.jpg differ diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3c49dd17..a189bb35 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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" diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index d533c5b5..908a87ef 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -136,11 +136,16 @@ "labels": { "picture": "プロフィール画像", "language": "言語", + "gender": "主人公", "private": "プライベート" }, "descriptions": { "private": "プロフィールを隠し、編成をコレクションに表示されないようにします" }, + "gender": { + "gran": "グラン", + "djeeta": "ジータ" + }, "language": { "english": "英語", "japanese": "日本語" diff --git a/types/Job.d.ts b/types/Job.d.ts new file mode 100644 index 00000000..bab89b90 --- /dev/null +++ b/types/Job.d.ts @@ -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 + } +} \ No newline at end of file diff --git a/types/User.d.ts b/types/User.d.ts index 9bd3443b..60d228ea 100644 --- a/types/User.d.ts +++ b/types/User.d.ts @@ -6,5 +6,6 @@ interface User { picture: string element: string } + gender: number private: boolean } \ No newline at end of file diff --git a/utils/accountState.tsx b/utils/accountState.tsx index d57df6a2..de0fdb65 100644 --- a/utils/accountState.tsx +++ b/utils/accountState.tsx @@ -10,6 +10,7 @@ interface AccountState { username: string picture: string element: string + gender: number } | undefined } } diff --git a/utils/api.tsx b/utils/api.tsx index a6f66471..2cad0cea 100644 --- a/utils/api.tsx +++ b/utils/api.tsx @@ -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' }) diff --git a/utils/appState.tsx b/utils/appState.tsx index d95d355c..22db2cbc 100644 --- a/utils/appState.tsx +++ b/utils/appState.tsx @@ -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, diff --git a/utils/jobGroups.tsx b/utils/jobGroups.tsx new file mode 100644 index 00000000..2741a638 --- /dev/null +++ b/utils/jobGroups.tsx @@ -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' + } + }, +] \ No newline at end of file