Update skills: add website-creator, mql-developer, ecommerce-astro
Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: #f5f5f5;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'Next.js + Payload CMS',
|
||||
template: '%s | Next.js + Payload CMS',
|
||||
},
|
||||
description: 'A website built with Next.js and Payload CMS',
|
||||
}
|
||||
|
||||
export default function FrontendLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="th">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { getPayload } from 'payload'
|
||||
import Link from 'next/link'
|
||||
import config from '@payload-config'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function HomePage() {
|
||||
let posts: any[] = []
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'posts',
|
||||
limit: 10,
|
||||
sort: '-createdAt',
|
||||
})
|
||||
posts = docs
|
||||
} catch (e) {
|
||||
// Table might not exist yet - that's OK for initial setup
|
||||
console.warn('Could not fetch posts:', e)
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<header style={{ marginBottom: '3rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>
|
||||
Next.js + Payload CMS
|
||||
</h1>
|
||||
<p style={{ color: '#666' }}>
|
||||
Welcome to your new website. Edit <code>src/app/(frontend)/page.tsx</code> to customize.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>Recent Posts</h2>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<p style={{ color: '#888' }}>
|
||||
No posts yet. Go to{' '}
|
||||
<Link href="/admin" style={{ color: '#0070f3' }}>
|
||||
Admin Panel
|
||||
</Link>{' '}
|
||||
to create your first post.
|
||||
</p>
|
||||
) : (
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{posts.map((post) => (
|
||||
<li
|
||||
key={post.id}
|
||||
style={{
|
||||
padding: '1rem',
|
||||
marginBottom: '1rem',
|
||||
border: '1px solid #eee',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/posts/${post.slug || post.id}`}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<h3 style={{ margin: 0, marginBottom: '0.5rem' }}>{post.title as string}</h3>
|
||||
{post.createdAt && (
|
||||
<p style={{ margin: 0, fontSize: '0.875rem', color: '#888' }}>
|
||||
{new Date(post.createdAt).toLocaleDateString('th-TH', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<footer style={{ marginTop: '4rem', paddingTop: '2rem', borderTop: '1px solid #eee' }}>
|
||||
<Link href="/admin" style={{ color: '#0070f3' }}>
|
||||
Go to Admin Panel
|
||||
</Link>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,2 @@
|
||||
/* THIS FILE IS GENERATED BY PAYLOAD - RUN `pnpm generate:importmap` AFTER CHANGING COLLECTIONS */
|
||||
export const importMap = {}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const PUT = REST_PUT(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* Run `pnpm generate:importmap` to regenerate */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1 @@
|
||||
/* Custom styles for Payload admin - add your overrides here */
|
||||
@@ -0,0 +1,30 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
upload: true,
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
description: 'URL-friendly version (e.g. "about-us")',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Published', value: 'published' },
|
||||
],
|
||||
defaultValue: 'draft',
|
||||
admin: {
|
||||
description: 'Control publication status',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
label: 'Page Content',
|
||||
admin: {
|
||||
description: 'Main page content — use the visual editor',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
if (data.title && !data.slug) {
|
||||
data.slug = data.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9ก-๙]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '')
|
||||
}
|
||||
return data
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'createdAt'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
description: 'URL-friendly version of the title',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'featuredImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Published', value: 'published' },
|
||||
],
|
||||
defaultValue: 'draft',
|
||||
admin: {
|
||||
description: 'Control publication status',
|
||||
},
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
if (data.title && !data.slug) {
|
||||
data.slug = data.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '')
|
||||
}
|
||||
return data
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
// Email added by default
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as config } from './payload.config'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import { Users } from './collections/Users'
|
||||
import { Media } from './collections/Media'
|
||||
import { Posts } from './collections/Posts'
|
||||
import { Pages } from './collections/Pages'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
Users,
|
||||
Media,
|
||||
Posts,
|
||||
Pages,
|
||||
],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URL || '',
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [],
|
||||
})
|
||||
Reference in New Issue
Block a user