Skip to content

Commit

Permalink
Added activity page (#127)
Browse files Browse the repository at this point in the history
* Added activity page layout

* Changed input box sizing

* Added progress bar component and help text

* Added activity level indicator and translations

* Added select initial selection side-effect

* Prepared update to SDK 3.0.6

* Updated SDK to 3.0.5 and added functional open/close button

* Split activity page into smaller components and added dynamic activity buttons

* Made all activity page elements dynamic

* Improved external link style

* Added dynamic data to progress bar

* Formatted code

Co-authored-by: Frewacom <[email protected]>
  • Loading branch information
Frewacom and Frewacom authored Jul 9, 2021
1 parent 56cfa48 commit 79cefe4
Show file tree
Hide file tree
Showing 35 changed files with 755 additions and 122 deletions.
11 changes: 2 additions & 9 deletions components/admin/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,11 @@ import GlobalSidebar from '@common/GlobalSidebar'
import AdminSettingsModal from '@components/admin/AdminSettingsModal'

const AdminSidebar = () => {
const { token, oid, logout } = useAuth()
const { user, logout } = useAuth()
const [settingsOpen, setSettingsOpen] = useState(false)

// Make sure to "logout" if we are not authenticated
// and prevent fetching data
if (!token || !oid) {
logout()
return null
}

// TODO: Handle error
const { data } = useNation(oid)
const { data } = useNation(user.oid)

return (
<GlobalSidebar>
Expand Down
25 changes: 21 additions & 4 deletions components/admin/UserPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import React, { useMemo } from 'react'
import Link from 'next/link'
import { useAuth } from '@contexts/Auth'
import { useTranslation } from 'next-i18next'
import { UserIcon, ArrowRightIcon } from '@heroicons/react/outline'

Expand All @@ -8,14 +9,30 @@ import Popover from '@common/Popover'
import PopoverSection from '@common/PopoverSection'

const UserPopover = () => {
const { user } = useAuth()
const { t } = useTranslation(['admin-common', 'common'])

const initials = useMemo(() => {
let acc = ''
const strings = user.full_name.split(' ')

for (const str of strings) {
if (str.length === 0) {
continue
}

acc += str[0].toUpperCase()
}

return acc
}, [user])

return (
<Popover
cardClassName="w-user-popover"
button={() => (
<Button style="secondary" size="medium" radius="large" className="w-10 h-10">
<p className="font-bold text-white">FE</p>
<p className="font-bold text-white">{initials}</p>
</Button>
)}
>
Expand All @@ -24,9 +41,9 @@ const UserPopover = () => {
<UserIcon className="w-5 h-5" />
</div>
<div>
<p className="font-bold leading-none text-text-highlight">Fredrik Engstrand</p>
<p className="font-bold leading-none text-text-highlight">{user.full_name}</p>
<p className="text-sm font-bold text-primary-text">
{t('common:auth.role.admin')}
{user.nation_admin ? t('common:auth.role.admin') : 'common:auth.role.staff'}
</p>
</div>
</PopoverSection>
Expand Down
42 changes: 26 additions & 16 deletions components/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,56 @@ import Router from 'next/router'
import { AUTH } from '@constants'
import { isClient } from '@utils'
import { AuthProvider } from '@contexts/Auth'
import { useApi, AuthenticatedUser } from '@nationskollen/sdk'

import LoadingIndicator from '@common/LoadingIndicator'

export interface Props {
redirectTo?: string
children: React.ReactNode
}

const ProtectedRoute = ({ redirectTo, children }: Props) => {
const [oid, setOid] = useState<number | null>(null)
const [token, setToken] = useState<string | null>(null)
const api = useApi()
const [user, setUser] = useState<AuthenticatedUser>()

const logout = useCallback(() => {
localStorage.removeItem(AUTH.TOKEN_STORAGE_KEY)
Router.replace(redirectTo || '/admin/login')
}, [])

const authenticate = async (token: string) => {
try {
const user = await api.auth.setToken(token)
setUser(user)
} catch (_) {
logout()
}
}

useEffect(() => {
if (isClient()) {
// Saving the token inside localStorage is bad practice.
// In the future, it would be better to use server sessions.
// Sessions also allow us to check if the user is logged in directly on the
// server, which will remove the flashing content.
const user = localStorage.getItem(AUTH.USER_STORAGE_KEY)
const token = localStorage.getItem(AUTH.TOKEN_STORAGE_KEY)

if (!user) {
Router.push(redirectTo || AUTH.DEFAULT_REDIRECT_ROUTE)
if (token) {
authenticate(token)
} else {
const parsed = JSON.parse(user)
setToken(parsed.token)
setOid(parseInt(parsed.oid))
logout()
}
}
}, [])

const logout = useCallback(() => {
localStorage.removeItem(AUTH.USER_STORAGE_KEY)
Router.replace('/admin/login')
}, [])

return (
<AuthProvider value={{ token: token as string, oid: oid as number, logout }}>
{token ? (
<AuthProvider value={{ user: user as AuthenticatedUser, logout }}>
{user ? (
children
) : (
<div className="flex items-center justify-center w-screen h-screen">
<p className="text-white">Laddar...</p>
<LoadingIndicator size="medium" />
</div>
)}
</AuthProvider>
Expand Down
58 changes: 58 additions & 0 deletions components/common/ActivityLevel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import clsx from 'clsx'
import React from 'react'
import { useTranslation, TFunction } from 'next-i18next'
import { useActivity, Location, ActivityLevels } from '@nationskollen/sdk'

export interface Props {
location: Location
}

const ACTIVITY_COLORS: Record<ActivityLevels, string> = {
[ActivityLevels.Closed]: 'bg-activity-closed',
[ActivityLevels.Low]: 'bg-activity-low',
[ActivityLevels.Medium]: 'bg-activity-medium',
[ActivityLevels.High]: 'bg-activity-high',
[ActivityLevels.VeryHigh]: 'bg-activity-very-high',
[ActivityLevels.Full]: 'bg-activity-full',
}

const getActivityString = (level: ActivityLevels, t: TFunction) => {
switch (level) {
case ActivityLevels.Closed:
return t('common:activity.level.closed')
case ActivityLevels.Low:
return t('common:activity.level.low')
case ActivityLevels.Medium:
return t('common:activity.level.medium')
case ActivityLevels.High:
return t('common:activity.level.high')
case ActivityLevels.VeryHigh:
return t('common:activity.level.very_high')
case ActivityLevels.Full:
return t('common:activity.level.full')
}
}

const ActivityLevel = ({ location }: Props) => {
const { t } = useTranslation('common')
const { level } = useActivity(location.id, { level: location.activity_level })
const color = ACTIVITY_COLORS[level]

return (
<div className="flex flex-row items-center px-md">
<span className={clsx('relative w-2 h-2 rounded-full mr-sm', color)}>
{level !== ActivityLevels.Closed && (
<span
className={clsx(
'absolute inset-0 w-full h-full rounded-full animate-ping',
color
)}
/>
)}
</span>
<p className="font-bold text-text-highlight">{getActivityString(level, t)}</p>
</div>
)
}

export default ActivityLevel
58 changes: 39 additions & 19 deletions components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ import { getFieldErrorMessage } from '@utils'
import { INPUT_FOCUS_STYLES } from '@common/Input'
import LoadingIndicator from '@common/LoadingIndicator'

export type ButtonRadius = 'default' | 'large'
export type ButtonSizes = 'small' | 'medium' | 'default' | 'large' | 'icon' | 'icon-small'
export type ButtonDirections = 'row' | 'column'
export type ButtonRadius = 'default' | 'large' | 'round'
export type ButtonSizes = 'small' | 'medium' | 'default' | 'large' | 'icon' | 'icon-small' | 'auto'
export type ButtonFocusStyles = 'primary' | 'default' | 'subtle' | 'error' | 'input'

export type ButtonStyles =
Expand All @@ -45,6 +46,7 @@ export type ButtonStyles =
| 'error'
| 'error-border'
| 'success'
| 'success-border'
| 'input'
| 'border'

Expand All @@ -62,6 +64,7 @@ export interface Props extends NativeButtonProps {
style?: ButtonStyles
leftAlignContent?: boolean
loading?: boolean
direction?: ButtonDirections
error?: boolean | FieldError
className?: string
containerClassName?: string
Expand All @@ -70,40 +73,52 @@ export interface Props extends NativeButtonProps {
}

const BUTTON_STYLES: Record<ButtonStyles, string> = {
'primary': clsx('bg-primary text-white', 'focus:ring-focus-primary hover:bg-primary-extra'),
'primary-extra': clsx('bg-primary-extra text-white', 'focus:ring-focus-primary'),
'primary': clsx(
'bg-primary text-white border-transparent',
'focus:ring-focus-primary hover:bg-primary-extra'
),
'primary-extra': clsx(
'bg-primary-extra text-white border-transparent',
'focus:ring-focus-primary'
),
'secondary': clsx(
'bg-secondary text-white',
'bg-secondary text-white border-transparent',
'hover:bg-secondary-extra focus:ring-focus-secondary'
),
'light': clsx(
'bg-background-extra text-text-highlight border-1 border-border-dark',
'bg-background-extra border-border-dark text-text-highlight',
'dark:bg-background-highlight dark:border-background-highlight',
'focus:ring-focus-default'
'focus:ring-focus-default hover:bg-border'
),
'lighter': clsx(
'bg-transparent text-text-highlight focus:ring-focus-default',
'hover:bg-hover'
'bg-transparent border-transparent text-text-highlight',
'hover:bg-hover focus:ring-focus-default'
),
'transparent': clsx('bg-transparent', 'focus:ring-focus-default'),
'transparent': clsx('bg-transparent border-transparent', 'focus:ring-focus-default'),
'error': clsx(
'bg-error text-white',
'bg-error border-transparent text-white',
'hover:filter hover:brightness-125 focus:ring-focus-error'
),
'error-border': clsx(
'bg-transparent text-error-text border-2 border-border',
'hover:border-border-dark hover:bg-background-extra hover:dark:bg-background-highlight'
'bg-transparent text-error-highlight-text border-border-dark',
'hover:border-error-highlight hover:bg-error-highlight',
'focus:ring-focus-error'
),
'success': clsx(
'bg-success text-white',
'bg-success border-transparent text-white',
'hover:filter hover:brightness-125 focus:ring-focus-success'
),
'success-border': clsx(
'bg-transparent text-success-highlight-text border-border-dark',
'hover:border-success-highlight hover:bg-success-highlight',
'focus:ring-focus-success'
),
'input': clsx(
'bg-transparent text-text-highlight border-1 border-border-dark',
'bg-transparent text-text-highlight border-border-dark',
'dark:bg-background-highlight dark:border-0'
),
'border': clsx(
'bg-transparent text-text border-2 border-border',
'bg-transparent text-text border-border-dark',
'hover:text-text-highlight hover:border-border-dark'
),
}
Expand All @@ -122,6 +137,7 @@ const BUTTON_FOCUS_STYLES: Record<ButtonFocusStyles, string> = {
const BUTTON_RADIUS: Record<ButtonRadius, string> = {
default: 'rounded-sm',
large: 'rounded',
round: 'rounded-full',
}

const BUTTON_SIZES: Record<ButtonSizes, string> = {
Expand All @@ -130,7 +146,8 @@ const BUTTON_SIZES: Record<ButtonSizes, string> = {
'default': 'h-12 p-3 px-md space-x-sm',
'large': 'h-14 text-lg p-4 space-x-2',
'icon': 'h-12 w-12 p-3',
'icon-small': 'h-9 w-9 p-2',
'icon-small': 'h-10 w-10 p-2',
'auto': 'h-auto px-md py-3 space-x-sm',
}

// We use forwardRef here so that our buttons can be used as children
Expand All @@ -144,6 +161,7 @@ const Button = React.forwardRef(
type,
href,
style,
direction,
leftAlignContent,
radius,
loading,
Expand All @@ -156,7 +174,7 @@ const Button = React.forwardRef(
}: Props,
ref: any
) => {
const sizing = BUTTON_SIZES[size || 'default']
const sizing = BUTTON_SIZES[direction === 'column' ? 'auto' : size || 'default']
const radiusStyle = BUTTON_RADIUS[radius || 'default']
const colorStyle = BUTTON_STYLES[style || 'primary']
let focusStyle = ''
Expand All @@ -171,7 +189,8 @@ const Button = React.forwardRef(
}

const classes = clsx(
'focus:ring focus:outline-none font-bold',
'focus:ring focus:outline-none font-bold border-1',
'transition-colors duration-out',
colorStyle,
focusStyle,
radiusStyle,
Expand All @@ -184,6 +203,7 @@ const Button = React.forwardRef(
className={clsx(
'overflow-hidden relative flex flex-row items-center',
leftAlignContent ? 'justify-start' : 'justify-center',
direction === 'column' ? 'flex-col space-y-sm' : 'flex-row',
sizing,
containerClassName
)}
Expand Down
4 changes: 2 additions & 2 deletions components/common/ExternalLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export interface Props {
const ExternalLink = ({ href, label }: Props) => {
return (
<Link href={href}>
<a className="flex flex-row items-center w-full h-10 font-bold py-sm space-x-sm hover:text-primary-text">
<a className="flex flex-row items-center h-10 font-bold py-sm space-x-sm hover:underline">
<span>{label}</span>
<ExternalLinkIcon className="w-6 h-6" />
<ExternalLinkIcon className="w-5 h-5" />
</a>
</Link>
)
Expand Down
2 changes: 1 addition & 1 deletion components/common/IconCircle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface Props {
const ICON_CIRCLE_SIZES: Record<IconCircleSizes, string> = {
small: 'h-9 w-9 p-sm',
default: 'h-10 w-10 p-sm',
large: 'h-14 w-14 p-md',
large: 'h-16 w-16 p-3',
}

const ICON_CIRCLE_STYLES: Record<IconCircleStyles, string> = {
Expand Down
Loading

0 comments on commit 79cefe4

Please sign in to comment.