media upload

This commit is contained in:
Tom Elliott 2025-04-13 10:47:45 +12:00
parent 1ab9c10548
commit 239b67719a
15 changed files with 576 additions and 22 deletions

BIN
media/pigeon-400x300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
media/pigeon-768x1024.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
media/pigeon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -28,6 +28,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"prettier": "^3.5.3",
"tailwindcss": "^4",
"typescript": "^5"
},

View File

@ -67,7 +67,11 @@ export interface Config {
};
blocks: {};
collections: {
projects: Project;
news: News;
media: Media;
documents: Document;
data: Datum;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@ -75,7 +79,11 @@ export interface Config {
};
collectionsJoins: {};
collectionsSelect: {
projects: ProjectsSelect<false> | ProjectsSelect<true>;
news: NewsSelect<false> | NewsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
documents: DocumentsSelect<false> | DocumentsSelect<true>;
data: DataSelect<false> | DataSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@ -117,6 +125,50 @@ export interface UserAuthOperations {
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects".
*/
export interface Project {
id: number;
title: string;
/**
* The slug is used to identify the news item in the URL.
*/
slug: string;
content: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
/**
* Show this project on the homepage.
*/
featured?: boolean | null;
links?:
| {
link: string;
description?: string | null;
/**
* Optional: organise link under this heading
*/
group?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "news".
@ -147,6 +199,93 @@ export interface News {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: number;
alt?: string | null;
description?: string | null;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
thumbnail?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
card?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
tablet?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "documents".
*/
export interface Document {
id: number;
title: string;
description?: string | null;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "data".
*/
export interface Datum {
id: number;
title: string;
description?: string | null;
source: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@ -171,10 +310,26 @@ export interface User {
export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: 'projects';
value: number | Project;
} | null)
| ({
relationTo: 'news';
value: number | News;
} | null)
| ({
relationTo: 'media';
value: number | Media;
} | null)
| ({
relationTo: 'documents';
value: number | Document;
} | null)
| ({
relationTo: 'data';
value: number | Datum;
} | null)
| ({
relationTo: 'users';
value: number | User;
@ -221,6 +376,26 @@ export interface PayloadMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects_select".
*/
export interface ProjectsSelect<T extends boolean = true> {
title?: T;
slug?: T;
content?: T;
featured?: T;
links?:
| T
| {
link?: T;
description?: T;
group?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "news_select".
@ -233,6 +408,98 @@ export interface NewsSelect<T extends boolean = true> {
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
alt?: T;
description?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
sizes?:
| T
| {
thumbnail?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
card?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
tablet?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "documents_select".
*/
export interface DocumentsSelect<T extends boolean = true> {
title?: T;
description?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "data_select".
*/
export interface DataSelect<T extends boolean = true> {
title?: T;
description?: T;
source?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".

View File

@ -1,20 +1,29 @@
import sharp from 'sharp'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { FixedToolbarFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { buildConfig } from 'payload'
import { News } from './src/collections/News'
import { Home } from '@/globals/Home'
import { News } from './src/collections/News'
import { Projects } from '@/collections/Projects'
import { Data, Documents, Media } from '@/collections/Files'
export default buildConfig({
// If you'd like to use Rich Text, pass your editor here
editor: lexicalEditor(),
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
FixedToolbarFeature(),
]
}),
serverURL: process.env.SERVER_URL || 'http://localhost:3000',
globals: [Home],
// Define and configure your collections in this array
collections: [News],
collections: [Projects, News, Media, Documents, Data],
// Your Payload secret - should be a complex and secure string, unguessable
secret: process.env.PAYLOAD_SECRET || '',

3
pnpm-lock.yaml generated
View File

@ -54,6 +54,9 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.0.4(@types/react@19.0.12)
prettier:
specifier: ^3.5.3
version: 3.5.3
tailwindcss:
specifier: ^4
version: 4.0.17

View File

@ -0,0 +1,53 @@
import configPromise from "@payload-config";
import { getPayload } from "payload";
import { notFound } from "next/navigation";
import { RichText } from "@payloadcms/richtext-lexical/react";
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const payload = await getPayload({ config: configPromise });
const { slug } = await params;
const result = await payload.find({
collection: "projects",
where: {
slug: {
equals: slug,
},
},
depth: 1,
});
const item = result.docs[0];
if (!item) {
notFound();
}
return (
<div>
<h1>{item.title}</h1>
<RichText data={item.content} className="prose bg-gray-50 p-2" />
</div>
);
}
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise });
const projectItems = await payload.find({
collection: "projects",
depth: 1,
limit: 5,
select: {
slug: true,
},
});
return projectItems.docs.map((item) => ({
slug: item.slug,
}));
}

View File

@ -0,0 +1,39 @@
import configPromise from "@payload-config";
import Link from "next/link";
import { getPayload } from "payload";
export default async function Page() {
const payload = await getPayload({ config: configPromise });
const projectItems = await payload.find({
collection: "projects",
depth: 1,
limit: 5,
select: {
title: true,
slug: true,
},
sort: "-created_at",
});
return (
<div>
<h1>Projects</h1>
<ul>
{projectItems.docs.map((projectItem) => (
<li key={projectItem.id} className="p-4">
<h3 className="text-lg">
<Link
href={`
/projects/${projectItem.slug}`}
>
{projectItem.title}
</Link>
</h3>
</li>
))}
</ul>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
@ -24,6 +25,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,

View File

@ -0,0 +1 @@
import type { CollectionConfig } from "payload";

90
src/collections/Files.ts Normal file
View File

@ -0,0 +1,90 @@
import type { CollectionConfig } from "payload";
export const Media: CollectionConfig = {
slug: 'media',
upload: {
staticDir: 'media',
imageSizes: [
{
name: 'thumbnail',
width: 400,
height: 300,
position: 'centre',
},
{
name: 'card',
width: 768,
height: 1024,
position: 'centre',
},
{
name: 'tablet',
width: 1024,
height: undefined,
position: 'centre',
}
],
adminThumbnail: 'thumbnail',
mimeTypes: ['image/*'],
},
fields: [
{
name: 'alt',
label: 'Alt Text',
type: 'text',
},
{
name: 'description',
label: 'Description',
type: 'textarea',
}
]
}
export const Documents: CollectionConfig = {
slug: 'documents',
upload: {
staticDir: 'documents',
mimeTypes: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Description',
type: 'textarea',
},
]
}
export const Data: CollectionConfig = {
slug: 'data',
upload: {
staticDir: 'data',
mimeTypes: ['application/json', 'text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain'],
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Description',
type: 'textarea',
},
{
name: 'source',
label: 'Source',
type: 'text',
required: true,
},
],
}

View File

@ -1,11 +1,6 @@
import { formatSlug } from "@/lib/slugs";
import { CollectionConfig } from "payload";
export const formatSlug = (val: string): string =>
val
.replace(/ /g, '-')
.replace(/[^\w-]+/g, '')
.toLowerCase()
export const News: CollectionConfig = {
slug: 'news',
fields: [
@ -28,18 +23,7 @@ export const News: CollectionConfig = {
},
hooks: {
beforeValidate: [
({data, operation, value}) => {
if (typeof value === 'string') {
return formatSlug(value);
}
if (operation === 'create' || !data?.slug) {
const fallbackData = data?.title;
if (fallbackData && typeof fallbackData === 'string') {
return formatSlug(fallbackData);
}
}
return value;
}
formatSlug('title'),
]
}
},

View File

@ -0,0 +1,84 @@
import { formatSlug } from "@/lib/slugs";
import { CollectionConfig } from "payload";
export const Projects: CollectionConfig = {
slug: 'projects',
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'slug',
label: 'Slug',
type: 'text',
required: true,
unique: true,
admin: {
position: 'sidebar',
description: 'The slug is used to identify the news item in the URL.',
// readOnly: true,
},
hooks: {
beforeValidate: [
formatSlug('title'),
]
}
},
{
name: 'content',
label: 'Content',
type: 'richText',
required: true,
},
// list of files
// gallery
// keywords
// is featured?
{
name: 'featured',
label: 'Featured',
type: 'checkbox',
defaultValue: false,
admin: {
position: 'sidebar',
description: 'Show this project on the homepage.',
}
},
// links
{
name: 'links',
label: 'Links',
type: 'array',
fields: [
{
name: 'link',
label: 'Link',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Description',
type: 'text',
required: false,
},
{
name: 'group',
label: 'Group',
type: 'text',
required: false,
admin: {
description: 'Optional: organise link under this heading',
},
}
],
admin: {
position: 'sidebar',
}
},
]
}

21
src/lib/slugs.ts Normal file
View File

@ -0,0 +1,21 @@
import { FieldHook } from "payload";
export const format = (val: string): string =>
val
.replace(/ /g, '-')
.replace(/[^\w-]+/g, '')
.toLowerCase()
export const formatSlug = (name: string): FieldHook =>
({data, operation, value}) => {
if (typeof value === 'string') {
return format(value);
}
if (operation === 'create' || !data?.[name]) {
const fallbackData = data?.[name];
if (fallbackData && typeof fallbackData === 'string') {
return format(fallbackData);
}
}
return value;
};