Merge pull request #12 from jedmund/profile
Redesign/fix styling for Profile
This commit is contained in:
commit
a1f9778185
14 changed files with 218 additions and 65 deletions
|
|
@ -1,54 +1,82 @@
|
||||||
.GridRep {
|
.GridRep {
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
gap: $unit;
|
||||||
margin: 0 8px 8px 0;
|
padding: $unit * 2;
|
||||||
padding: 4px;
|
|
||||||
height: 148px;
|
|
||||||
width: 311px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.GridRep:hover {
|
&:hover {
|
||||||
border: 2px solid #2360C5;
|
background: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.GridRep .weapon {
|
.Grid .grid_weapons .grid_weapon {
|
||||||
background: white;
|
box-shadow: inset 0 0 1px $grey-70;
|
||||||
border-radius: 4px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.GridRep .grid_mainhand {
|
.Grid {
|
||||||
flex-shrink: 0;
|
display: flex;
|
||||||
height: 136px;
|
flex-direction: row;
|
||||||
width: 65px;
|
flex-shrink: 0;
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.GridRep .grid_weapons {
|
.grid_mainhand {
|
||||||
list-style: none;
|
margin-right: $unit;
|
||||||
margin: 0;
|
height: 139px;
|
||||||
padding: 0;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.GridRep .grid_weapon {
|
.grid_weapons {
|
||||||
background: white;
|
display: grid;
|
||||||
border-radius: 4px;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
float: left;
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
margin: 0 8px 8px 0;
|
gap: $unit;
|
||||||
height: 40px;
|
margin: 0;
|
||||||
width: 70px;
|
padding: 0;
|
||||||
}
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.GridRep .grid_weapon:nth-child(3n+3) {
|
.grid_weapon {
|
||||||
margin-right: 0;
|
background: white;
|
||||||
}
|
border-radius: 4px;
|
||||||
|
float: left;
|
||||||
|
height: 40px;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid_mainhand img[src*="jpg"],
|
||||||
|
.grid_weapon img[src*="jpg"] {
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.GridRep .grid_mainhand img[src*="jpg"],
|
.Details {
|
||||||
.GridRep .grid_weapon img[src*="jpg"] {
|
display: flex;
|
||||||
border-radius: 4px;
|
flex-direction: column;
|
||||||
width: 100%;
|
gap: $unit / 2;
|
||||||
height: 100%;
|
|
||||||
}
|
h2 {
|
||||||
|
color: $grey-00;
|
||||||
|
font-size: $font-regular;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
color: $grey-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid, time {
|
||||||
|
color: $grey-50;
|
||||||
|
font-size: $font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,37 @@
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
shortcode: string
|
shortcode: string
|
||||||
|
name: string
|
||||||
|
raid: Raid
|
||||||
grid: GridWeapon[]
|
grid: GridWeapon[]
|
||||||
|
updatedAt: Date
|
||||||
onClick: (shortcode: string) => void
|
onClick: (shortcode: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const GridRep = (props: Props) => {
|
const GridRep = (props: Props) => {
|
||||||
|
|
||||||
|
console.log(props)
|
||||||
const numWeapons: number = 9
|
const numWeapons: number = 9
|
||||||
|
|
||||||
const [mainhand, setMainhand] = useState<Weapon>()
|
const [mainhand, setMainhand] = useState<Weapon>()
|
||||||
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||||
|
|
||||||
|
const titleClass = classNames({
|
||||||
|
'empty': !props.name
|
||||||
|
})
|
||||||
|
|
||||||
|
const raidClass = classNames({
|
||||||
|
'raid': true,
|
||||||
|
'empty': !props.raid
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newWeapons = Array(numWeapons)
|
const newWeapons = Array(numWeapons)
|
||||||
|
|
||||||
|
|
@ -43,21 +61,31 @@ const GridRep = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="GridRep" onClick={navigate}>
|
<div className="GridRep" onClick={navigate}>
|
||||||
<div className="weapon grid_mainhand">
|
<div className="Details">
|
||||||
{generateMainhandImage()}
|
<h2 className={titleClass}>{ (props.name) ? props.name : 'Untitled' }</h2>
|
||||||
|
<div className="bottom">
|
||||||
|
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
|
||||||
|
<time className="last-updated" dateTime={props.updatedAt.toISOString()}>{formatTimeAgo(props.updatedAt, 'en-us')}</time>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="grid_weapons">
|
<div className="Grid">
|
||||||
{
|
<div className="weapon grid_mainhand">
|
||||||
Array.from(Array(numWeapons)).map((x, i) => {
|
{generateMainhandImage()}
|
||||||
return (
|
</div>
|
||||||
<li key={`${props.shortcode}-${i}`} className="grid_weapon">
|
|
||||||
{generateGridImage(i)}
|
<ul className="grid_weapons">
|
||||||
</li>
|
{
|
||||||
)
|
Array.from(Array(numWeapons)).map((x, i) => {
|
||||||
})
|
return (
|
||||||
}
|
<li key={`${props.shortcode}-${i}`} className="grid_weapon">
|
||||||
</ul>
|
{generateGridImage(i)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
.GridRepCollection {
|
.GridRepCollection {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: auto auto auto;
|
||||||
justify-content: center;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
.Details {
|
.PartyDetails {
|
||||||
display: none; // This breaks transition, find a workaround
|
display: none; // This breaks transition, find a workaround
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ const PartyDetails = (props: Props) => {
|
||||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||||
|
|
||||||
const readOnlyClasses = classNames({
|
const readOnlyClasses = classNames({
|
||||||
'Details': true,
|
'PartyDetails': true,
|
||||||
'ReadOnly': true,
|
'ReadOnly': true,
|
||||||
'Visible': !appSnapshot.party.detailsVisible
|
'Visible': !appSnapshot.party.detailsVisible
|
||||||
})
|
})
|
||||||
|
|
||||||
const editableClasses = classNames({
|
const editableClasses = classNames({
|
||||||
'Details': true,
|
'PartyDetails': true,
|
||||||
'Editable': true,
|
'Editable': true,
|
||||||
'Visible': appSnapshot.party.detailsVisible
|
'Visible': appSnapshot.party.detailsVisible
|
||||||
})
|
})
|
||||||
|
|
|
||||||
32
components/ProfileHeader/index.scss
Normal file
32
components/ProfileHeader/index.scss
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ProfileHeader {
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: $unit;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: $unit * 5;
|
||||||
|
max-width: $unit * 52;
|
||||||
|
padding: ($unit * 3) ($unit * 5);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $font-xxlarge;
|
||||||
|
font-weight: $normal;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
$diameter: 120px;
|
||||||
|
border-radius: $diameter / 2;
|
||||||
|
height: $diameter;
|
||||||
|
width: $diameter;
|
||||||
|
|
||||||
|
&.gran {
|
||||||
|
background-color: #CEE7FE;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.djeeta {
|
||||||
|
background-color: #FFE1FE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
components/ProfileHeader/index.tsx
Normal file
28
components/ProfileHeader/index.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
import { appState } from '~utils/appState'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
username: string
|
||||||
|
gender: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileHeader = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<section id="ProfileHeader">
|
||||||
|
<h1>{props.username}</h1>
|
||||||
|
<img
|
||||||
|
alt="Gran"
|
||||||
|
className="gran"
|
||||||
|
srcSet="/profile/gran.png,
|
||||||
|
/profile/gran@2x.png 2x"
|
||||||
|
src="/profile/gran.png" />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileHeader
|
||||||
|
|
@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
|
||||||
|
import ProfileHeader from '~components/ProfileHeader'
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
import GridRepCollection from '~components/GridRepCollection'
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
|
|
||||||
|
|
@ -14,8 +15,11 @@ interface User {
|
||||||
|
|
||||||
interface Party {
|
interface Party {
|
||||||
id: string
|
id: string
|
||||||
|
name: string
|
||||||
|
raid: Raid
|
||||||
shortcode: string
|
shortcode: string
|
||||||
weapons: GridWeapon[]
|
weapons: GridWeapon[]
|
||||||
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileRoute: React.FC = () => {
|
const ProfileRoute: React.FC = () => {
|
||||||
|
|
@ -44,7 +48,9 @@ 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
|
||||||
})
|
})
|
||||||
setParties(response.data.user.parties)
|
|
||||||
|
const parties: Party[] = response.data.user.parties
|
||||||
|
setParties(parties.sort((a, b) => (a.updated_at > b.updated_at) ? -1 : 1))
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setFound(true)
|
setFound(true)
|
||||||
|
|
@ -65,7 +71,7 @@ const ProfileRoute: React.FC = () => {
|
||||||
const content = (parties && parties.length > 0) ? renderGrids() : renderNoGrids()
|
const content = (parties && parties.length > 0) ? renderGrids() : renderNoGrids()
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{user.username}</h1>
|
<ProfileHeader username={user.username} gender={true} />
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -82,6 +88,9 @@ const ProfileRoute: React.FC = () => {
|
||||||
parties.map((party, i) => {
|
parties.map((party, i) => {
|
||||||
return <GridRep
|
return <GridRep
|
||||||
shortcode={party.shortcode}
|
shortcode={party.shortcode}
|
||||||
|
name={party.name}
|
||||||
|
updatedAt={new Date(party.updated_at)}
|
||||||
|
raid={party.raid}
|
||||||
grid={party.weapons}
|
grid={party.weapons}
|
||||||
key={`party-${i}`}
|
key={`party-${i}`}
|
||||||
onClick={goTo}
|
onClick={goTo}
|
||||||
|
|
|
||||||
BIN
public/profile/djeeta.png
Normal file
BIN
public/profile/djeeta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
public/profile/djeeta@2x.png
Normal file
BIN
public/profile/djeeta@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
public/profile/gran.png
Normal file
BIN
public/profile/gran.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
public/profile/gran@2x.png
Normal file
BIN
public/profile/gran@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -63,6 +63,7 @@ $font-button: 15px;
|
||||||
$font-regular: 16px;
|
$font-regular: 16px;
|
||||||
$font-large: 21px;
|
$font-large: 21px;
|
||||||
$font-xlarge: 24px;
|
$font-xlarge: 24px;
|
||||||
|
$font-xxlarge: 28px;
|
||||||
|
|
||||||
// Scale factors
|
// Scale factors
|
||||||
$scale-wide: scale(1.05, 1.05);
|
$scale-wide: scale(1.05, 1.05);
|
||||||
|
|
|
||||||
26
utils/timeAgo.tsx
Normal file
26
utils/timeAgo.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
const DIVISIONS = [
|
||||||
|
{ amount: 60, name: 'seconds' },
|
||||||
|
{ amount: 60, name: 'minutes' },
|
||||||
|
{ amount: 24, name: 'hours' },
|
||||||
|
{ amount: 7, name: 'days' },
|
||||||
|
{ amount: 4.34524, name: 'weeks' },
|
||||||
|
{ amount: 12, name: 'months' },
|
||||||
|
{ amount: Number.POSITIVE_INFINITY, name: 'years' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function formatTimeAgo(date: Date, locale: string = 'en-us') {
|
||||||
|
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
|
||||||
|
|
||||||
|
let duration = (date.getTime() - new Date().getTime()) / 1000
|
||||||
|
|
||||||
|
for (let i = 0; i <= DIVISIONS.length; i++) {
|
||||||
|
const division = DIVISIONS[i]
|
||||||
|
|
||||||
|
if (Math.abs(duration) < division.amount) {
|
||||||
|
return formatter.format(Math.round(duration), division.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration /= division.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue