Skip to content

Commit

Permalink
Merge pull request #364 from vevcom/feat/shop
Browse files Browse the repository at this point in the history
Feat/Shop
  • Loading branch information
theodorklauritzen authored Jan 27, 2025
2 parents c0ce0b1 + 32ade52 commit 8b85650
Show file tree
Hide file tree
Showing 66 changed files with 1,403 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs
Submodule docs updated from 97b8ef to 3cc94e
22 changes: 22 additions & 0 deletions src/actions/shop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use server'

import { action } from '@/actions/action'
import { createProduct, createProductForShop, createShopProductConnection } from '@/services/shop/product/create'
import { readProduct, readProducts } from '@/services/shop/product/read'
import { updateProduct, updateProductForShop } from '@/services/shop/product/update'
import { createShop } from '@/services/shop/shop/create'
import { readShop, readShops } from '@/services/shop/shop/read'

export const readShopsAction = action(readShops)
export const readShopAction = action(readShop)
export const createShopAction = action(createShop)

export const readProductsAction = action(readProducts)
export const readProductAction = action(readProduct)
export const createProductAction = action(createProduct)
export const updateProductAction = action(updateProduct)

export const createProductForShopAction = action(createProductForShop)
export const updateProductForShopAction = action(updateProductForShop)

export const createShopProductConnectionAction = action(createShopProductConnection)
11 changes: 10 additions & 1 deletion src/actions/users/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
import { action } from '@/actions/action'
import { safeServerCall } from '@/actions/safeServerCall'
import { createZodActionError, createActionError } from '@/actions/error'
import { registerUser, updateUserPassword, verifyUserEmail, registerNewEmail, updateUser } from '@/services/users/update'
import {
registerUser,
updateUserPassword,
verifyUserEmail,
registerNewEmail,
updateUser,
registerStudentCardInQueue
} from '@/services/users/update'
import { getUser } from '@/auth/getUser'
import {
registerUserValidation,
Expand Down Expand Up @@ -76,3 +83,5 @@ export async function verifyUserEmailAction(token: string): Promise<ActionReturn
return await verifyUserEmail(userId, email)
})
}

export const registerStudentCardInQueueAction = action(registerStudentCardInQueue)
19 changes: 18 additions & 1 deletion src/app/admin/SlideSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
faPaperPlane,
faSchool,
faDotCircle,
faListDots
faShop,
faListDots,
} from '@fortawesome/free-solid-svg-icons'
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import type { ReactNode } from 'react'
Expand Down Expand Up @@ -175,6 +176,22 @@ const navigations = [
},
]
},
{
header: {
icon: faShop,
title: 'Shop'
},
links: [
{
title: 'Butikker',
href: '/admin/shop'
},
{
title: 'Produkter',
href: '/admin/product'
},
]
},
{
header: {
title: 'Annet',
Expand Down
5 changes: 5 additions & 0 deletions src/app/admin/product/[productId]/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use '@/styles/ohma';

.table {
@include ohma.table();
}
41 changes: 41 additions & 0 deletions src/app/admin/product/[productId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import styles from './page.module.scss'
import ProductForm from '@/app/admin/product/productForm'
import { readProductAction } from '@/actions/shop'
import { unwrapActionReturn } from '@/app/redirectToErrorPage'
import PageWrapper from '@/app/_components/PageWrapper/PageWrapper'
import { v4 as uuid } from 'uuid'
import Link from 'next/link'
import { displayPrice } from '@/lib/money/convert'

export default async function ProductPage({
params
}: {
params: {
productId: number
}
}) {
const product = unwrapActionReturn(await readProductAction({ productId: Number(params.productId) }))

return <PageWrapper
title={product.name}
>
<ProductForm product={product} />

<table className={styles.table}>
<thead>
<tr>
<th>Navn</th>
<th>Aktiv</th>
<th>Pris</th>
</tr>
</thead>
<tbody>
{product.ShopProduct.map(shopProduct => <tr key={uuid()}>
<td><Link href={`/admin/shop/${shopProduct.shopId}`}>{shopProduct.shop.name}</Link></td>
<td>{shopProduct.active ? 'AKTIV' : 'INAKTIV'}</td>
<td>{displayPrice(shopProduct.price, false)}</td>
</tr>)}
</tbody>
</table>
</PageWrapper>
}
5 changes: 5 additions & 0 deletions src/app/admin/product/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use '@/styles/ohma';

.table {
@include ohma.table();
}
44 changes: 44 additions & 0 deletions src/app/admin/product/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use server'

import ProductForm from './productForm'
import styles from './page.module.scss'
import { readProductsAction } from '@/actions/shop'
import { AddHeaderItemPopUp } from '@/app/_components/HeaderItems/HeaderItemPopUp'
import PageWrapper from '@/app/_components/PageWrapper/PageWrapper'
import { unwrapActionReturn } from '@/app/redirectToErrorPage'
import { sortObjectsByName } from '@/lib/sortObjects'
import { v4 as uuid } from 'uuid'
import Link from 'next/link'


export default async function ProductPage() {
const products = unwrapActionReturn(await readProductsAction())

return <PageWrapper
title="Produkter"
headerItem={
<AddHeaderItemPopUp PopUpKey="ProductForm">
<ProductForm />
</AddHeaderItemPopUp>
}
>
<table className={styles.table}>
<thead>
<tr>
<th>Produkt</th>
<th>Beskrivelse</th>
<th>Strekkode</th>
</tr>
</thead>
<tbody>
{sortObjectsByName(products).map(product => <tr key={uuid()}>
<Link style={{ display: 'contents' }} href={`./product/${product.id}`} passHref>
<td>{product.name}</td>
<td>{product.description}</td>
<td>{product.barcode ?? ''}</td>
</Link>
</tr>)}
</tbody>
</table>
</PageWrapper>
}
25 changes: 25 additions & 0 deletions src/app/admin/product/productForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createProductAction, updateProductAction } from '@/actions/shop'
import Form from '@/app/_components/Form/Form'
import TextInput from '@/app/_components/UI/TextInput'
import type { Product } from '@prisma/client'


export default function ProductForm({
product
}: {
product?: Product
}) {
const submitForm = product
? updateProductAction
: createProductAction
return <Form
action={submitForm}
submitText={product ? 'Oppdater produkt' : 'Lag nytt produkt'}
refreshOnSuccess
>
<TextInput name="name" label="Navn" defaultValue={product?.name} />
<TextInput name="description" label="Beskrivelse" defaultValue={product?.description} />
<TextInput name="barcode" label="Strekkode" defaultValue={product?.barcode ? product.barcode : ''} />
{product && <input type="hidden" name="productId" value={product.id} /> }
</Form>
}
34 changes: 34 additions & 0 deletions src/app/admin/shop/[shop]/EditProductForShopForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createProductForShopAction, updateProductForShopAction } from '@/actions/shop'
import Form from '@/app/_components/Form/Form'
import Checkbox from '@/app/_components/UI/Checkbox'
import NumberInput from '@/app/_components/UI/NumberInput'
import TextInput from '@/app/_components/UI/TextInput'
import { displayPrice } from '@/lib/money/convert'
import type { ExtendedProduct } from '@/services/shop/product/Types'


export function EditProductForShopForm({
shopId,
product,
}: {
shopId: number,
product?: ExtendedProduct,
}) {
const submitAction = product
? updateProductForShopAction.bind(null, { shopId, productId: product.id })
: createProductForShopAction.bind(null, { shopId })

return <Form
action={submitAction}
submitText={product ? 'Oppdater produkt' : 'Lag nytt produkt'}
>
<TextInput name="name" label="Navn" defaultValue={product?.name}/>
<TextInput name="description" label="Beskrivelse" defaultValue={product?.description}/>
<TextInput name="barcode" label="Strekkode" defaultValue={product?.barcode ?? ''}/>
{product && <>
<input type="hidden" name="productId" value={product.id} />
<Checkbox name="active" label="Aktiv" defaultChecked={product?.active} />
</>}
<NumberInput name="price" label="Pris" defaultValue={product ? displayPrice(product.price, true) : undefined}/>
</Form>
}
29 changes: 29 additions & 0 deletions src/app/admin/shop/[shop]/FindProductForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'
import { createShopProductConnectionAction } from '@/actions/shop'
import Form from '@/app/_components/Form/Form'
import NumberInput from '@/app/_components/UI/NumberInput'
import { SelectNumber } from '@/app/_components/UI/Select'
import type { Product } from '@prisma/client'


export default function FindProductForm({
shopId,
products,
}: {
shopId: number,
products: Pick<Product, 'id' | 'name'>[]
}) {
if (products.length === 0) return <></>

return <Form
action={createShopProductConnectionAction}
submitText="Legg til produkt"
>
<SelectNumber name="productId" label="Produkt" options={products.map(product => ({
label: product.name,
value: product.id,
}))} />
<input type="hidden" name="shopId" value={shopId} />
<NumberInput name="price" label="Pris" />
</Form>
}
25 changes: 25 additions & 0 deletions src/app/admin/shop/[shop]/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use '@/styles/ohma';

.button {
@include ohma.btn(ohma.$colors-primary);
}

.table {
@include ohma.table();
}

.deactivatedProduct {
color: ohma.$colors-gray-600;
font-style: italic;
}

.editButtonWrapper {
text-align: center;

> button {
width: 30px;
height: 30px;
border: none;
background: transparent;
}
}
81 changes: 81 additions & 0 deletions src/app/admin/shop/[shop]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { EditProductForShopForm } from './EditProductForShopForm'
import styles from './page.module.scss'
import FindProductForm from './FindProductForm'
import { readProductsAction, readShopAction } from '@/actions/shop'
import PageWrapper from '@/app/_components/PageWrapper/PageWrapper'
import PopUp from '@/app/_components/PopUp/PopUp'
import { unwrapActionReturn } from '@/app/redirectToErrorPage'
import { displayPrice } from '@/lib/money/convert'
import { sortObjectsByName } from '@/lib/sortObjects'
import { faPencil } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { notFound } from 'next/navigation'
import { v4 as uuid } from 'uuid'
import type { Product } from '@prisma/client'

export default async function Shop(params: {
params: {
shop: string
}
}) {
const shopId = parseInt(params.params.shop, 10)
if (isNaN(shopId)) notFound()

const shopData = unwrapActionReturn(await readShopAction({
shopId,
}))

if (!shopData) notFound()

const allProducts = unwrapActionReturn(await readProductsAction())
let unconnectedProducts: Product[] = []
if (allProducts) {
const existingProductIds = new Set(shopData.products.map(p => p.id))
unconnectedProducts = allProducts.filter(product => !existingProductIds.has(product.id))
}

return <PageWrapper
title={shopData.name}
>
<p>{shopData.description}</p>

<PopUp
showButtonClass={styles.button}
showButtonContent="Legg til Produkt"
PopUpKey="createProductForShop"
>
<FindProductForm shopId={shopId} products={unconnectedProducts} />
<br />
<EditProductForShopForm shopId={shopId} />
</PopUp>

<table className={styles.table}>
<thead>
<tr>
<th>Rediger</th>
<th>Produkt</th>
<th>Beskrivelse</th>
<th>Pris</th>
</tr>
</thead>
<tbody>
{sortObjectsByName(shopData.products).map(product => <tr
key={uuid()}
className={product.active ? '' : styles.deactivatedProduct}
>
<td className={styles.editButtonWrapper}>
<PopUp
showButtonContent={<FontAwesomeIcon icon={faPencil} />}
PopUpKey={'EditProductForShop'}
>
<EditProductForShopForm shopId={shopId} product={product} />
</PopUp>
</td>
<td>{product.name}</td>
<td>{product.description}</td>
<td>{displayPrice(product.price, false)}</td>
</tr>)}
</tbody>
</table>
</PageWrapper>
}
5 changes: 5 additions & 0 deletions src/app/admin/shop/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use '@/styles/ohma';

.table {
@include ohma.table();
}
Loading

0 comments on commit 8b85650

Please sign in to comment.