Skip to content

Commit 6d63de7

Browse files
committed
feat: aciton improt route
1 parent 95a933a commit 6d63de7

10 files changed

Lines changed: 13624 additions & 13 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
66
DATABASE_URL=postgresql://user:1234@localhost:5432/payload
77
PAYLOAD_SECRET=secret_key
88
PAYLOAD_USER_PASS=test_password123
9+
ACTIONS_IMPORT_SECRET=secret_import_key
910

1011
# Run-Time
1112
SMTP_HOST=smtp.example.com

src/app/(landing)/[locale]/actions/[slug]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default async function ActionDetailPage({ params }: { params: LocaleSlugP
4343
</LinkButton>
4444

4545
<div className="flex flex-col gap-8">
46-
<div className="relative z-10 flex flex-col gap-8">
46+
<div className="relative z-10 flex flex-col gap-12">
4747
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between">
4848
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
4949
{action.icon && (
@@ -80,8 +80,8 @@ export default async function ActionDetailPage({ params }: { params: LocaleSlugP
8080
)}
8181

8282
{references.length > 0 && (
83-
<div className="space-y-3">
84-
<p className="text-sm tracking-wider text-tertiary">{referencesLabel}</p>
83+
<div className="space-y-4">
84+
<p className="text-sm text-tertiary">{referencesLabel}</p>
8585
<div className="grid gap-4 md:grid-cols-2">
8686
{references.map((reference) => (
8787
<ActionCard key={reference.id} action={reference} locale={locale} />
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { extractActionModuleInfo } from "@/lib/actionExtraction"
2+
import { getPayloadClient } from "@/lib/payloadClient"
3+
import type { Action } from "@/payload-types"
4+
import { NextResponse } from "next/server"
5+
6+
function getBearerToken(request: Request) {
7+
const authorization = request.headers.get("authorization")?.trim()
8+
if (!authorization?.toLowerCase().startsWith("bearer ")) return null
9+
10+
return authorization.slice("bearer ".length).trim()
11+
}
12+
13+
function createJsonUploadFile(identifier: string, module: unknown) {
14+
const json = JSON.stringify(module, null, 2)
15+
return {
16+
data: Buffer.from(json),
17+
mimetype: "application/json",
18+
name: `${identifier}.json`,
19+
size: Buffer.byteLength(json),
20+
}
21+
}
22+
23+
export async function POST(request: Request) {
24+
const expectedToken = process.env.ACTIONS_IMPORT_SECRET?.trim()
25+
const receivedToken = getBearerToken(request)
26+
27+
if (!expectedToken || receivedToken !== expectedToken) {
28+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
29+
}
30+
31+
const module = await request.json().catch(() => null)
32+
if (!module) {
33+
return NextResponse.json({ error: "Missing module JSON." }, { status: 400 })
34+
}
35+
36+
const moduleInfo = extractActionModuleInfo(module)
37+
if (!moduleInfo?.identifier) {
38+
return NextResponse.json({ error: "Module identifier is required." }, { status: 400 })
39+
}
40+
41+
if (!moduleInfo.title) {
42+
return NextResponse.json({ error: "Module name is required." }, { status: 400 })
43+
}
44+
45+
const payload = await getPayloadClient()
46+
const existingActions = await payload.find({
47+
collection: "actions",
48+
depth: 0,
49+
limit: 1,
50+
overrideAccess: true,
51+
pagination: false,
52+
where: {
53+
identifier: {
54+
equals: moduleInfo.identifier,
55+
},
56+
},
57+
})
58+
const existingAction = existingActions.docs[0] as Action | undefined
59+
60+
const media = await payload.create({
61+
collection: "media",
62+
data: {
63+
alt: `${moduleInfo.title} module`,
64+
},
65+
file: createJsonUploadFile(moduleInfo.identifier, module),
66+
overrideAccess: true,
67+
})
68+
69+
const actionData = {
70+
identifier: moduleInfo.identifier,
71+
module: media.id,
72+
}
73+
const action = existingAction
74+
? await payload.update({
75+
id: existingAction.id,
76+
collection: "actions",
77+
data: actionData,
78+
overrideAccess: true,
79+
})
80+
: await payload.create({
81+
collection: "actions",
82+
data: actionData,
83+
overrideAccess: true,
84+
})
85+
86+
return NextResponse.json({
87+
id: action.id,
88+
identifier: moduleInfo.identifier,
89+
mediaId: media.id,
90+
status: existingAction ? "updated" : "created",
91+
})
92+
}

src/collections/actions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ export const Actions: CollectionConfig = {
1212
delete: ({ req }) => Boolean(req.user),
1313
},
1414
fields: [
15+
{
16+
name: "identifier",
17+
type: "text",
18+
required: true,
19+
unique: true,
20+
index: true,
21+
},
1522
{
1623
name: "module",
1724
type: "upload",

src/components/ActionTriggerCard.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,13 @@ export function ActionTriggerCard({ type, item }: ActionTriggerCardProps) {
4747
)
4848

4949
return (
50-
<div
51-
className={cn(
52-
"overflow-hidden rounded-xl border transition-[background-color,border-color,box-shadow] duration-300 ease-out",
53-
open ? "border-white/10 bg-light shadow-[0_10px_30px_rgba(0,0,0,0.18)]" : "border-transparent bg-transparent"
54-
)}
55-
>
50+
<div className={cn("overflow-hidden rounded-xl border duration-300 ease-out", open ? "border-white/10 bg-light" : "border-transparent bg-transparent")}>
5651
<div className="relative z-10">
57-
<button type="button" onClick={() => toggleItem(0)} className={cn("flex w-full items-center justify-between gap-3 text-left transition-[padding] duration-300 ease-out", open ? "px-2 py-2" : "p-0")}>
52+
<button
53+
type="button"
54+
onClick={() => toggleItem(0)}
55+
className={cn("flex w-full items-center justify-between gap-3 text-left transition-[padding] duration-300 ease-out", open ? "px-2 py-2" : "p-0")}
56+
>
5857
<span className="min-w-0 truncate text-sm font-medium text-white">{item.name || item.identifier}</span>
5958
<IconChevronDown size={16} className={cn("shrink-0 text-tertiary transition-transform duration-300 ease-out", open && "rotate-180")} />
6059
</button>

src/lib/cms.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ interface FeatureItem {
4747
}
4848
}
4949

50-
export type ActionItem = Pick<Action, "id" | "module" | "tags"> & {
50+
export type ActionItem = Pick<Action, "id" | "identifier" | "module" | "tags"> & {
5151
slug: string
5252
title: string
5353
shortDescription?: string | null
@@ -406,11 +406,12 @@ async function enrichAction(action: Action): Promise<ActionItem> {
406406
const module = typeof action.module === "number" ? undefined : action.module
407407
const moduleJson = await fetchMediaJson(module).catch(() => null)
408408
const moduleInfo = extractActionModuleInfo(moduleJson)
409-
const slug = moduleInfo?.identifier || `action-${action.id}`
409+
const slug = moduleInfo?.identifier || action.identifier || `action-${action.id}`
410410
const title = moduleInfo?.title || slug
411411

412412
return {
413413
id: action.id,
414+
identifier: action.identifier,
414415
module: action.module,
415416
slug,
416417
title,
@@ -448,6 +449,7 @@ const getActionsCached = cache(async (locale: AppLocale): Promise<ActionItem[]>
448449
depth: 1,
449450
select: {
450451
module: true,
452+
identifier: true,
451453
tags: true,
452454
references: true,
453455
},

0 commit comments

Comments
 (0)