Skip to content

Commit

Permalink
feat: added select trainer
Browse files Browse the repository at this point in the history
Took 1 minute
  • Loading branch information
rokartur committed Jun 23, 2024
1 parent a1a76cb commit f9f2c1d
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 3 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ FitPlan Connect is a scheduling app that has earned recognition as one of the be
- [GitHub OAuth](https://docs.github.com/en/apps)

## 🇵🇱 Summary
### Concept

### Design

### Technology

### Implementation


## Resources
Expand Down
43 changes: 42 additions & 1 deletion backend/src/routes/trainer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,45 @@ import { validateRequest } from '@/utils/validateRequest'
import { t } from 'elysia'

export default (app: ElysiaApp) =>
app
app.patch('/:trainerID', async ({ set, params: { trainerID }, cookie: { auth_session } }) => {
const userSession = await db.query.sessions.findFirst({ where: eq(sessions.id, auth_session.value) })
const { user, session } = await validateRequest(auth_session)

if (user && session && userSession) {
const userData = await db.query.users.findFirst({ where: eq(users.id, userSession.userId) })

if (userData?.accessToken) {
const trainer = await db.query.trainers.findFirst({ where: eq(trainers.id, trainerID) })

if (!trainer) {
set.status = 404
return { message: 'trainer not found' }
}

const user = await db
.update(users)
.set({ selectedTrainerId: trainer.id })
.where(eq(users.accessToken, userData.accessToken))
.returning({ id: users.id })

if (user.length === 0) {
set.status = 403
return { message: 'forbidden' }
}

set.status = 200
}

if (!userData) {
set.status = 401
return { message: 'unauthorized' }
}
} else {
set.status = 401
return { message: 'unauthorized' }
}
}, {
params: t.Object({
trainerID: t.String()
})
})
2 changes: 1 addition & 1 deletion backend/src/routes/trainers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default (app: ElysiaApp) =>
name: trainers.name,
username: trainers.username,
email: trainers.email,
profilePicture: trainers.profilePictureUrl,
profile_picture_url: trainers.profilePictureUrl,
}).from(trainers)
set.status = 200
return data
Expand Down
3 changes: 2 additions & 1 deletion website/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ gsap.registerPlugin(useGSAP, ScrollTrigger)

const Settings = lazy(() => import('@/pages/app/settings'))
const Calendar = lazy(() => import('@/pages/app/calendar'))
const Trainers = lazy(() => import('@/pages/app/trainers'))
const Billing = lazy(() => import('@/pages/app/billing'))
const BillingComplete = lazy(() => import('@/pages/app/billing.complete'))
const BillingCancel = lazy(() => import('@/pages/app/billing.cancel'))
Expand All @@ -28,7 +29,7 @@ const MemoizedRoutes = memo(() => (
<Routes>
<Route path={'/'} element={<h1>landing</h1>} />
<Route path={'/app/calendar'} element={<Calendar />} />
<Route path={'/app/trainers'} element={<h1>trainers</h1>} />
<Route path={'/app/trainers'} element={<Trainers/>} />
<Route path={'/app/billing'} element={<Billing/>} />
<Route path={'/app/billing/complete'} element={<BillingComplete/>} />
<Route path={'/app/billing/cancel'} element={<BillingCancel/>} />
Expand Down
113 changes: 113 additions & 0 deletions website/src/pages/app/trainers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import styles from '@/styles/trainers.module.scss'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAppSelector } from '@/utils/store.ts'
import moment from 'moment'
import { SEO } from '@/components/seo'
import { Overlay } from '@/components/overlay/overlay'
import { AnimateWrapper } from '@/components/animateWrapper/animateWrapper'
import { Container } from '@/components/container/container'
import { Image } from '@/components/image'
import wretch from 'wretch'
import Swal from 'sweetalert2'

const metaData = {
title: 'Trainers',
path: '/app/trainers',
}

export default function Trainers() {
const navigate = useNavigate()
const user = useAppSelector(state => state.user.data)
const trainers = useAppSelector(state => state.trainers.data)

useEffect(() => {
if (isNaN(moment(user?.subscription_expiration_date).unix())) {
navigate('/app/billing')
} else if (moment().unix() > moment(user?.subscription_expiration_date).unix()) {
navigate('/app/billing')
}
}, [user])

return (
<>
<SEO title={metaData.title} path={metaData.path} />

<Overlay>
<AnimateWrapper>
<Container>
<div className={styles.content}>

{trainers?.map(({ id, name, username, profile_picture_url }) => (
<div key={id} className={styles.trainerCard} onClick={async () => {
if (user?.selected_trainer_id === id) {
Swal.fire({
title: 'Trainer already selected',
icon: 'warning',
})
return
} else {
await wretch(`/api/trainer/${id}`)
.patch()
.res(res => {
if (res.status === 200) {
Swal.fire({
title: 'Trainer selected successfully',
icon: 'success',
}).then(() => location.reload())
}
})
.catch(() => {
Swal.fire({
title: 'An error occurred',
icon: 'error',
}).then(() => location.reload())
})
}
}}>
<Image source={profile_picture_url} />

<div className={styles.trainerShadow}/>

{user?.selected_trainer_id === id && <div className={styles.selectedTrainer}>
<svg width='24' height='24' viewBox='0 0 24 24' fill='none'
xmlns='http://www.w3.org/2000/svg'>
<path fillRule='evenodd' clipRule='evenodd'
d='M15.5837 10.5577L11.5107 14.6327C11.3697 14.7737 11.1787 14.8527 10.9797 14.8527C10.7807 14.8527 10.5897 14.7737 10.4497 14.6327L8.47274 12.6527C8.18075 12.3587 8.18075 11.8837 8.47375 11.5907C8.76775 11.2987 9.24174 11.2997 9.53475 11.5917L10.9807 13.0407L14.5227 9.49667C14.8157 9.20367 15.2907 9.20367 15.5837 9.49667C15.8767 9.78967 15.8767 10.2647 15.5837 10.5577ZM20.7037 10.0857L20.0047 9.38667C19.6857 9.06667 19.5107 8.64067 19.5107 8.18967V7.18967C19.5107 5.70067 18.2987 4.48967 16.8107 4.48967H15.8087C15.3557 4.48967 14.9307 4.31467 14.6127 3.99667L13.9017 3.28667C12.8447 2.23867 11.1327 2.24367 10.0837 3.29767L9.38674 3.99667C9.06574 4.31567 8.64075 4.49067 8.18875 4.49067H7.18775C5.71675 4.49167 4.51675 5.67567 4.48975 7.14167C4.48874 7.15767 4.48775 7.17367 4.48775 7.19067V8.18767C4.48775 8.63967 4.31275 9.06467 3.99375 9.38367L3.28575 10.0927C3.28475 10.0957 3.28175 10.0967 3.27975 10.0987C2.23475 11.1567 2.24375 12.8687 3.29675 13.9117L3.99575 14.6127C4.31375 14.9317 4.48975 15.3557 4.48975 15.8077V16.8127C4.48975 18.3007 5.69975 19.5117 7.18775 19.5117H8.18675C8.63975 19.5127 9.06475 19.6877 9.38275 20.0047L10.0957 20.7157C10.6037 21.2207 11.2777 21.4987 11.9947 21.4987H12.0067C12.7277 21.4957 13.4037 21.2117 13.9097 20.7027L14.6107 20.0027C14.9257 19.6887 15.3617 19.5087 15.8067 19.5087H16.8127C18.2977 19.5087 19.5087 18.2997 19.5117 16.8127V15.8097C19.5117 15.3587 19.6867 14.9337 20.0037 14.6147L20.7147 13.9037C21.7647 12.8477 21.7587 11.1347 20.7037 10.0857Z'
fill='white' />
</svg>
</div>}

<div className={styles.trainerInfo}>
<button>
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path fillRule='evenodd' clipRule='evenodd'
d='M7.8401 2.5C4.9238 2.5 2.5601 4.96219 2.5601 8C2.5601 11.0372 4.92384 13.5 7.8401 13.5C10.7564 13.5 13.1201 11.0372 13.1201 8C13.1201 4.96219 10.7564 2.5 7.8401 2.5ZM1.6001 8C1.6001 4.40991 4.39361 1.5 7.8401 1.5C11.2866 1.5 14.0801 4.40991 14.0801 8C14.0801 11.5894 11.2866 14.5 7.8401 14.5C4.39358 14.5 1.6001 11.5894 1.6001 8Z'
fill='white' />
<path fillRule='evenodd' clipRule='evenodd'
d='M7.83986 5.02734C8.10495 5.02734 8.31986 5.2512 8.31986 5.52734V5.5695C8.31986 5.84564 8.10495 6.0695 7.83986 6.0695C7.57478 6.0695 7.35986 5.84564 7.35986 5.5695V5.52734C7.35986 5.2512 7.57478 5.02734 7.83986 5.02734ZM7.84351 7.09573C8.1086 7.09573 8.32351 7.3196 8.32351 7.59573V10.4621C8.32351 10.7383 8.1086 10.9621 7.84351 10.9621C7.57842 10.9621 7.36351 10.7383 7.36351 10.4621V7.59573C7.36351 7.3196 7.57842 7.09573 7.84351 7.09573Z'
fill='white' />
</svg>

<p>{name ? `${name} (${username})` : username}</p>
</button>

<button
className={user?.selected_trainer_id === id ? styles.trainerSelectedButton : styles.trainerSelectButton}>
<p>{user?.selected_trainer_id === id ? 'Selected' : 'Select'}</p>
<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'>
<path fillRule='evenodd' clipRule='evenodd'
d="M7.8332 3C7.77253 3 7.6232 3.01667 7.54386 3.17533L6.32653 5.60933C6.13386 5.994 5.76253 6.26133 5.3332 6.32267L2.60786 6.71533C2.42786 6.74133 2.36653 6.87467 2.34786 6.93067C2.3312 6.98467 2.30453 7.122 2.42853 7.24067L4.3992 9.134C4.7132 9.436 4.85586 9.87133 4.7812 10.2973L4.3172 12.9707C4.28853 13.138 4.3932 13.2353 4.43986 13.2687C4.4892 13.306 4.6212 13.38 4.78453 13.2947L7.2212 12.0313C7.6052 11.8333 8.06253 11.8333 8.4452 12.0313L10.8812 13.294C11.0452 13.3787 11.1772 13.3047 11.2272 13.2687C11.2739 13.2353 11.3785 13.138 11.3499 12.9707L10.8845 10.2973C10.8099 9.87133 10.9525 9.436 11.2665 9.134L13.2372 7.24067C13.3619 7.122 13.3352 6.984 13.3179 6.93067C13.2999 6.87467 13.2385 6.74133 13.0585 6.71533L10.3332 6.32267C9.90453 6.26133 9.5332 5.994 9.34053 5.60867L8.12186 3.17533C8.0432 3.01667 7.89386 3 7.8332 3ZM4.6312 14.3333C4.35586 14.3333 4.08253 14.2467 3.84853 14.076C3.44453 13.78 3.24653 13.2913 3.33253 12.7993L3.79653 10.126C3.81386 10.0267 3.77986 9.926 3.70653 9.85533L1.73586 7.962C1.3732 7.61467 1.2432 7.10133 1.39653 6.62467C1.5512 6.14267 1.96053 5.798 2.4652 5.726L5.19053 5.33333C5.29586 5.31867 5.38653 5.254 5.43186 5.162L6.64986 2.72733C6.87453 2.27867 7.32786 2 7.8332 2C8.33853 2 8.79186 2.27867 9.01653 2.72733L10.2352 5.16133C10.2812 5.254 10.3712 5.31867 10.4759 5.33333L13.2012 5.726C13.7059 5.798 14.1152 6.14267 14.2699 6.62467C14.4232 7.10133 14.2925 7.61467 13.9299 7.962L11.9592 9.85533C11.8859 9.926 11.8525 10.0267 11.8699 10.1253L12.3345 12.7993C12.4199 13.292 12.2219 13.7807 11.8172 14.076C11.4072 14.3767 10.8732 14.4173 10.4205 14.1813L7.9852 12.9193C7.88986 12.87 7.77586 12.87 7.68053 12.9193L5.2452 14.182C5.05053 14.2833 4.84053 14.3333 4.6312 14.3333Z" fill="#FBBF24" />
</svg>
</button>
</div>
</div>
))}

</div>
</Container>
</AnimateWrapper>
</Overlay>
</>
)
}
165 changes: 165 additions & 0 deletions website/src/styles/trainers.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
@import "variables";
@import "mixins";

.content {
padding: 0 0 64px 0;

position: relative;
@include flex(row, center, start);
gap: 48px;
flex-wrap: wrap;

@media (width <= 768px) {
flex-direction: column;
align-items: center;
}
}

.trainerCard {
position: relative;
height: 100%;
max-height: 336px;

display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1 0;

border-radius: 16px;
box-shadow: $drop-shadow-xlarge;
overflow: hidden;
cursor: pointer;

&:is(:hover) {
.trainerShadow {
opacity: 1;
}

.trainerInfo {
opacity: 1;
visibility: initial;
transform: translateY(0);
}

img {
transform: scale(1.1);
}
}

img {
width: 100%;
height: 100%;
border-radius: 16px;
object-fit: cover;

transition: transform .3s;
}

.selectedTrainer {
position: absolute;
width: 40px;
height: 40px;
background: $yellow-500;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
top: 16px;
right: 16px;

svg {
width: 24px;
height: 24px;
path {
fill: $white;
}
}
}

.trainerShadow {
opacity: 0;

position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, .70) 100%);
border-radius: 16px;

transition: opacity .3s, visibility .3s, transform .3s;
}

.trainerInfo {
opacity: 0;
visibility: hidden;
transform: translateY(-48px);

width: calc(100% - 32px);
position: absolute;
bottom: 16px;

display: flex;
justify-content: space-between;
align-items: center;

transition: opacity .3s, visibility .3s, transform .3s;

@media (width <= 1144px) {
justify-content: center;
flex-direction: column;
gap: 16px;
}

button:nth-child(1) {
padding: 4px 8px;

display: flex;
justify-content: center;
align-items: center;
gap: 8px;

border-radius: 40px;
background: rgba(249, 250, 251, 0.18);
cursor: pointer;

font: 500 14px/16px $font-family;
color: $white;
}

.trainerSelectButton {
padding: 4px 12px;

display: flex;
justify-content: center;
align-items: center;
gap: 4px;

border-radius: 40px;
background: rgba(249, 250, 251, 0.18);
cursor: pointer;

font: 500 14px/16px $font-family;
color: $white;
}

.trainerSelectedButton {
padding: 4px 12px;

display: flex;
justify-content: center;
align-items: center;
gap: 4px;

border-radius: 40px;
background: $yellow-500;
cursor: pointer;

font: 500 14px/16px $font-family;
color: $white;

svg path {
fill: $white;
}
}
}
}

0 comments on commit f9f2c1d

Please sign in to comment.