diff --git a/src/actions/images/create.ts b/src/actions/images/create.ts index 910286657..6518da918 100644 --- a/src/actions/images/create.ts +++ b/src/actions/images/create.ts @@ -1,49 +1,7 @@ 'use server' -import { safeServerCall } from '@/actions/safeServerCall' -import { createZodActionError } from '@/actions/error' -import { createImage } from '@/services/images/create' -import { createImagesValidation, createImageValidation } from '@/services/images/validation' -import type { ActionReturn } from '@/actions/Types' -import type { Image } from '@prisma/client' -import type { CreateImageTypes, CreateImagesTypes } from '@/services/images/validation' +import { Action } from '@/actions/Action' +import { Images } from '@/services/images' -export async function createImageAction( - collectionId: number, - rawdata: FormData | CreateImageTypes['Type'] -): Promise> { - //TODO: add auth +export const createImageAction = Action(Images.create) - const parse = createImageValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => createImage({ ...data, collectionId })) -} - -export async function createImagesAction( - useFileName: boolean, - collectionId: number, - rawdata: FormData | CreateImagesTypes['Type'] -): Promise> { - //TODO: add auth - - const parse = createImagesValidation.typeValidate(rawdata) - - if (!parse.success) return createZodActionError(parse) - - const data = parse.data - - let finalReturn: ActionReturn = { success: true, data: undefined } - for (const file of data.files) { - const name = useFileName ? file.name.split('.')[0] : undefined - const ret = await safeServerCall( - () => createImage({ file, name, alt: file.name.split('.')[0], collectionId }) - ) - if (!ret.success) return ret - finalReturn = { - ...finalReturn, - success: ret.success, - } - } - return finalReturn -} +export const createImagesAction = Action(Images.createMany) diff --git a/src/actions/images/read.ts b/src/actions/images/read.ts index 6c65701db..2711802eb 100644 --- a/src/actions/images/read.ts +++ b/src/actions/images/read.ts @@ -1,56 +1,19 @@ 'use server' -import { safeServerCall } from '@/actions/safeServerCall' -import { createActionError } from '@/actions/error' -import { readImage, readImagesPage, readSpecialImage } from '@/services/images/read' -import { createBadImage } from '@/services/images/create' -import { SpecialImage } from '@prisma/client' -import type { Image } from '@prisma/client' -import type { ReadPageInput } from '@/lib/paging/Types' -import type { ActionReturn } from '@/actions/Types' -import type { ImageDetails, ImageCursor } from '@/services/images/Types' +import { ActionNoData } from '@/actions/Action' +import { Images } from '@/services/images' + /** * Read one page of images. - * @param pageReadInput - the page with details and page. - * @returns */ -export async function readImagesPageAction( - pageReadInput: ReadPageInput -): Promise> { - //TODO: auth route based on collection - return await safeServerCall(() => readImagesPage(pageReadInput)) -} +export const readImagesPageAction = ActionNoData(Images.readPage) /** * Read one image. - * @param nameOrId - the name or id of the image to read - * @returns - */ -export async function readImageAction(id: number): Promise> { - //TODO: auth route based on collection - return await safeServerCall(() => readImage(id)) -} +*/ +export const readImageAction = ActionNoData(Images.read) /** - * Action that reads a "special" image - read on this in the docs. If it does not exist it will create it, but - * its conntent will not be the intended content. This is NOT under any circomstainses supposed to happen - * @param special - the special image to read - * @returns the special image + * Read one special image. */ -export async function readSpecialImageAction(special: SpecialImage): Promise> { - if (!Object.values(SpecialImage).includes(special)) { - return createActionError('BAD PARAMETERS', `${special} is not special`) - } - //TODO: auth image based on collection - const imageRes = await safeServerCall(() => readSpecialImage(special)) - if (!imageRes.success) { - if (imageRes.errorCode === 'NOT FOUND') { - return await safeServerCall(() => createBadImage(special, { - special, - })) - } - return imageRes - } - const image = imageRes.data - return { success: true, data: image } -} +export const readSpecialImageAction = ActionNoData(Images.readSpecial) diff --git a/src/actions/licenses/create.ts b/src/actions/licenses/create.ts new file mode 100644 index 000000000..0cf2bd68b --- /dev/null +++ b/src/actions/licenses/create.ts @@ -0,0 +1,5 @@ +'use server' +import { Action } from '@/actions/Action' +import { Licenses } from '@/services/licenses' + +export const createLicenseAction = Action(Licenses.create) diff --git a/src/actions/licenses/destroy.ts b/src/actions/licenses/destroy.ts new file mode 100644 index 000000000..953306e6f --- /dev/null +++ b/src/actions/licenses/destroy.ts @@ -0,0 +1,5 @@ +'use server' +import { ActionNoData } from '@/actions/Action' +import { Licenses } from '@/services/licenses' + +export const destroyLicenseAction = ActionNoData(Licenses.destroy) diff --git a/src/actions/licenses/read.ts b/src/actions/licenses/read.ts new file mode 100644 index 000000000..143c3efb5 --- /dev/null +++ b/src/actions/licenses/read.ts @@ -0,0 +1,7 @@ +'use server' + +import { ActionNoData } from '@/actions/Action' +import { Licenses } from '@/services/licenses' + + +export const readLicensesAction = ActionNoData(Licenses.readAll) diff --git a/src/actions/licenses/update.ts b/src/actions/licenses/update.ts new file mode 100644 index 000000000..e279b56dd --- /dev/null +++ b/src/actions/licenses/update.ts @@ -0,0 +1,6 @@ +'use server' + +import { Licenses } from '@/services/licenses' +import { Action } from '@/actions/Action' + +export const updateLicenseAction = Action(Licenses.update) diff --git a/src/app/(home)/Section.module.scss b/src/app/(home)/Section.module.scss index 3ad34657c..fa0b67080 100644 --- a/src/app/(home)/Section.module.scss +++ b/src/app/(home)/Section.module.scss @@ -31,14 +31,14 @@ color: ohma.$colors-white; } } - a { + .readMore { @include ohma.borderBtn(ohma.$colors-primary); } } &:not(.blue) { background-color: ohma.$colors-white; color: ohma.$colors-black; - a { + .readMore { @include ohma.borderBtn(ohma.$colors-secondary); } } diff --git a/src/app/(home)/Section.tsx b/src/app/(home)/Section.tsx index e48940ebd..15bea3932 100644 --- a/src/app/(home)/Section.tsx +++ b/src/app/(home)/Section.tsx @@ -28,7 +28,7 @@ function Section({ specialCmsImage, specialCmsParagraph, lesMer, right, imgWidth {!right && imgContainer}
- Les mer + Les mer
{right && imgContainer} diff --git a/src/app/_components/Cms/CmsImage/CmsImage.tsx b/src/app/_components/Cms/CmsImage/CmsImage.tsx index b89e23d5d..3b6ffafb4 100644 --- a/src/app/_components/Cms/CmsImage/CmsImage.tsx +++ b/src/app/_components/Cms/CmsImage/CmsImage.tsx @@ -37,7 +37,7 @@ export default async function CmsImage({ }: PropTypes) { let image = cmsImage.image if (!image) { - const defaultRes = await readSpecialImageAction('DEFAULT_IMAGE') + const defaultRes = await readSpecialImageAction.bind(null, { special: 'DEFAULT_IMAGE' })() if (!defaultRes.success) return image = defaultRes.data } diff --git a/src/app/_components/Cms/CmsImage/CmsImageClient.tsx b/src/app/_components/Cms/CmsImage/CmsImageClient.tsx index 09307d0c5..103f5e52c 100644 --- a/src/app/_components/Cms/CmsImage/CmsImageClient.tsx +++ b/src/app/_components/Cms/CmsImage/CmsImageClient.tsx @@ -30,7 +30,7 @@ export default function CmsImageClient({ useEffect(() => { if (image) return - readSpecialImageAction('DEFAULT_IMAGE').then(res => { + readSpecialImageAction.bind(null, { special: 'DEFAULT_IMAGE' })().then(res => { if (!res.success) return setFallback(true) return setCmsImage(res.data) }) diff --git a/src/app/_components/Form/Form.module.scss b/src/app/_components/Form/Form.module.scss index f7bc11b3a..898a2e00c 100644 --- a/src/app/_components/Form/Form.module.scss +++ b/src/app/_components/Form/Form.module.scss @@ -40,4 +40,8 @@ } } } -} \ No newline at end of file + .submitButton { + margin: 0; + } +} + diff --git a/src/app/_components/Form/Form.tsx b/src/app/_components/Form/Form.tsx index ddd34dbbb..1a007f584 100644 --- a/src/app/_components/Form/Form.tsx +++ b/src/app/_components/Form/Form.tsx @@ -148,7 +148,7 @@ export default function Form({ success={success} generalErrors={generalErrors} confirmation={confirmation} - className={buttonClassName} + className={`${buttonClassName} ${styles.submitButton}`} > {submitText} diff --git a/src/app/_components/Image/Image.module.scss b/src/app/_components/Image/Image.module.scss index a47feaa1a..46ad93eb3 100644 --- a/src/app/_components/Image/Image.module.scss +++ b/src/app/_components/Image/Image.module.scss @@ -5,10 +5,55 @@ margin: 0; box-sizing: border-box; display: flex; + flex-direction: column; > img { margin: 0; width: 100%; height: 100%; } -} + + .credit { + position: absolute; + background-color: rgba(ohma.$colors-gray-300, .5); + &.bottom { + bottom: 0; + left: 0; + } + &.top { + top: 0; + left: 0; + } + padding: .5em + } + + .license { + position: absolute; + right: 0; + top: 0; + text-decoration: none; + color: ohma.$colors-black; + z-index: 2; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + svg { + color: ohma.$colors-white; + width: 20px; + height: 20px; + margin: .6em; + } + a, p { + display: none; + font-size: ohma.$fonts-s; + margin-left: .5em; + } + &:hover { + background-color: rgba(ohma.$colors-gray-300, .9); + a, p { + display: inline; + } + } + } +} \ No newline at end of file diff --git a/src/app/_components/Image/Image.tsx b/src/app/_components/Image/Image.tsx index cc08dc87b..570c280c2 100644 --- a/src/app/_components/Image/Image.tsx +++ b/src/app/_components/Image/Image.tsx @@ -1,6 +1,9 @@ import styles from './Image.module.scss' -import type { ImageProps } from 'next/image' +import Link from 'next/link' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCopyright } from '@fortawesome/free-solid-svg-icons' import type { Image, ImageSize, Image as ImageT } from '@prisma/client' +import type { ImageProps } from 'next/image' export type ImageSizeOptions = ImageSize | 'ORIGINAL' @@ -10,6 +13,10 @@ export type PropTypes = Omit & { alt?: string, smallSize?: boolean, imageContainerClassName?: string, + creditPlacement?: 'top' | 'bottom', + hideCredit?: boolean, + hideCopyRight?: boolean, + disableLinkingToLicense?: boolean, } & ( | { imageSize?: never, smallSize?: never, largeSize?: boolean } | { imageSize?: never, smallSize?: boolean, largeSize?: never } @@ -24,6 +31,12 @@ export type PropTypes = Omit & { * @param smallSize - (optional) if true, the image will be the small size * @param largeSize - (optional) if true, the image will be the large size * @param imageSize - (optional) the size of the image + * @param imageContainerClassName - (optional) the class name of the + * @param creditPlacement - (optional) the placement of the credit + * @param hideCredit - (optional) if true, the credit will be hidden + * @param hideCopyRight - (optional) if true, the copy right will be hidden + * @param disableLinkingToLicense - (optional) if true, the license will not be linked rather + * the name will be disblayed alone * @param props - the rest of the props to pass to the img tag */ export default function Image({ @@ -34,6 +47,10 @@ export default function Image({ largeSize, imageSize, imageContainerClassName, + creditPlacement = 'bottom', + hideCredit = false, + hideCopyRight = false, + disableLinkingToLicense = false, ...props }: PropTypes) { let url = `/store/images/${image.fsLocationMediumSize}` @@ -65,6 +82,17 @@ export default function Image({ alt={alt || image.alt} src={url} /> + {image.credit && !hideCredit &&

{image.credit}

} + {!hideCopyRight && image.licenseLink && ( +
+ {disableLinkingToLicense ?

{image.licenseName}

: ( + + {image.licenseName} + + )} + +
+ )} ) } diff --git a/src/app/_components/Image/ImageList/ImageDisplay.module.scss b/src/app/_components/Image/ImageList/ImageDisplay.module.scss index 061f75ade..aa72d9b2f 100644 --- a/src/app/_components/Image/ImageList/ImageDisplay.module.scss +++ b/src/app/_components/Image/ImageList/ImageDisplay.module.scss @@ -15,6 +15,8 @@ bottom: 0; left: 0; z-index: 2; + background-color: transparent; + border: none; svg { color: ohma.$colors-black; width: 35px; diff --git a/src/app/_components/Image/ImageList/ImageDisplay.tsx b/src/app/_components/Image/ImageList/ImageDisplay.tsx index 322668f79..f1097d307 100644 --- a/src/app/_components/Image/ImageList/ImageDisplay.tsx +++ b/src/app/_components/Image/ImageList/ImageDisplay.tsx @@ -11,10 +11,12 @@ import { destroyImageAction } from '@/actions/images/destroy' import { ImagePagingContext } from '@/contexts/paging/ImagePaging' import { ImageDisplayContext } from '@/contexts/ImageDisplayProvider' import { updateImageCollectionAction } from '@/actions/images/collections/update' +import LicenseChooser from '@/components/LicenseChooser/LicenseChooser' import { useRouter } from 'next/navigation' import { faChevronRight, faChevronLeft, faX, faCog } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useContext } from 'react' +import Link from 'next/link' import type { ImageSizeOptions } from '@/components/Image/Image' import type { Image as ImageT } from '@prisma/client' @@ -132,7 +134,6 @@ export default function ImageDisplay() { } if (!image) return <> - console.log(image) return (
@@ -167,13 +168,28 @@ export default function ImageDisplay() {

{image.name}

- {image.alt} + Alt-tekst: {image.alt} Type: {getCurrentType(image, displayContext.imageSize)} + Kreditert: {image.credit ?? 'ingen'} + Lisens: { + image.licenseLink ? + + {image.licenseName} + + : 'ingen' + } + { pagingContext.loading ? (
) : ( { canEdit && ( - - -
+ }>
+ +
- +
diff --git a/src/app/_components/Image/ImageUploader.tsx b/src/app/_components/Image/ImageUploader.tsx index 0486d3109..19d8a7a30 100644 --- a/src/app/_components/Image/ImageUploader.tsx +++ b/src/app/_components/Image/ImageUploader.tsx @@ -2,6 +2,7 @@ import Form from '@/components/Form/Form' import { createImageAction } from '@/actions/images/create' import TextInput from '@/components/UI/TextInput' import FileInput from '@/components/UI/FileInput' +import LicenseChooser from '@/components/LicenseChooser/LicenseChooser' import type { PropTypes as FormPropTypes } from '@/components/Form/Form' type ResponseType = Awaited>; @@ -22,12 +23,14 @@ export default function ImageUploader({ collectionId, ...formProps }: PropTypes) + + ) diff --git a/src/app/_components/ImageCard/ImageCard.tsx b/src/app/_components/ImageCard/ImageCard.tsx index 4bc48831f..066121429 100644 --- a/src/app/_components/ImageCard/ImageCard.tsx +++ b/src/app/_components/ImageCard/ImageCard.tsx @@ -18,7 +18,7 @@ export default function ImageCard({ image, title, children, href, className }: P
{ image && ( - + ) }
diff --git a/src/app/_components/LicenseChooser/LicenseChooser.module.scss b/src/app/_components/LicenseChooser/LicenseChooser.module.scss new file mode 100644 index 000000000..a3ae6aec2 --- /dev/null +++ b/src/app/_components/LicenseChooser/LicenseChooser.module.scss @@ -0,0 +1,5 @@ +@use '@/styles/ohma'; + +.LicenseChooser { + width: 200px; +} \ No newline at end of file diff --git a/src/app/_components/LicenseChooser/LicenseChooser.tsx b/src/app/_components/LicenseChooser/LicenseChooser.tsx new file mode 100644 index 000000000..ce30a5918 --- /dev/null +++ b/src/app/_components/LicenseChooser/LicenseChooser.tsx @@ -0,0 +1,37 @@ +'use client' +import styles from './LicenseChooser.module.scss' +import { SelectNumberPossibleNULL } from '@/UI/Select' +import { readLicensesAction } from '@/actions/licenses/read' +import useActionCall from '@/hooks/useActionCall' +import { useCallback } from 'react' + +type PropTypes = { + defaultLicenseName?: string | null +} + +/** + * A component to choose a license. It makes a CLIENT SIDE request to the server to get the licenses + * The selection has the name "licenseId" + * @returns A component to choose a license + */ +export default function LicenseChooser({ defaultLicenseName }: PropTypes) { + const action = useCallback(readLicensesAction.bind(null, {}), [readLicensesAction]) + const { data } = useActionCall(action) + + const defaultLicenseId = data?.find(license => license.name === defaultLicenseName)?.id + + return ( + ({ value: license.id, label: license.name })) ?? []) + ] as const + } + defaultValue={defaultLicenseId ?? 'NULL'} + /> + ) +} diff --git a/src/app/_components/PopUp/PopUp.tsx b/src/app/_components/PopUp/PopUp.tsx index 33373d48d..f3e8a6514 100644 --- a/src/app/_components/PopUp/PopUp.tsx +++ b/src/app/_components/PopUp/PopUp.tsx @@ -1,6 +1,5 @@ 'use client' import styles from './PopUp.module.scss' -import Button from '@/components/UI/Button' import useKeyPress from '@/hooks/useKeyPress' import { PopUpContext } from '@/contexts/PopUp' import useClickOutsideRef from '@/hooks/useClickOutsideRef' @@ -54,9 +53,9 @@ export default function PopUp({
- +
{ children }
diff --git a/src/app/admin/(permissions)/default-permissions/page.tsx b/src/app/admin/(permissions)/default-permissions/page.tsx index 6c5ea3a81..be9ad2ce0 100644 --- a/src/app/admin/(permissions)/default-permissions/page.tsx +++ b/src/app/admin/(permissions)/default-permissions/page.tsx @@ -14,20 +14,25 @@ export default async function Defaults() { const defaultPermissions = defaultPermissionsRes.data return ( -
- ( - - ) - } - /> - + <> +

Standard Tilganger

+ Dette er tilganger alle har på nettsiden uavhengig av innlogging og gruppeafiliasjoner +
+ ( + + ) + } + /> + + + ) } diff --git a/src/app/admin/SlideSidebar.tsx b/src/app/admin/SlideSidebar.tsx index 5fb61ac88..bbd639e31 100644 --- a/src/app/admin/SlideSidebar.tsx +++ b/src/app/admin/SlideSidebar.tsx @@ -14,7 +14,8 @@ import { faArrowLeft, faPaperPlane, faSchool, - faDotCircle + faDotCircle, + faListDots } from '@fortawesome/free-solid-svg-icons' import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' import type { ReactNode } from 'react' @@ -173,6 +174,18 @@ const navigations = [ href: '/admin/dots-freeze-periods' }, ] + }, + { + header: { + title: 'Annet', + icon: faListDots + }, + links: [ + { + title: 'Lisenser', + href: '/admin/licenses' + }, + ] } ] satisfies { header: { diff --git a/src/app/admin/committees/page.tsx b/src/app/admin/committees/page.tsx index c9013a4d1..761980ad5 100644 --- a/src/app/admin/committees/page.tsx +++ b/src/app/admin/committees/page.tsx @@ -13,7 +13,9 @@ export default async function adminCommittee() { if (!committeeLogoCollectionRes.success) throw new Error('Kunne ikke finne komitelogoer') const { id: collectionId } = committeeLogoCollectionRes.data - const defaultCommitteeLogoRes = await readSpecialImageAction('DAFAULT_COMMITTEE_LOGO') + const defaultCommitteeLogoRes = await readSpecialImageAction.bind( + null, { special: 'DAFAULT_COMMITTEE_LOGO' } + )() if (!defaultCommitteeLogoRes.success) throw new Error('Kunne ikke finne standard komitelogo') const defaultCommitteeLogo = defaultCommitteeLogoRes.data diff --git a/src/app/admin/licenses/page.module.scss b/src/app/admin/licenses/page.module.scss new file mode 100644 index 000000000..2177a9ab8 --- /dev/null +++ b/src/app/admin/licenses/page.module.scss @@ -0,0 +1,8 @@ +@use '@/styles/ohma'; + +.wrapper { + padding: 1em; + .licenses { + @include ohma.table; + } +} \ No newline at end of file diff --git a/src/app/admin/licenses/page.tsx b/src/app/admin/licenses/page.tsx new file mode 100644 index 000000000..158dde635 --- /dev/null +++ b/src/app/admin/licenses/page.tsx @@ -0,0 +1,73 @@ +import styles from './page.module.scss' +import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { readLicensesAction } from '@/actions/licenses/read' +import { SettingsHeaderItemPopUp } from '@/app/_components/HeaderItems/HeaderItemPopUp' +import Form from '@/app/_components/Form/Form' +import { destroyLicenseAction } from '@/actions/licenses/destroy' +import { createLicenseAction } from '@/actions/licenses/create' +import TextInput from '@/UI/TextInput' +import { updateLicenseAction } from '@/actions/licenses/update' +import Link from 'next/link' + +export default async function Licenses() { + const licenses = unwrapActionReturn(await readLicensesAction.bind(null, {})()) + + return ( +
+

Lisenser

+

Lisenser brukes for bilder

+ + + + + + + + + + + {licenses.map((license) => ( + + + + + + + ))} + +
idNavnLink
{license.id}{license.name} + + Til lisens + + + +
+ + + + +
+ +
+ + + + + +
+ ) +} diff --git a/src/app/career/page.tsx b/src/app/career/page.tsx index f8aa20394..c13500cd4 100644 --- a/src/app/career/page.tsx +++ b/src/app/career/page.tsx @@ -12,9 +12,15 @@ import Link from 'next/link' export default async function CareerLandingPage() { const session = await Session.fromNextAuth() - const jobAdImageRes = await readSpecialImageAction('ENGINEER') - const eventImageRes = await readSpecialImageAction('FAIR') - const comanyImageRes = await readSpecialImageAction('SKYSCRAPER') + const jobAdImageRes = await readSpecialImageAction.bind(null, { + special: 'MACHINE' + })() + const eventImageRes = await readSpecialImageAction.bind(null, { + special: 'FAIR' + })() + const comanyImageRes = await readSpecialImageAction.bind(null, { + special: 'REALFAGSBYGGET' + })() const conactorCmsLinkRes = await readSpecialCmsLinkAction.bind( null, { special: 'CAREER_LINK_TO_CONTACTOR' } )() @@ -37,6 +43,7 @@ export default async function CareerLandingPage() { { jobAdImage ? : <> }

Jobbanonser

@@ -45,12 +52,14 @@ export default async function CareerLandingPage() { companyPresentationEventTag ? [companyPresentationEventTag.name] : [] )}`}> { eventImage ? : <> }

Bedriftpresentasjoner

{ companyImage ? : <> }

Bedrifter

diff --git a/src/app/committees/[shortName]/layout.tsx b/src/app/committees/[shortName]/layout.tsx index 13bc7dc6b..848ee28db 100644 --- a/src/app/committees/[shortName]/layout.tsx +++ b/src/app/committees/[shortName]/layout.tsx @@ -19,7 +19,9 @@ export default async function Committee({ params, children }: PropTypes) { let committeeLogo = committee.logoImage.image if (!committeeLogo) { - const res = await readSpecialImageAction('DAFAULT_COMMITTEE_LOGO') + const res = await readSpecialImageAction.bind( + null, { special: 'DAFAULT_COMMITTEE_LOGO' } + )() if (!res.success) throw new Error('Kunne ikke finne standard komitelogo') committeeLogo = res.data } diff --git a/src/app/committees/page.tsx b/src/app/committees/page.tsx index e9679be5d..b79b5ee1c 100644 --- a/src/app/committees/page.tsx +++ b/src/app/committees/page.tsx @@ -8,7 +8,9 @@ export default async function Committees() { if (!res.success) throw new Error(`Kunne ikke hente komiteer - ${res.errorCode}`) const committees = res.data - const strandardCommitteeLogoRes = await readSpecialImageAction('DAFAULT_COMMITTEE_LOGO') + const strandardCommitteeLogoRes = await readSpecialImageAction.bind( + null, { special: 'DAFAULT_COMMITTEE_LOGO' } + )() const standardCommitteeLogo = strandardCommitteeLogoRes.success ? strandardCommitteeLogoRes.data : null return ( diff --git a/src/app/education/page.tsx b/src/app/education/page.tsx index 104c1acc9..8e0b2070b 100644 --- a/src/app/education/page.tsx +++ b/src/app/education/page.tsx @@ -4,11 +4,15 @@ import PageWrapper from '@/components/PageWrapper/PageWrapper' import ImageCard from '@/components/ImageCard/ImageCard' export default async function education() { - const hovedbyggningenRes = await readSpecialImageAction('HOVEDBYGGNINGEN') - const R1Res = await readSpecialImageAction('R1') + const hovedbyggningenRes = await readSpecialImageAction.bind(null, { + special: 'HOVEDBYGGNINGEN' + })() + const BooksRes = await readSpecialImageAction.bind(null, { + special: 'BOOKS' + })() const hovedbyggningen = hovedbyggningenRes.success ? hovedbyggningenRes.data : null - const R1 = R1Res.success ? R1Res.data : null + const Books = BooksRes.success ? BooksRes.data : null return ( @@ -21,7 +25,7 @@ export default async function education() {

På fagveven kan du finne mange ulike skoler som man kan lese om

- +

Her kan du lese om ulike emner som du kan ta både på NTNU og på utveksling

diff --git a/src/app/images/collections/[id]/CollectionAdminUpload.tsx b/src/app/images/collections/[id]/CollectionAdminUpload.tsx index d7abfdc71..81a05b180 100644 --- a/src/app/images/collections/[id]/CollectionAdminUpload.tsx +++ b/src/app/images/collections/[id]/CollectionAdminUpload.tsx @@ -6,6 +6,8 @@ import { maxNumberOfImagesInOneBatch } from '@/services/images/ConfigVars' import Form from '@/components/Form/Form' import Slider from '@/components/UI/Slider' import ProgressBar from '@/components/ProgressBar/ProgressBar' +import TextInput from '@/app/_components/UI/TextInput' +import LicenseChooser from '@/app/_components/LicenseChooser/LicenseChooser' import { useCallback, useState } from 'react' import type { FileWithStatus } from '@/components/UI/Dropzone' import type { ActionReturn } from '@/actions/Types' @@ -31,12 +33,16 @@ export default function CollectionAdminUpload({ collectionId, refreshImages }: P const doneFiles: FileWithStatus[] = [] const useFileName = data.get('useFileName') === 'on' + const credit = typeof data.get('credit') === 'string' ? data.get('credit') : undefined + const licenseId = typeof data.get('licenseId') === 'string' ? data.get('licenseId') : undefined let res: ActionReturn = { success: true, data: undefined } setProgress(0) const progressIncrement = 1 / batches.length for (const batch of batches) { const formData = new FormData() + if (credit) formData.append('credit', credit) + if (licenseId) formData.append('licenseId', licenseId) batch.forEach(file => { formData.append('files', file.file) }) @@ -46,7 +52,7 @@ export default function CollectionAdminUpload({ collectionId, refreshImages }: P } return file })) - res = await createImagesAction.bind(null, useFileName).bind(null, collectionId)(formData) + res = await createImagesAction.bind(null, { useFileName, collectionId })(formData) if (res.success) { doneFiles.push(...batch) setFiles(files.map(file => { @@ -82,6 +88,8 @@ export default function CollectionAdminUpload({ collectionId, refreshImages }: P action={handleBatchedUpload} > + + { progress ? : <> diff --git a/src/app/images/collections/[id]/page.tsx b/src/app/images/collections/[id]/page.tsx index ac086faf3..c1ba6bbc8 100644 --- a/src/app/images/collections/[id]/page.tsx +++ b/src/app/images/collections/[id]/page.tsx @@ -22,10 +22,12 @@ export default async function Collection({ params }: PropTypes) { if (!readCollection.success) notFound() //TODO: replace with better error page if error is UNAUTHORIZED. const collection = readCollection.data - const readImages = await readImagesPageAction({ - page: { pageSize, page: 0, cursor: null }, - details: { collectionId: collection.id } - }) + const readImages = await readImagesPageAction.bind(null, { + paging: { + page: { pageSize, page: 0, cursor: null }, + details: { collectionId: collection.id } + } + })() if (!readImages.success) notFound() const images = readImages.data diff --git a/src/app/users/[username]/page.tsx b/src/app/users/[username]/page.tsx index 0e93cfc03..c99f6dfcd 100644 --- a/src/app/users/[username]/page.tsx +++ b/src/app/users/[username]/page.tsx @@ -1,5 +1,5 @@ import styles from './page.module.scss' -import { readSpecialImage } from '@/services/images/read' +import { readSpecialImageAction } from '@/actions/images/read' import BorderButton from '@/components/UI/BorderButton' import { readCommitteesFromIds } from '@/services/groups/committees/read' import { readUserProfileAction } from '@/actions/users/read' @@ -41,7 +41,12 @@ export default async function User({ params }: PropTypes) { // TODO: Change to the correct order const order = 105 - const profileImage = profile.user.image ? profile.user.image : await readSpecialImage('DEFAULT_PROFILE_IMAGE') + const profileImage = profile.user.image ? profile.user.image : await readSpecialImageAction.bind( + null, { special: 'DEFAULT_PROFILE_IMAGE' } + )().then(res => { + if (!res.success) throw new Error('Kunne ikke finne standard profilbilde') + return res.data + }) const { authorized: canAdministrate } = UserProfileUpdateAuther.dynamicFields( { username: profile.user.username } diff --git a/src/contexts/paging/ImagePaging.tsx b/src/contexts/paging/ImagePaging.tsx index c535c521a..66d9e095e 100644 --- a/src/contexts/paging/ImagePaging.tsx +++ b/src/contexts/paging/ImagePaging.tsx @@ -7,7 +7,7 @@ import type { ImageCursor, ImageDetails } from '@/services/images/Types' export type PageSizeImage = 30 const fetcher = async (x: ReadPageInput) => { - const ret = await readImagesPageAction(x) + const ret = await readImagesPageAction.bind(null, { paging: x })() return ret } diff --git a/src/prisma/prismaservice/src/seedImages.ts b/src/prisma/prismaservice/src/seedImages.ts index 477083875..328a0661c 100644 --- a/src/prisma/prismaservice/src/seedImages.ts +++ b/src/prisma/prismaservice/src/seedImages.ts @@ -1,4 +1,4 @@ -import { seedImageConfig, seedSpecialImageConfig } from './seedImagesConfig' +import { seedImageConfig, seedSpecialImageConfig, seedLicenseConfig } from './seedImagesConfig' import { v4 as uuid } from 'uuid' import sharp from 'sharp' import { readdir, copyFile } from 'fs/promises' @@ -25,6 +25,11 @@ export const imageStoreLocation = join(__dirname, '..', 'store', 'images') * @param pisama - the prisma client */ export default async function seedImages(prisma: PrismaClient) { + // Seed all licenses + await prisma.license.createMany({ + data: seedLicenseConfig + }) + const files = await readdir(standardLocation) //Get the to bjects to a common format @@ -93,6 +98,12 @@ export default async function seedImages(prisma: PrismaClient) { data: { name: image.name, alt: image.alt, + credit: image.credit, + license: image.license ? { + connect: { + name: image.license + } + } : undefined, fsLocationOriginal, fsLocationSmallSize, fsLocationMediumSize, diff --git a/src/prisma/prismaservice/src/seedImagesConfig.ts b/src/prisma/prismaservice/src/seedImagesConfig.ts index 4576389d5..21f803271 100644 --- a/src/prisma/prismaservice/src/seedImagesConfig.ts +++ b/src/prisma/prismaservice/src/seedImagesConfig.ts @@ -1,13 +1,33 @@ import type { SpecialImage, SpecialCollection } from '@/generated/pn' +type LicenseSeed = { + name: string, + link: string, +} + +export const seedLicenseConfig = [ + { + name: 'CC BY-SA 4.0', + link: 'https://creativecommons.org/licenses/by-sa/4.0/', + }, + { + name: 'Paxels', + link: 'https://www.pexels.com/license/', + }, +] as const satisfies LicenseSeed[] + +type licenseName = typeof seedLicenseConfig[number]['name'] + type ImageSeedConfigBase = { name: string, alt: string, fsLocation: string, //location in standard_store/images collection: string, + credit: string | null, + license: licenseName | null, } -const defaultCollection = 'STANDARDIMAGES' satisfies SpecialCollection +const defaultCollection = 'STANDARDIMAGES' as const satisfies SpecialCollection type ImageSeedConfig = ImageSeedConfigBase[] @@ -23,56 +43,74 @@ export const seedImageConfig: ImageSeedConfig = [ { name: 'traktat', alt: 'En gammel traktat', - fsLocation: 'traktat.jpg', + fsLocation: 'treaty.jpeg', collection: defaultCollection, + credit: 'Skylar Kang', + license: 'Paxels', }, { name: 'kappemann', alt: 'En kappemann', fsLocation: 'kappemann.jpeg', collection: defaultCollection, + credit: null, + license: null, }, { name: 'kongsberg', alt: 'Kongsberg', fsLocation: 'kongsberg.png', collection: defaultCollection, + credit: null, + license: null, }, { name: 'nordic', alt: 'Nordic', fsLocation: 'nordic.png', collection: defaultCollection, + credit: null, + license: null, }, { name: 'ohma', alt: 'Ohma', fsLocation: 'ohma.jpeg', collection: defaultCollection, + credit: null, + license: null, }, { name: 'omega_mai', alt: 'Omega mai', fsLocation: 'Omegamai.jpeg', collection: defaultCollection, + credit: null, + license: null, }, { name: 'ov', alt: 'OV', fsLocation: 'ov.jpeg', collection: defaultCollection, + credit: null, + license: null, }, { name: 'pwa', alt: 'PWA', fsLocation: 'pwa.png', collection: defaultCollection, + credit: null, + license: null, }, { name: 'harambe', alt: 'Harambe', fsLocation: 'harambe.jpg', - collection: 'PROFILEIMAGES' + collection: 'PROFILEIMAGES', + credit: null, + license: null, } ] @@ -86,77 +124,110 @@ export const seedSpecialImageConfig: ImageSeedSpecialConfig = { alt: 'standard bilde (ikke funnet)', fsLocation: 'default_image.jpeg', collection: defaultCollection, + credit: null, + license: null, }, DEFAULT_IMAGE_COLLECTION_COVER: { name: 'lens_camera', alt: 'Et kamera med en linse', fsLocation: 'lens_camera.jpeg', collection: defaultCollection, + credit: null, + license: null, }, DEFAULT_PROFILE_IMAGE: { name: 'default_profile_image', alt: 'standard profilbilde', fsLocation: 'default_profile_image.png', collection: 'PROFILEIMAGES', + credit: null, + license: null, }, DAFAULT_COMMITTEE_LOGO: { name: 'default_committee_logo', alt: 'komité logo', fsLocation: 'logo_simple.png', collection: 'COMMITTEELOGOS', + credit: null, + license: null, }, LOGO_SIMPLE: { name: 'logo_simple', alt: 'Logo', fsLocation: 'logo_simple.png', collection: defaultCollection, + credit: null, + license: null, }, LOGO_WHITE: { name: 'logo_white', alt: 'Logo', fsLocation: 'logo_white.png', collection: defaultCollection, + credit: null, + license: null, }, LOGO_WHITE_TEXT: { name: 'logo_white_text', alt: 'Logo', fsLocation: 'omega_logo_white.png', collection: defaultCollection, + credit: null, + license: null, }, MAGISK_HATT: { name: 'magisk_hatt', alt: 'Magisk hatt', fsLocation: 'magisk_hatt.png', collection: defaultCollection, + credit: null, + license: null, }, HOVEDBYGGNINGEN: { name: 'hovedbygningen', alt: 'Hovedbygningen', - fsLocation: 'hovedbygningen.webp', + fsLocation: 'hovedbygget.jpeg', collection: defaultCollection, + credit: 'Thomas Høstad/NTNU', + license: 'CC BY-SA 4.0', + // https://ntnu.fotoware.cloud/fotoweb/archives/ + // 5005-Blinkskudd/Folder%20112/Hovedbygget-03.jpg.info#c=%2Ffotoweb%2Farchives%2F5005-Blinkskudd%2F }, - R1: { - name: 'R1 NTNU', - alt: 'R1 på NTNU', - fsLocation: 'R1NTNU.jpeg', + BOOKS: { + name: 'Books', + alt: 'Bøker', + fsLocation: 'books.jpeg', collection: defaultCollection, + credit: 'Alexander Grey', + license: 'Paxels', }, - ENGINEER: { - name: 'engineer', - alt: 'Engineer', - fsLocation: 'engineer.jpeg', + MACHINE: { + name: 'machine', + alt: 'Maskin', + fsLocation: 'machine.jpeg', collection: defaultCollection, + credit: 'Børge Sandnes/NTNU', + license: 'CC BY-SA 4.0', + // https://ntnu.fotoware.cloud/fotoweb/archives/ + // 5005-Blinkskudd/Folder%20112/Manu-08.jpg.info#c=%2Ffotoweb%2Farchives%2F5005-Blinkskudd%2F }, - SKYSCRAPER: { - name: 'skyscraper', - alt: 'Skyscraper', - fsLocation: 'skyscraper.jpeg', + REALFAGSBYGGET: { + name: 'Realfagsbygget', + alt: 'Realfagsbygget', + fsLocation: 'realfagsbygget.jpeg', collection: defaultCollection, + credit: 'Per Henning/NTNU', + license: 'CC BY-SA 4.0', + // https://ntnu.fotoware.cloud/fotoweb/archives/ + // 5019-Campuser/Folder%20112/Realfagbygget-soloppgang-2015-2.jpg.info#c=%2Ffotoweb%2Farchives%2F5019-Campuser%2F }, FAIR: { name: 'fair', alt: 'Fair', - fsLocation: 'fair.webp', + fsLocation: 'fair.jpeg', collection: defaultCollection, + credit: null, + license: null, + // From contactor }, } as const diff --git a/src/prisma/prismaservice/standard_store/images/R1NTNU.jpeg b/src/prisma/prismaservice/standard_store/images/R1NTNU.jpeg deleted file mode 100644 index 8dbd8c5f2..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/R1NTNU.jpeg and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/books.jpeg b/src/prisma/prismaservice/standard_store/images/books.jpeg new file mode 100644 index 000000000..197f18bf1 Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/books.jpeg differ diff --git a/src/prisma/prismaservice/standard_store/images/engineer.jpeg b/src/prisma/prismaservice/standard_store/images/engineer.jpeg deleted file mode 100644 index df26cc3ac..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/engineer.jpeg and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/fair.jpeg b/src/prisma/prismaservice/standard_store/images/fair.jpeg new file mode 100644 index 000000000..9962465d1 Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/fair.jpeg differ diff --git a/src/prisma/prismaservice/standard_store/images/fair.webp b/src/prisma/prismaservice/standard_store/images/fair.webp deleted file mode 100644 index 554eab784..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/fair.webp and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/hovedbygget.jpeg b/src/prisma/prismaservice/standard_store/images/hovedbygget.jpeg new file mode 100644 index 000000000..9bbbfa6af Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/hovedbygget.jpeg differ diff --git a/src/prisma/prismaservice/standard_store/images/hovedbygningen.webp b/src/prisma/prismaservice/standard_store/images/hovedbygningen.webp deleted file mode 100644 index f3fe7f9fc..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/hovedbygningen.webp and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/machine.jpeg b/src/prisma/prismaservice/standard_store/images/machine.jpeg new file mode 100644 index 000000000..8f7a9710a Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/machine.jpeg differ diff --git a/src/prisma/prismaservice/standard_store/images/realfagsbygget.jpeg b/src/prisma/prismaservice/standard_store/images/realfagsbygget.jpeg new file mode 100644 index 000000000..c92c9e189 Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/realfagsbygget.jpeg differ diff --git a/src/prisma/prismaservice/standard_store/images/skyscraper.jpeg b/src/prisma/prismaservice/standard_store/images/skyscraper.jpeg deleted file mode 100644 index 385ab63be..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/skyscraper.jpeg and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/traktat.jpg b/src/prisma/prismaservice/standard_store/images/traktat.jpg deleted file mode 100644 index 77a03c913..000000000 Binary files a/src/prisma/prismaservice/standard_store/images/traktat.jpg and /dev/null differ diff --git a/src/prisma/prismaservice/standard_store/images/treaty.jpeg b/src/prisma/prismaservice/standard_store/images/treaty.jpeg new file mode 100644 index 000000000..81a708d1e Binary files /dev/null and b/src/prisma/prismaservice/standard_store/images/treaty.jpeg differ diff --git a/src/prisma/schema/image.prisma b/src/prisma/schema/image.prisma index 7b1431aca..3a2d0aaca 100644 --- a/src/prisma/schema/image.prisma +++ b/src/prisma/schema/image.prisma @@ -8,9 +8,9 @@ enum SpecialImage { LOGO_WHITE_TEXT MAGISK_HATT HOVEDBYGGNINGEN - R1 - SKYSCRAPER - ENGINEER + BOOKS + REALFAGSBYGGET + MACHINE FAIR } @@ -26,13 +26,27 @@ model Image { collection ImageCollection @relation(fields: [collectionId], references: [id], onDelete: Cascade) coverImageForCollection ImageCollection? @relation(name: "coverImageForCollection") collectionId Int + credit String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + license License? @relation(fields: [licenseName, licenseLink], references: [name, link]) + licenseName String? + licenseLink String? cmsImages CmsImage[] special SpecialImage? @unique User User[] } +model License { + id Int @id @default(autoincrement()) + name String @unique + link String + + images Image[] + + @@unique([name, link]) +} + enum SpecialCollection { STANDARDIMAGES OMBULCOVERS diff --git a/src/prisma/schema/permission.prisma b/src/prisma/schema/permission.prisma index f135f8376..209d33ff9 100644 --- a/src/prisma/schema/permission.prisma +++ b/src/prisma/schema/permission.prisma @@ -139,6 +139,8 @@ enum Permission { COMPANY_ADMIN DOTS_ADMIN + + LICENSE_ADMIN } model Role { diff --git a/src/services/error.ts b/src/services/error.ts index 19be17e9c..881a101bb 100644 --- a/src/services/error.ts +++ b/src/services/error.ts @@ -61,6 +61,11 @@ export const errorCodes = [ httpCode: 401, defaultMessage: 'Du er ikke innlogget', }, + { + name: 'UNPERMITTED CASCADE', + httpCode: 400, + defaultMessage: 'Du kan ikke slette denne ressursen fordi den er tilknyttet andre ressurser', + } ] as const export type ErrorCode = typeof errorCodes[number]['name'] diff --git a/src/services/groups/committees/create.ts b/src/services/groups/committees/create.ts index 86af5c8c2..49bc490c4 100644 --- a/src/services/groups/committees/create.ts +++ b/src/services/groups/committees/create.ts @@ -1,6 +1,6 @@ import { createCommitteeValidation } from './validation' import prisma from '@/prisma' -import { readSpecialImage } from '@/services/images/read' +import { Images } from '@/services/images' import { prismaCall } from '@/services/prismaCall' import { createArticle } from '@/services/cms/articles/create' import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' @@ -12,7 +12,9 @@ export async function createCommittee(rawdata: CreateCommitteeTypes['Detailed']) const { name, shortName, logoImageId } = createCommitteeValidation.detailedValidate(rawdata) let defaultLogoImageId: number if (!logoImageId) { - defaultLogoImageId = (await readSpecialImage('DAFAULT_COMMITTEE_LOGO')).id + defaultLogoImageId = await Images.readSpecial.client(prisma).execute({ + params: { special: 'DAFAULT_COMMITTEE_LOGO' }, session: null //TODO: pass session + }).then(res => res.id) } const article = await createArticle({}) diff --git a/src/services/groups/committees/update.ts b/src/services/groups/committees/update.ts index ac61a9e8c..68b815b2d 100644 --- a/src/services/groups/committees/update.ts +++ b/src/services/groups/committees/update.ts @@ -1,7 +1,7 @@ import { updateCommitteeValidation } from './validation' import prisma from '@/prisma' -import { readSpecialImage } from '@/services/images/read' import { prismaCall } from '@/services/prismaCall' +import { Images } from '@/services/images' import type { ExpandedCommittee } from './Types' import type { UpdateCommitteeTypes } from './validation' @@ -13,7 +13,9 @@ export async function updateCommittee( let defaultLogoImageId: number if (!logoImageId) { - defaultLogoImageId = (await readSpecialImage('DAFAULT_COMMITTEE_LOGO')).id + defaultLogoImageId = await Images.readSpecial.client(prisma).execute({ + params: { special: 'DAFAULT_COMMITTEE_LOGO' }, session: null //TODO: pass session + }).then(res => res.id) } return await prismaCall(() => prisma.committee.update({ diff --git a/src/services/images/collections/read.ts b/src/services/images/collections/read.ts index 910a97866..1b89b20d2 100644 --- a/src/services/images/collections/read.ts +++ b/src/services/images/collections/read.ts @@ -1,6 +1,6 @@ import 'server-only' import { specialCollectionsSpecialVisibilityMap } from './ConfigVars' -import { readSpecialImage } from '@/services/images/read' +import { Images } from '@/services/images' import prisma from '@/prisma' import logger from '@/lib/logger' import { prismaCall } from '@/services/prismaCall' @@ -68,7 +68,12 @@ export async function readImageCollectionsPage( ...cursorPageingSelection(page) })) - const lensCamera = await readSpecialImage('DEFAULT_IMAGE_COLLECTION_COVER') + const lensCamera = await Images.readSpecial.client(prisma).execute({ + params: { + special: 'DEFAULT_IMAGE_COLLECTION_COVER' + }, + session: null //TODO: pass session + }) const chooseCoverImage = (collection: { coverImage: Image | null, diff --git a/src/services/images/create.ts b/src/services/images/create.ts index bc18ccba9..fac7aacc3 100644 --- a/src/services/images/create.ts +++ b/src/services/images/create.ts @@ -1,61 +1,122 @@ import 'server-only' import { readSpecialImageCollection } from './collections/read' -import { createImageValidation } from './validation' +import { createImagesValidation, createImageValidation } from './validation' import { allowedExtImageUpload, avifOptions, imageSizes } from './ConfigVars' -import { prismaCall } from '@/services/prismaCall' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' import { createFile } from '@/services/store/createFile' -import prisma from '@/prisma' import logger from '@/lib/logger' import sharp from 'sharp' -import type { CreateImageTypes } from './validation' -import type { Image, SpecialImage } from '@prisma/client' +import type { SpecialImage } from '@prisma/client' /** - * Creates one image from a file (creates all the types of resolutions and stores them) - * @param file - The file to create the image from - * @param meta - The metadata for the image for the db - * @returns + * Creates an image. + * The method will resize the image to the correct sizes and save it to the store. + * It will also save the original image to the store. + * All images are saved as avif (except the original). + * @param collectionId - The id of the collection to add the image to */ -export async function createImage({ - collectionId, - ...rawdata -}: CreateImageTypes['Detailed'] & { collectionId: number }): Promise { - const { file, ...meta } = createImageValidation.detailedValidate(rawdata) - - const buffer = Buffer.from(await file.arrayBuffer()) - const avifBuffer = await sharp(buffer).toFormat('avif').avif(avifOptions).toBuffer() - const avifFile = new File([avifBuffer], 'image.avif', { type: 'image/avif' }) +export const create = ServiceMethodHandler({ + withData: true, + validation: createImageValidation, + handler: async (prisma, { collectionId }: { collectionId: number }, data) => { + const { file, ...meta } = data + const buffer = Buffer.from(await file.arrayBuffer()) + const avifBuffer = await sharp(buffer).toFormat('avif').avif(avifOptions).toBuffer() + const avifFile = new File([avifBuffer], 'image.avif', { type: 'image/avif' }) - const uploadPromises = [ - createOneInStore(avifFile, ['avif'], imageSizes.small), - createOneInStore(avifFile, ['avif'], imageSizes.medium), - createOneInStore(avifFile, ['avif'], imageSizes.large), - createFile(file, 'images', [...allowedExtImageUpload]), - ] + const uploadPromises = [ + createOneInStore(avifFile, ['avif'], imageSizes.small), + createOneInStore(avifFile, ['avif'], imageSizes.medium), + createOneInStore(avifFile, ['avif'], imageSizes.large), + createFile(file, 'images', [...allowedExtImageUpload]), + ] - const [smallSize, mediumSize, largeSize, original] = await Promise.all(uploadPromises) - const fsLocationSmallSize = smallSize.fsLocation - const fsLocationMediumSize = mediumSize.fsLocation - const fsLocationLargeSize = largeSize.fsLocation - const fsLocationOriginal = original.fsLocation - const extOriginal = original.ext - return await prismaCall(() => prisma.image.create({ - data: { - name: meta.name, - alt: meta.alt, - fsLocationOriginal, - fsLocationSmallSize, - fsLocationMediumSize, - fsLocationLargeSize, - extOriginal, - collection: { - connect: { - id: collectionId, + const [smallSize, mediumSize, largeSize, original] = await Promise.all(uploadPromises) + const fsLocationSmallSize = smallSize.fsLocation + const fsLocationMediumSize = mediumSize.fsLocation + const fsLocationLargeSize = largeSize.fsLocation + const fsLocationOriginal = original.fsLocation + const extOriginal = original.ext + return await prisma.image.create({ + data: { + name: meta.name, + alt: meta.alt, + license: meta.licenseId ? { connect: { id: meta.licenseId } } : undefined, + credit: meta.credit, + fsLocationOriginal, + fsLocationSmallSize, + fsLocationMediumSize, + fsLocationLargeSize, + extOriginal, + collection: { + connect: { + id: collectionId, + } } } + }) + } +}) + +/** + * Creates many images from files. + * The method will resize the images to the correct sizes and save them to the store. + */ +export const createMany = ServiceMethodHandler({ + withData: true, + validation: createImagesValidation, + handler: async ( + prisma, + { useFileName, collectionId }: { useFileName: boolean, collectionId: number }, + data, + session + ) => { + for (const file of data.files) { + const name = useFileName ? file.name.split('.')[0] : undefined + await create.client(prisma).execute({ + params: { collectionId }, + data: { file, name, alt: file.name.split('.')[0], licenseId: data.licenseId, credit: data.credit }, + session + }) } - })) -} + } +}) + +/** + * WARNING: This function should only be used in extreme cases + * Creates bad image this should really only happen in worst case scenario + * where the server has lost a image and needs to be replaced with a bad image. + * A bad image is an image that has no correct fsLocation attributes. + * @param name - the name of the image + * @param config - the config for the image (special) + */ +export const createSourceless = ServiceMethodHandler({ + withData: false, + handler: async (prisma, { name, special }: { name: string, special: SpecialImage }) => { + const standardCollection = await readSpecialImageCollection('STANDARDIMAGES') + logger.warn(` + creating a bad image, Something has caused the server to lose a neccesary image. + It was replaced with a image model with no correct fsLocation atrributes. + `) + return await prisma.image.create({ + data: { + name, + special, + fsLocationOriginal: 'not_found', + fsLocationMediumSize: 'not_found', + fsLocationSmallSize: 'not_found', + fsLocationLargeSize: 'not_found', + extOriginal: 'jpg', + alt: 'not found', + collection: { + connect: { + id: standardCollection.id + } + } + }, + }) + } +}) /** * Creates one image from a file. @@ -72,33 +133,3 @@ export async function createOneInStore(file: File, allowedExt: string[], size: n return ret } -/** - * WARNING: This function should only be used in extreme cases - * Creats bad image this should really only happen in worst case scenario - * Ads it to the standard collection - * @param name - the name of the image - * @param config - the config for the image (special) - */ -export async function createBadImage(name: string, config: { - special: SpecialImage -}): Promise { - const standardCollection = await readSpecialImageCollection('STANDARDIMAGES') - logger.warn('creating a bad image, this should only happen in extreme cases.') - return await prismaCall(() => prisma.image.create({ - data: { - name, - special: config.special, - fsLocationOriginal: 'not_found', - fsLocationMediumSize: 'not_found', - fsLocationSmallSize: 'not_found', - fsLocationLargeSize: 'not_found', - extOriginal: 'jpg', - alt: 'not found', - collection: { - connect: { - id: standardCollection.id - } - } - }, - })) -} diff --git a/src/services/images/index.ts b/src/services/images/index.ts new file mode 100644 index 000000000..274fcb336 --- /dev/null +++ b/src/services/images/index.ts @@ -0,0 +1,37 @@ +import 'server-only' +import { create, createSourceless, createMany } from './create' +import { readPage, read, readSpecial } from './read' +import { ServiceMethod } from '@/services/ServiceMethod' + +export const Images = { + create: ServiceMethod({ + withData: true, + serviceMethodHandler: create, + hasAuther: false // TODO: add auth - visibilty + }), + createMany: ServiceMethod({ + withData: true, + serviceMethodHandler: createMany, + hasAuther: false // TODO: add auth - visibilty + }), + createSourceless: ServiceMethod({ + withData: false, + serviceMethodHandler: createSourceless, + hasAuther: false // TODO: add auth - visibilty + }), + readPage: ServiceMethod({ + withData: false, + serviceMethodHandler: readPage, + hasAuther: false // TODO: add auth - visibilty + }), + read: ServiceMethod({ + withData: false, + serviceMethodHandler: read, + hasAuther: false // TODO: add auth - visibilty + }), + readSpecial: ServiceMethod({ + withData: false, + serviceMethodHandler: readSpecial, + hasAuther: false // TODO: add auth - visibilty + }), +} as const diff --git a/src/services/images/read.ts b/src/services/images/read.ts index cab13428b..5ab2ca081 100644 --- a/src/services/images/read.ts +++ b/src/services/images/read.ts @@ -1,42 +1,70 @@ import 'server-only' +import { createSourceless } from './create' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' import { cursorPageingSelection } from '@/lib/paging/cursorPageingSelection' -import { prismaCall } from '@/services/prismaCall' import { ServerError } from '@/services/error' -import prisma from '@/prisma' +import { SpecialImage } from '@prisma/client' import type { ReadPageInput } from '@/lib/paging/Types' import type { ImageDetails, ImageCursor } from '@/services/images/Types' -import type { Image, SpecialImage } from '@prisma/client' -export async function readImagesPage( - { page, details }: ReadPageInput -): Promise { - const { collectionId } = details - return await prismaCall(() => prisma.image.findMany({ - where: { - collectionId, - }, - ...cursorPageingSelection(page) - })) -} +/** + * Reads a page of images in a collection by collectionId. + */ +export const readPage = ServiceMethodHandler({ + withData: false, + handler: async (prisma, params: { + paging: ReadPageInput + }) => { + const { collectionId } = params.paging.details + return await prisma.image.findMany({ + where: { + collectionId, + }, + ...cursorPageingSelection(params.paging.page) + }) + } +}) -export async function readImage(id: number): Promise { - const image = await prismaCall(() => prisma.image.findUnique({ - where: { - id, - }, - })) +/** + * Reads an image by id. + */ +export const read = ServiceMethodHandler({ + withData: false, + handler: async (prisma, { id }: { id: number }) => { + const image = await prisma.image.findUnique({ + where: { + id, + }, + }) - if (!image) throw new ServerError('NOT FOUND', 'Image not found') - return image -} + if (!image) throw new ServerError('NOT FOUND', 'Image not found') + return image + } +}) -export async function readSpecialImage(special: SpecialImage): Promise { - const image = await prisma.image.findUnique({ - where: { - special, - }, - }) +/** + * Reads a special image by name (special atr.). + * In the case that the special image does not exist (bad state) a "bad" image will be created. + */ +export const readSpecial = ServiceMethodHandler({ + withData: false, + handler: async (prisma, { special }: { special: SpecialImage }, session) => { + if (!Object.values(SpecialImage).includes(special)) { + throw new ServerError('BAD PARAMETERS', 'Bildet er ikke spesielt') + } + + const image = await prisma.image.findUnique({ + where: { + special, + }, + }) + + if (!image) { + return await createSourceless.client(prisma).execute( + { params: { name: special, special }, session } + ) + } + return image + } +}) - if (!image) throw new ServerError('NOT FOUND', 'Image not found') - return image -} diff --git a/src/services/images/update.ts b/src/services/images/update.ts index 2124af3a2..35df5da14 100644 --- a/src/services/images/update.ts +++ b/src/services/images/update.ts @@ -9,11 +9,14 @@ export async function updateImage( imageId: number, rawdata: UpdateImageTypes['Detailed'] ): Promise { - const data = updateImageValidation.detailedValidate(rawdata) + const { licenseId, ...data } = updateImageValidation.detailedValidate(rawdata) return await prismaCall(() => prisma.image.update({ where: { id: imageId, }, - data + data: { + license: licenseId ? { connect: { id: licenseId } } : undefined, + ...data, + } })) } diff --git a/src/services/images/validation.ts b/src/services/images/validation.ts index 85bf176ef..93c9d3771 100644 --- a/src/services/images/validation.ts +++ b/src/services/images/validation.ts @@ -19,6 +19,8 @@ export const baseImageValidation = new ValidationBase({ name: z.string().optional(), alt: z.string(), files: zfd.repeatable(z.array(z.instanceof(File))), + licenseId: z.string().optional(), + credit: z.string().optional(), }, details: { file: imageFileSchema, @@ -31,27 +33,37 @@ export const baseImageValidation = new ValidationBase({ files => files.every(file => allowedExtImageUpload.includes(file.type.split('/')[1])), `File type must be one of ${allowedExtImageUpload.join(', ')}` ), + licenseId: z.number().optional(), + credit: z.string().optional(), } }) export const createImageValidation = baseImageValidation.createValidation({ - keys: ['name', 'alt', 'file'], - transformer: data => data + keys: ['name', 'alt', 'file', 'licenseId', 'credit'], + transformer, }) export type CreateImageTypes = ValidationTypes export const createImagesValidation = baseImageValidation.createValidation({ - keys: ['files'], - transformer: data => data, + keys: ['files', 'credit', 'licenseId'], + transformer, refiner: { - fcn: data => data.files.length < maxNumberOfImagesInOneBatch && data.files.length > 0, + fcn: data => data.files.length <= maxNumberOfImagesInOneBatch && data.files.length > 0, message: `Du kan bare laste opp mellom 1 og ${maxNumberOfImagesInOneBatch} bilder av gangen` } }) export type CreateImagesTypes = ValidationTypes export const updateImageValidation = baseImageValidation.createValidation({ - keys: ['name', 'alt'], - transformer: data => data + keys: ['name', 'alt', 'credit', 'licenseId'], + transformer, }) export type UpdateImageTypes = ValidationTypes + +function transformer(data: Data & { licenseId?: string }) { + if (data.licenseId === undefined) return { ...data, licenseId: undefined } + return { + ...data, + licenseId: data.licenseId === 'NULL' ? undefined : parseInt(data.licenseId, 10) + } +} diff --git a/src/services/licenses/Authers.ts b/src/services/licenses/Authers.ts new file mode 100644 index 000000000..8848c1c21 --- /dev/null +++ b/src/services/licenses/Authers.ts @@ -0,0 +1,5 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +export const CreateLicenseAuther = RequirePermission.staticFields({ permission: 'LICENSE_ADMIN' }) +export const UpdateLicenseAuther = RequirePermission.staticFields({ permission: 'LICENSE_ADMIN' }) +export const DestroyLicenseAuther = RequirePermission.staticFields({ permission: 'LICENSE_ADMIN' }) diff --git a/src/services/licenses/create.ts b/src/services/licenses/create.ts new file mode 100644 index 000000000..3f8f3755a --- /dev/null +++ b/src/services/licenses/create.ts @@ -0,0 +1,11 @@ +import 'server-only' +import { createLicenseValidation } from './validation' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' + +export const create = ServiceMethodHandler({ + withData: true, + validation: createLicenseValidation, + handler: async (prisma, _, data) => await prisma.license.create({ + data, + }), +}) diff --git a/src/services/licenses/destroy.ts b/src/services/licenses/destroy.ts new file mode 100644 index 000000000..3778b5cbf --- /dev/null +++ b/src/services/licenses/destroy.ts @@ -0,0 +1,28 @@ +import 'server-only' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' +import { ServerError } from '@/services/error' + +export const destroy = ServiceMethodHandler({ + withData: false, + handler: async (prisma, params: { id: number }) => { + const { name: licenseName } = await prisma.license.findUniqueOrThrow({ + where: { id: params.id }, + select: { name: true } + }) + + const imagesOfLicense = await prisma.image.findMany({ + where: { + licenseName + }, + take: 1 + }) + if (imagesOfLicense.length > 0) { + throw new ServerError( + 'UNPERMITTED CASCADE', + 'Lisensen har bilder tilknyttet - slett bildene først eller endre lisensen på bildene' + ) + } + + await prisma.license.delete({ where: { id: params.id } }) + } +}) diff --git a/src/services/licenses/index.ts b/src/services/licenses/index.ts new file mode 100644 index 000000000..427515f77 --- /dev/null +++ b/src/services/licenses/index.ts @@ -0,0 +1,36 @@ +import 'server-only' +import { destroy } from './destroy' +import { readAll } from './read' +import { CreateLicenseAuther, DestroyLicenseAuther, UpdateLicenseAuther } from './Authers' +import { create } from './create' +import { update } from './update' +import { ServiceMethod } from '@/services/ServiceMethod' + +export const Licenses = { + readAll: ServiceMethod({ + withData: false, + serviceMethodHandler: readAll, + hasAuther: false, + }), + destroy: ServiceMethod({ + withData: false, + serviceMethodHandler: destroy, + hasAuther: true, + auther: DestroyLicenseAuther, + dynamicFields: () => ({}), + }), + create: ServiceMethod({ + withData: true, + serviceMethodHandler: create, + hasAuther: true, + auther: CreateLicenseAuther, + dynamicFields: () => ({}), + }), + update: ServiceMethod({ + withData: true, + serviceMethodHandler: update, + hasAuther: true, + auther: UpdateLicenseAuther, + dynamicFields: () => ({}), + }) +} as const diff --git a/src/services/licenses/read.ts b/src/services/licenses/read.ts new file mode 100644 index 000000000..61a16c0b1 --- /dev/null +++ b/src/services/licenses/read.ts @@ -0,0 +1,7 @@ +import 'server-only' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' + +export const readAll = ServiceMethodHandler({ + withData: false, + handler: async (prisma) => await prisma.license.findMany() +}) diff --git a/src/services/licenses/update.ts b/src/services/licenses/update.ts new file mode 100644 index 000000000..9f49d2db4 --- /dev/null +++ b/src/services/licenses/update.ts @@ -0,0 +1,16 @@ +import 'server-only' +import { updateLicenseValidation } from './validation' +import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' + +export const update = ServiceMethodHandler({ + withData: true, + validation: updateLicenseValidation, + handler: async (prisma, params: { id: number }, data) => { + await prisma.license.update({ + where: { + id: params.id + }, + data + }) + } +}) diff --git a/src/services/licenses/validation.ts b/src/services/licenses/validation.ts new file mode 100644 index 000000000..271362bd7 --- /dev/null +++ b/src/services/licenses/validation.ts @@ -0,0 +1,32 @@ +import { ValidationBase } from '@/services/Validation' +import { z } from 'zod' +import type { ValidationTypes } from '@/services/Validation' + +export const baseLicenseValidation = new ValidationBase({ + type: { + name: z.string(), + link: z.string(), + }, + details: { + name: z.string().min( + 5, 'Navn må være minst 5 tegn langt' + ).max( + 50, 'Navn kan maks være 50 tegn langt' + ), + link: z.string().min( + 1, 'Link må være minst 1 tegn langt' + ) + } +}) + +export const createLicenseValidation = baseLicenseValidation.createValidation({ + keys: ['name', 'link'], + transformer: data => data +}) +export type CreateLicenseTypes = ValidationTypes + +export const updateLicenseValidation = baseLicenseValidation.createValidationPartial({ + keys: ['name', 'link'], + transformer: data => data +}) + diff --git a/src/services/ombul/create.ts b/src/services/ombul/create.ts index f9d57f745..6b2e151aa 100644 --- a/src/services/ombul/create.ts +++ b/src/services/ombul/create.ts @@ -5,7 +5,7 @@ import { readSpecialImageCollection } from '@/services/images/collections/read' import { createCmsImage } from '@/services/cms/images/create' import prisma from '@/prisma' import { createFile } from '@/services/store/createFile' -import { createImage } from '@/services/images/create' +import { Images } from '@/services/images' import type { CreateOmbulTypes } from './validation' import type { Ombul } from '@prisma/client' @@ -42,11 +42,16 @@ export async function createOmbul( // create coverimage const ombulCoverCollection = await readSpecialImageCollection('OMBULCOVERS') - const coverImage = await createImage({ - name: fsLocation, - alt: `cover of ${config.name}`, - collectionId: ombulCoverCollection.id, - file: cover, + const coverImage = await Images.create.client(prisma).execute({ + params: { + collectionId: ombulCoverCollection.id, + }, + data: { + name: fsLocation, + alt: `cover of ${config.name}`, + file: cover, + }, + session: null }) const cmsCoverImage = await createCmsImage({ name: fsLocation }, coverImage) diff --git a/src/services/permissionRoles/ConfigVars.ts b/src/services/permissionRoles/ConfigVars.ts index 937c13e23..2bec8093d 100644 --- a/src/services/permissionRoles/ConfigVars.ts +++ b/src/services/permissionRoles/ConfigVars.ts @@ -436,6 +436,14 @@ export const PermissionConfig = { name: 'Prikkadministrator', description: 'kan administrere prikker', category: 'brukere' + }, + LICENSE_ADMIN: { + name: 'Lisensadministrator', + description: ` + kan administrere lisenser. Alle som eier et bilde kan + legge til en lisens uavhengig av denne tillatelsen + `, + category: 'diverse admin' } } satisfies Record diff --git a/src/services/prismaCall.ts b/src/services/prismaCall.ts index 855c2c647..0c555289a 100644 --- a/src/services/prismaCall.ts +++ b/src/services/prismaCall.ts @@ -1,4 +1,4 @@ -import { ServerError } from './error' +import { ServerError, Smorekopp } from './error' import { Prisma } from '@prisma/client' import type { ServerErrorCode } from './error' @@ -16,6 +16,10 @@ export async function prismaCall(call: () => Promise): Promise { try { return await call() } catch (error) { + if (error instanceof Smorekopp) { + throw error + } + if (!(error instanceof Prisma.PrismaClientKnownRequestError)) { console.error(error) throw new ServerError('UNKNOWN ERROR', 'unknown error') @@ -27,6 +31,7 @@ export async function prismaCall(call: () => Promise): Promise { } } +//TODO: Remove prismaCall and use prismaErrorWrapper instead export async function prismaErrorWrapper( call: () => Promise, ) { diff --git a/src/services/users/read.ts b/src/services/users/read.ts index 95915ae06..026ec13b6 100644 --- a/src/services/users/read.ts +++ b/src/services/users/read.ts @@ -1,5 +1,5 @@ import { maxNumberOfGroupsInFilter, standardMembershipSelection, userFilterSelection } from './ConfigVars' -import { readSpecialImage } from '@/services/images/read' +import { Images } from '@/services/images' import { ServiceMethodHandler } from '@/services/ServiceMethodHandler' import { ServerError } from '@/services/error' import { prismaCall } from '@/services/prismaCall' @@ -140,7 +140,10 @@ export async function readUserOrNull(where: readUserWhere): Promise } export async function readUserProfile(username: string): Promise { - const defaultProfileImage = await readSpecialImage('DEFAULT_PROFILE_IMAGE') + const defaultProfileImage = await Images.readSpecial.client(prisma).execute({ + params: { special: 'DEFAULT_PROFILE_IMAGE' }, + session: null, //TODO: pass session + }) const user = await prismaCall(() => prisma.user.findUniqueOrThrow({ where: { username: username.toLowerCase() }, select: { @@ -160,7 +163,7 @@ export async function readUserProfile(username: string): Promise { export const readProfile = ServiceMethodHandler({ withData: false, - handler: async (prisma_, params: {username: string}) => { + handler: async (prisma_, params: {username: string}, session) => { const user = await prisma_.user.findUniqueOrThrow({ where: { username: params.username.toLowerCase() }, select: { @@ -170,7 +173,10 @@ export const readProfile = ServiceMethodHandler({ }, }).then(async u => ({ ...u, - image: u.image || await readSpecialImage('DEFAULT_PROFILE_IMAGE') + image: u.image || await Images.readSpecial.client(prisma_).execute({ + params: { special: 'DEFAULT_PROFILE_IMAGE' }, + session, + }) })) const memberships = await readMembershipsOfUser(user.id) diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 745a352a4..41268a563 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -44,7 +44,6 @@ text-decoration: none; font-size: fonts.$m; background: $color; - padding: variables.$gap; padding: 2*variables.$gap; margin: variables.$gap; color: colors.$black;