yvr-cms
Version:
A headless CMS boilerplate with Node.js, Mongoose, Nextjs and CLI
60 lines • 135 kB
JavaScript
const clientFiles = [
{
"path": ".gitignore",
"content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies \n/node_modules\n/.pnp \n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/client/.next/\n/public/files/*\n/public/images/*\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env\n"
},
{
"path": "app.js",
"content": "import { initializeServer } from 'yvr-core/server';\n\n\ninitializeServer()"
},
{
"path": "client/components/Login.js",
"content": "\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { Label } from \"@/components/ui/label\"\nimport { Input } from \"@/components/ui/input\"\nimport { Button } from \"@/components/ui/button\"\nimport { useState } from \"react\"\nimport { toast } from \"@/hooks/use-toast\"\nimport { useRouter } from \"next/router\"\nimport { setCookie } from 'cookies-next';\nimport { api } from \"yvr-core/client\";\n\nexport default function Login() {\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n\n\n const router = useRouter();\n\n const handleSubmit = async (e) => {\n e.preventDefault();\n\n if (!email || !password) {\n toast({\n description: \"Please fill all fields\",\n variant: \"destructive\"\n });\n return;\n }\n\n try {\n const data = await api.post(`login`, { email, password });\n if (data.token) {\n setCookie(\"token\", data.token, {\n maxAge: 24 * 60 * 60, // 24 hours\n });\n router.push(\"/\");\n } else {\n toast({\n description: data.error,\n variant: \"destructive\"\n });\n }\n } catch (error) {\n toast({\n description: error.response.data.error,\n variant: \"destructive\"\n });\n }\n }\n return (\n <div className=\"h-screen w-full flex flex-col justify-center items-center\">\n <title>Login Page</title>\n <h1 className=\"text-4xl font-bold mb-4\">\n Admin Panel\n </h1>\n <Card className=\"w-full max-w-sm\">\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>\n Please login to continue\n </CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4\">\n <form\n onSubmit={handleSubmit}\n className=\"grid gap-4\"\n >\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n onChange={(e) => setEmail(e.target.value)}\n value={email}\n id=\"email\" type=\"email\" placeholder=\"m@example.com\" required />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n onChange={(e) => setPassword(e.target.value)}\n value={password}\n id=\"password\"\n placeholder=\"********\"\n type=\"password\" required />\n </div>\n <Button\n onClick={handleSubmit}\n className=\"w-full\">\n Login\n </Button>\n </form>\n </CardContent>\n <CardFooter>\n\n </CardFooter>\n </Card>\n </div>\n )\n}"
},
{
"path": "client/components/Register.js",
"content": "\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { Label } from \"@/components/ui/label\"\nimport { Input } from \"@/components/ui/input\"\nimport { Button } from \"@/components/ui/button\"\nimport { useState } from \"react\"\nimport { toast } from \"@/hooks/use-toast\"\nimport { useRouter } from \"next/router\"\nimport { setCookie } from 'cookies-next';\nimport { api } from \"yvr-core/client\";\nimport axios from \"axios\"\n\n\nconst apiBaseUrl = process.env.API_BASE_URL;\n\nexport default function Register() {\n const [name, setName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n\n const router = useRouter();\n\n const handleSubmit = async (e) => {\n e.preventDefault();\n\n if (!email || !password || !name) {\n toast({\n description: \"Please fill all fields\",\n variant: \"destructive\"\n });\n return;\n }\n\n try {\n const { data } = await axios.post(`/admin/api/createFirstUser`, { name, email, password });\n if (data.error) {\n toast({\n description: data.error,\n variant: \"destructive\"\n });\n return;\n } else {\n toast({\n description: \"User created successfully\",\n variant: \"success\"\n });\n router.push(\"/login\");\n }\n \n } catch (error) {\n toast({\n description: error,\n variant: \"destructive\"\n });\n }\n }\n return (\n <div className=\"h-screen w-full flex flex-col justify-center items-center\">\n <title>Register Page</title>\n <h1 className=\"text-4xl font-bold mb-4\">\n Admin Panel\n </h1>\n <Card className=\"w-full max-w-sm\">\n <CardHeader>\n <CardTitle className=\"text-2xl\">Register</CardTitle>\n <CardDescription>\n Please register to continue\n </CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"name\">Name</Label>\n <Input\n onChange={(e) => setName(e.target.value)}\n value={name}\n id=\"name\" type=\"string\" placeholder=\"Name\" required />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n onChange={(e) => setEmail(e.target.value)}\n value={email}\n id=\"email\" type=\"email\" placeholder=\"m@example.com\" required />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n onChange={(e) => setPassword(e.target.value)}\n value={password}\n id=\"password\"\n placeholder=\"********\"\n type=\"password\" required />\n </div>\n </CardContent>\n <CardFooter>\n <Button\n onClick={handleSubmit}\n className=\"w-full\">\n Register\n </Button>\n </CardFooter>\n </Card>\n </div>\n )\n}\n"
},
{
"path": "client/components/app-sidebar.jsx",
"content": "import * as React from \"react\"\nimport { GalleryVerticalEnd } from \"lucide-react\"\n\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n} from \"@/components/ui/sidebar\"\nimport Link from \"next/link\"\nimport { useRouter } from \"next/router\";\n\nconst dev = process.env.NODE_ENV !== \"production\"\n\nexport function AppSidebar({ ...props }) {\n\n const router = useRouter();\n const path = router.pathname;\n\n // This is sample data.\n const data = {\n navMain: [\n {\n title: \"Home\",\n url: \"/\",\n items: [\n {\n title: \"Collections\",\n url: \"/collections\",\n isActive: path.includes(\"/collections\") ? true : false,\n },\n {\n title: \"Content Management\",\n url: \"/content-management\",\n isActive: path.includes(\"/content-management\") ? true : false,\n },\n {\n title: \"Permissions\",\n url: \"/permissions\",\n isActive: path.includes(\"/permissions\") ? true : false,\n }\n ],\n },\n ],\n }\n\n\n if (!dev) {\n // Remove the Collections from the sidebar\n data.navMain[0].items = data.navMain[0].items.filter(item => item.title !== \"Collections\");\n }\n\n return (\n <Sidebar variant=\"floating\" {...props}>\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild>\n <Link href=\"#\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"flex flex-col gap-0.5 leading-none\">\n <span className=\"font-semibold\">Documentation</span>\n <span className=\"\">v1.0.0</span>\n </div>\n </Link>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarMenu className=\"gap-2\">\n {data.navMain.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild>\n <Link href={item.url} className=\"font-medium\">\n {item.title}\n </Link>\n </SidebarMenuButton>\n {item.items?.length ? (\n <SidebarMenuSub className=\"ml-0 border-l-0 px-1.5\">\n {item.items.map((item) => (\n <SidebarMenuSubItem key={item.title}>\n <SidebarMenuSubButton asChild isActive={item.isActive}>\n <Link href={item.url}>{item.title}</Link>\n </SidebarMenuSubButton>\n </SidebarMenuSubItem>\n ))}\n </SidebarMenuSub>\n ) : null}\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroup>\n </SidebarContent>\n </Sidebar>\n )\n}\n"
},
{
"path": "client/components/collection/CollectionNavigate.js",
"content": "import { toast } from \"@/hooks/use-toast\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/router\";\nimport { useEffect, useState } from \"react\";\nimport { schemaManager } from \"yvr-core/client\";\nimport { Separator } from \"@/components/ui/separator\"\nimport { Button } from \"@/components/ui/button\"\nimport { FolderOpen } from \"lucide-react\"\n\nconst CollectionNavigate = () => {\n const [collectionList, setCollectionList] = useState([]);\n const router = useRouter();\n const path = router.pathname;\n const { collectionName } = router.query;\n const pageName = path.split(\"/\")[2];\n\n const getCollections = async () => {\n try {\n const schema = await schemaManager.loadAll();\n if (!schema.error) {\n setCollectionList(schema?.map((s) => s.model));\n } else {\n toast({\n description: schema.error,\n variant: \"destructive\"\n });\n }\n } catch (error) {\n toast({\n description: error.message,\n variant: \"destructive\"\n })\n }\n }\n\n useEffect(() => {\n getCollections();\n }, []);\n\n return (\n <nav className=\"space-y-4\">\n <Link href=\"/collections/new\" \n className={`block p-3 rounded-lg text-center text-sm font-medium transition-all duration-300 \n ${pageName === \"new\" ? \"bg-primary text-white shadow-md scale-105\" : \"bg-gray-300 text-black hover:bg-muted-foreground\"}`} \n prefetch={false}>\n New Collection +\n </Link>\n {collectionList.length > 0 && (\n <>\n <Separator className=\"my-2\" />\n <div className=\"space-y-1\">\n {collectionList?.map((collection, i) => (\n <Button\n key={i}\n asChild\n variant=\"ghost\"\n className={`w-full justify-start ${\n collectionName === collection.name ? \"bg-accent\" : \"\"\n }`}\n >\n <Link\n href={`/collections/${collection.name}`}\n prefetch={false}\n >\n <FolderOpen className=\"mr-2 h-4 w-4\" />\n {collection.displayName}\n </Link>\n </Button>\n ))}\n </div>\n </>\n )}\n </nav>\n );\n}\n\nexport default CollectionNavigate;\n"
},
{
"path": "client/components/collection/ContentTypeBuilder.js",
"content": "import { useState, useEffect } from 'react'\nimport { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\"\nimport { EditIcon } from 'lucide-react'\nimport { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from \"@/components/ui/select\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { schemaManager } from \"yvr-core/client\";\nimport { useRouter } from \"next/router\"\nimport { toast } from \"@/hooks/use-toast\";\nimport TypeIcon from '../contentManagement/TypeIcon'\n\nconst contentTypes = [\n { id: 'String', name: 'String' },\n { id: 'Number', name: 'Number' },\n { id: 'Boolean', name: 'Boolean' },\n { id: 'Date', name: 'Date' },\n { id: 'ObjectId', name: 'ObjectId' },\n { id: 'Media', name: 'Media' },\n { id: 'Slug', name: 'Slug' },\n { id: 'RichText', name: 'Rich Text Editor' },\n]\n\nexport default function ContentTypeBuilder({ editFieldName }) {\n const [open, setOpen] = useState(false)\n const [step, setStep] = useState(0)\n const [selectedType, setSelectedType] = useState('')\n const [fieldName, setFieldName] = useState('')\n const [isRequired, setIsRequired] = useState(false)\n const [isUnique, setIsUnique] = useState(false)\n const [defaultValue, setDefaultValue] = useState('')\n const [schemas, setSchemas] = useState([]);\n const [model, setModel] = useState({});\n const [fields, setFields] = useState([]);\n const [referenceSchema, setReferenceSchema] = useState(\"\");\n const [slugSource, setSlugSource] = useState(\"\");\n\n const router = useRouter();\n const { collectionName } = router.query;\n\n // Eski useEffect fonksiyonlarını aktarma\n useEffect(() => {\n if (fields && editFieldName) {\n const foundField = fields?.find(f => f.name == editFieldName);\n setFieldName(foundField?.name);\n setSelectedType(foundField?.type);\n setIsRequired(foundField?.required);\n setIsUnique(foundField?.unique);\n setDefaultValue(foundField?.defaultValue);\n setReferenceSchema(foundField?.referenceSchema);\n setSlugSource(foundField?.slugSource);\n }\n }, [fields, editFieldName]);\n\n const handleTypeSelect = (typeId) => {\n setSelectedType(typeId)\n setStep(1)\n }\n\n useEffect(() => {\n if (editFieldName) {\n setStep(1)\n }\n }, [editFieldName]);\n\n const handleBack = () => {\n if (step > 0) {\n setStep(step - 1)\n setFieldName('')\n setIsRequired(false)\n setIsUnique(false)\n setDefaultValue('')\n setReferenceSchema('')\n setSlugSource('')\n }\n }\n\n const handleFinish = async () => {\n if (!fieldName || !selectedType) {\n toast({\n description: \"Please fill all the fields\",\n })\n return;\n }\n\n let newFields;\n\n if (editFieldName) {\n newFields = fields.map(f => {\n if (f.name == editFieldName) {\n return {\n name: fieldName,\n type: selectedType,\n required: isRequired,\n unique: isUnique,\n defaultValue: defaultValue,\n referenceSchema,\n slugSource\n }\n }\n return f;\n });\n } else {\n newFields = [...fields, {\n name: fieldName,\n type: selectedType,\n required: isRequired,\n unique: isUnique,\n defaultValue: defaultValue,\n referenceSchema,\n slugSource\n }];\n }\n\n try {\n const data = await schemaManager.update(model, newFields);\n toast({\n description: \"Field added successfully\",\n type: \"success\"\n })\n router.reload();\n } catch (error) {\n toast({\n description: error.message,\n })\n }\n\n setOpen(false)\n setStep(0)\n setSelectedType('')\n setFieldName('')\n setIsRequired(false)\n setIsUnique(false)\n setDefaultValue('')\n setReferenceSchema('')\n setSlugSource('')\n }\n\n const getFields = async () => {\n try {\n const schema = await schemaManager.load(collectionName);\n setFields(schema?.fields || []);\n setModel(schema?.model || {});\n } catch (error) {\n toast({\n description: error.message,\n })\n }\n }\n\n useEffect(() => {\n collectionName && getFields();\n }, [collectionName]);\n\n const getSchemas = async () => {\n const schemas = await schemaManager.loadAll();\n setSchemas(schemas);\n }\n\n useEffect(() => {\n selectedType == \"ObjectId\" && getSchemas();\n selectedType == \"media\" && setReferenceSchema(\"Media\");\n }, [selectedType]);\n\n const renderDefaultValueInput = () => {\n switch (selectedType) {\n case 'string':\n return <Input type=\"text\" id=\"defaultValue\" value={defaultValue} onChange={(e) => setDefaultValue(e.target.value)} />\n case 'number':\n return <Input type=\"number\" id=\"defaultValue\" value={defaultValue} onChange={(e) => setDefaultValue(e.target.value)} />\n case 'boolean':\n return (\n <Switch\n id=\"defaultValue\"\n className=\"block\"\n checked={defaultValue == 'true'}\n onCheckedChange={(checked) => setDefaultValue(checked ? 'true' : 'false')}\n />\n )\n case 'date':\n return <Input type=\"date\" id=\"defaultValue\" value={defaultValue} onChange={(e) => setDefaultValue(e.target.value)} />\n default:\n return null\n }\n }\n\n const renderNameInput = () => {\n switch (selectedType) {\n default:\n return (\n <div className=\"space-y-2\">\n <Label htmlFor=\"fieldName\">Field Name</Label>\n <Input\n id=\"fieldName\"\n value={fieldName}\n onChange={(e) => setFieldName(e.target.value)}\n placeholder=\"Enter field name\"\n />\n </div>\n )\n }\n }\n\n const renderStep = () => {\n switch (step) {\n case 0:\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Select Content Type</h2>\n <div className=\"grid grid-cols-2 gap-4\">\n {contentTypes.map((type) => {\n return (\n <Button\n key={type.id}\n variant=\"outline\"\n className=\"h-auto py-4 px-4 flex flex-col items-center justify-center\"\n onClick={() => handleTypeSelect(type.id)}\n >\n <TypeIcon type={type.id} />\n <span>{type.name}</span>\n </Button>\n )\n })}\n </div>\n </div>\n )\n case 1:\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold\">Configure {selectedType} Field</h2>\n {renderNameInput()}\n <div className=\"flex items-center space-x-2\">\n <Checkbox\n id=\"required\"\n checked={isRequired}\n onCheckedChange={(checked) => setIsRequired(checked)}\n />\n <Label htmlFor=\"required\">Required</Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <Checkbox\n id=\"unique\"\n checked={isUnique}\n onCheckedChange={(checked) => setIsUnique(checked)}\n />\n <Label htmlFor=\"unique\">Unique</Label>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"defaultValue\">Default Value</Label>\n {renderDefaultValueInput()}\n </div>\n {selectedType === \"ObjectId\" && (\n <div className=\"space-y-2\">\n <Label htmlFor=\"referenceSchema\">Reference Schema</Label>\n <Select\n onValueChange={(e) => setReferenceSchema(e)}\n value={referenceSchema}\n id=\"field-reference-schema\"\n >\n <SelectTrigger>\n <SelectValue placeholder=\"Select reference schema\" />\n </SelectTrigger>\n <SelectContent>\n {schemas.map((schema) => (\n <SelectItem key={schema?.model?.name} value={schema?.model?.name}>\n {schema?.model?.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n {selectedType == \"Slug\" && (\n <div className=\"space-y-2\">\n <Label htmlFor=\"slugSource\">Slug Source</Label>\n <Select\n onValueChange={(e) => setSlugSource(e)}\n value={slugSource}\n id=\"field-slug-source\"\n >\n <SelectTrigger>\n <SelectValue placeholder=\"Select slug source\" />\n </SelectTrigger>\n <SelectContent>\n {fields.filter(f => f.type == \"String\").map((field, index) => (\n <SelectItem key={index} value={field?.name}>\n {field?.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n )\n default:\n return null\n }\n }\n\n return (\n <div className=\"p-4\">\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogTrigger asChild>\n {editFieldName ? <EditIcon className=\"h-5 w-5\" /> : <Button variant=\"default\">New Content Type</Button>}\n\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-[50vw]\">\n <DialogHeader>\n <DialogTitle>Create New Content Type</DialogTitle>\n </DialogHeader>\n <div className=\"mt-4\">\n {renderStep()}\n </div>\n <div className=\"mt-6 flex justify-between\">\n {step > 0 && (\n <Button variant=\"outline\" onClick={handleBack}>\n Back\n </Button>\n )}\n {step > 0 && (\n <Button onClick={handleFinish} disabled={!fieldName}>\n Finish\n </Button>\n )}\n </div>\n </DialogContent>\n </Dialog>\n </div>\n )\n}"
},
{
"path": "client/components/collection/FieldList.js",
"content": "import {\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\"\nimport { EditIcon, TrashIcon } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport { useRouter } from \"next/router\";\nimport { schemaManager } from \"yvr-core/client\";\nimport { toast } from \"@/hooks/use-toast\";\nimport { Type, Hash, ToggleLeft, Calendar, Key, Image, Link } from 'lucide-react'\nimport TypeIcon from \"../contentManagement/TypeIcon\";\nimport ContentTypeBuilder from \"./ContentTypeBuilder\";\n\nconst FieldList = ({ collection }) => {\n const router = useRouter();\n const { collectionName } = router.query;\n\n const deleteField = async (field) => {\n const model = collection.model;\n const newFields = collection.fields.filter(f => f.name !== field.name);\n try {\n const data = await schemaManager.update(model, newFields);\n toast({\n description: \"Field added successfully\",\n type: \"success\"\n })\n router.reload();\n } catch (error) {\n toast({\n description: error.message,\n })\n }\n };\n\n return (\n <div className=\"z-40\">\n <Table>\n <TableCaption>\n Fields\n </TableCaption>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[100px]\">Name</TableHead>\n <TableHead>Type</TableHead>\n <TableHead>Default Value</TableHead>\n <TableHead>Required</TableHead>\n <TableHead>Unique</TableHead>\n <TableHead>Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {collection?.fields?.map((field, index) => (\n <TableRow \n key={index}\n className=\"cursor-pointer\"\n >\n <TableCell className=\"font-medium\">{field.name}</TableCell>\n <TableCell>\n <div className=\"flex items-center gap-x-2 h-full\">\n <TypeIcon type={field.type} size={14} />\n {field.type}\n </div>\n </TableCell>\n <TableCell>{field.defaultValue}</TableCell>\n <TableCell>{field.required ? \"Yes\" : \"No\"}</TableCell>\n <TableCell>{field.unique ? \"Yes\" : \"No\"}</TableCell>\n <TableCell>\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" className=\"w-10 h-10 p-0\">\n <ContentTypeBuilder editFieldName={field.name} />\n \n </Button>\n <Button variant=\"outline\" size=\"icon\" onClick={() => deleteField(field)}\n >\n <TrashIcon className=\"h-5 w-5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n );\n}\n\nexport default FieldList;"
},
{
"path": "client/components/collection/NewCollection.js",
"content": "import { schemaManager } from \"yvr-core/client\";\nimport { Button } from \"../ui/button\";\nimport { Input } from \"../ui/input\";\nimport { Label } from \"../ui/label\";\nimport { useState, useEffect } from \"react\";\nimport { toast } from \"@/hooks/use-toast\";\nimport { useRouter } from \"next/router\";\n\nconst NewCollection = () => {\n const [name, setName] = useState(\"\");\n const [displayName, setDisplayName] = useState(\"\");\n\n const router = useRouter();\n\n const handleSubmit = async (e) => {\n if (!name) {\n toast({\n description: \"Please enter a collection name\",\n\n })\n }\n\n try {\n const schema = await schemaManager.create({name, displayName, description:\"description\"}, []);\n toast({\n description: schema.message,\n type: \"success\"\n })\n router.push('/collections');\n } catch (error) {\n toast({\n description: error.message,\n })\n }\n };\n\n useEffect(() => {\n if (displayName) {\n const turkishToEnglish = (str) => {\n const charMap = {\n 'ç': 'c',\n 'ğ': 'g',\n 'ı': 'i',\n 'İ': 'I',\n 'ö': 'o',\n 'ş': 's',\n 'ü': 'u',\n 'Ç': 'C',\n 'Ğ': 'G',\n 'Ö': 'O',\n 'Ş': 'S',\n 'Ü': 'U'\n };\n return str.replace(/[çğışüöÇĞİŞÜÖ]/g, char => charMap[char] || char);\n };\n \n const formattedName = turkishToEnglish(displayName)\n .toLowerCase() // Hepsini küçük harfe çevir\n .split(' ') // Boşluklara göre böl\n .map(word => word.charAt(0) + word.slice(1)) // İlk harfi büyük yap\n .join(''); // Kelimeleri birleştir\n \n setName(formattedName);\n } else {\n setName('');\n }\n }, [displayName]);\n\n\n return (\n <div>\n <div className=\"grid md:grid-cols-2 space-x-5\">\n <div className=\"space-y-3\">\n <Label htmlFor=\"displayName\">Collection Display Name</Label>\n <Input\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n name=\"displayName\" id=\"displayName\" placeholder=\"Collection Display Name\" />\n </div>\n <div className=\"space-y-3\">\n <Label htmlFor=\"name\">Collection Name</Label>\n <Input\n value={name}\n onChange={(e) => setName(e.target.value)}\n name=\"name\" id=\"name\" placeholder=\"Collection Name\" />\n </div>\n </div>\n <Button\n className=\"mt-5\"\n onClick={handleSubmit}>Create Collection</Button>\n </div>\n );\n}\n\nexport default NewCollection;\n"
},
{
"path": "client/components/contentManagement/CollectionList.js",
"content": "import { useRouter } from 'next/router';\nimport { useState, useEffect } from 'react';\nimport { api, schemaManager } from 'yvr-core/client';\nimport CustomTable from '../global/CustomTable';\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Button } from \"@/components/ui/button\"\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\"\nimport { MoreHorizontal } from \"lucide-react\"\nimport Link from \"next/link\"\n\n\nconst CollectionList = () => {\n const [list, setList] = useState([]);\n const [columns, setColumns] = useState([]);\n const [filterColumn, setFilterColumn] = useState(\"\");\n const [mediaTypes, setMediaTypes] = useState(\"\");\n\n const router = useRouter();\n const { collectionName } = router.query;\n\n // Listeyi API'den alma fonksiyonu\n const getList = async () => {\n try {\n const { list } = await api.get(`${collectionName}:getAll?populate=${mediaTypes}`);\n setList(list || []);\n } catch (error) {\n }\n };\n\n // Silme fonksiyonu\n const deleteItem = async (id) => {\n try {\n await api.delete(`${collectionName}:delete?id=${id}`);\n getList();\n } catch (error) {\n }\n };\n\n // Schema'yı API'den alma fonksiyonu\n const getSchema = async () => {\n try {\n const schema = await schemaManager.load(collectionName);\n const mediaFields = schema.fields.filter(field => field.type == 'Media');\n const mediaFieldNames = mediaFields.map(field => field.name);\n setMediaTypes(mediaFieldNames.join(','));\n\n const fields = schema.fields.filter(field => field.name !== 'password' && field.type !== 'RichText' && field.type !== 'Media');\n const tempColumns = [\n {\n id: \"select\",\n header: ({ table }) => (\n <Checkbox\n checked={\n table.getIsAllPageRowsSelected() ||\n (table.getIsSomePageRowsSelected() && \"indeterminate\")\n }\n onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n aria-label=\"Select all\"\n />\n ),\n cell: ({ row }) => (\n <Checkbox\n checked={row.getIsSelected()}\n onCheckedChange={(value) => row.toggleSelected(!!value)}\n aria-label=\"Select row\"\n />\n ),\n enableSorting: false,\n enableHiding: false,\n },\n ...fields.map(field => ({\n accessorKey: field.name,\n header: field.name.charAt(0).toUpperCase() + field.name.slice(1),\n cell: ({ row }) => <div>{row.getValue(field.name)}</div>,\n })),\n {\n id: \"actions\",\n enableHiding: false,\n cell: ({ row }) => {\n const item = row.original;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" className=\"h-8 w-8 p-0\">\n <span className=\"sr-only\">Open menu</span>\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuLabel>Actions</DropdownMenuLabel>\n <DropdownMenuItem\n className=\"capitalize cursor-pointer\"\n onClick={() => navigator.clipboard.writeText(JSON.stringify(item))}\n >\n Copy Object\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem className=\"capitalize cursor-pointer\">\n <Link href={`/content-management/${collectionName}/${item._id}`}>\n Edit\n </Link>\n </DropdownMenuItem>\n <DropdownMenuItem\n className=\"capitalize cursor-pointer hover:bg-red-600 hover:text-white\"\n onClick={(e) => {\n e.stopPropagation();\n deleteItem(item._id)\n }}\n >\n Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n },\n accessorKey: \"Actions\",\n },\n ];\n\n setColumns(tempColumns);\n setFilterColumn(fields.length > 0 ? fields[0].name : \"\");\n } catch (error) {\n }\n };\n\n useEffect(() => {\n if (collectionName) {\n getSchema();\n }\n }, [collectionName]);\n\n useEffect(() => {\n if (collectionName) {\n getList();\n }\n }, [mediaTypes, collectionName]);\n\n return (\n <div>\n {list.length > 0 && (\n <CustomTable\n list={list}\n columns={columns}\n filterColumn={filterColumn}\n getList={getList}\n onClickRow={(item) => router.push(`/content-management/${collectionName}/${item._id}`)}\n />\n )}\n </div>\n );\n};\n\nexport default CollectionList;"
},
{
"path": "client/components/contentManagement/ContentManagementNavigate.js",
"content": "\nimport { toast } from \"@/hooks/use-toast\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/router\";\nimport { useEffect, useState } from \"react\";\nimport { schemaManager } from \"yvr-core/client\";\nimport { Separator } from \"@/components/ui/separator\"\nimport { Button } from \"@/components/ui/button\"\nimport { FolderOpen } from \"lucide-react\"\n\n\nconst ContentManagementNavigate = () => {\n const [collectionList, setCollectionList] = useState([]);\n const router = useRouter();\n const { collectionName } = router.query;\n\n const getCollections = async () => {\n try {\n const schema = await schemaManager.loadAll();\n if (!schema.error) {\n setCollectionList(schema?.map((s) => s.model));\n } else {\n toast({\n description: schema.error,\n variant: \"destructive\"\n });\n }\n } catch (error) {\n toast({\n description: error.message,\n variant: \"destructive\"\n })\n }\n\n }\n\n useEffect(() => {\n getCollections();\n }, []);\n\n return (\n <nav className=\"grid gap-4 text-sm text-muted-foreground\" x-chunk=\"dashboard-04-chunk-0\">\n {collectionList.length > 0 && (\n <>\n <Separator className=\"my-2\" />\n <div className=\"space-y-1\">\n {collectionList?.map((collection, i) => (\n <Button\n key={i}\n asChild\n variant=\"ghost\"\n className={`w-full justify-start ${\n collectionName === collection.name ? \"bg-accent\" : \"\"\n }`}\n >\n <Link\n href={`/content-management/${collection.name}`}\n prefetch={false}\n >\n <FolderOpen className=\"mr-2 h-4 w-4\" />\n {collection.displayName}\n </Link>\n </Button>\n ))}\n </div>\n </>\n )}\n </nav>\n );\n}\n\nexport default ContentManagementNavigate;\n"
},
{
"path": "client/components/contentManagement/NewContent.js",
"content": "import { useRouter } from \"next/router\";\nimport { schemaManager } from \"yvr-core/client\";\nimport { useEffect, useState } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { api } from 'yvr-core/client';\nimport ContentTypes from \"./contentTypes/ContentTypes\";\nimport { toast } from \"@/hooks/use-toast\";\n\nconst NewContent = () => {\n const [schema, setSchema] = useState(null);\n const [formData, setFormData] = useState({});\n\n const router = useRouter();\n const { collectionName, id } = router.query;\n\n const getSchema = async () => {\n const schema = await schemaManager.load(collectionName);\n setSchema(schema);\n }\n\n useEffect(() => {\n collectionName && getSchema();\n }, [collectionName]);\n\n\n const handleChange = (e) => {\n setFormData({\n ...formData,\n [e.target.name]: e.target.value,\n });\n };\n\n const handleSelectChange = (name, value) => {\n setFormData({\n ...formData,\n [name]: value,\n });\n };\n\n const createSubmit = async (e) => {\n e.preventDefault();\n const data = await api.post(`${collectionName}:create`, formData);\n if (data?.error) {\n toast({\n description: data.error,\n variant: \"destructive\",\n });\n } else {\n toast({\n description: \"Item created successfully\",\n });\n router.push(`/content-management/${collectionName}`);\n }\n };\n\n const getItem = async () => {\n const item = await api.get(`${collectionName}:get?filter.id=${id}`);\n setFormData(item);\n };\n\n useEffect(() => {\n id && getItem();\n }, [id, router.query]);\n\n const updateSubmit = async (e) => {\n e.preventDefault();\n const data = await api.put(`${collectionName}:update?id=${id}`, formData);\n if (data?.error) {\n toast({\n description: data.error,\n variant: \"destructive\",\n });\n } else {\n toast({\n description: \"Item updated successfully\",\n });\n router.push(`/content-management/${collectionName}`);\n }\n };\n\n\n return (\n <div>\n <h1\n className=\"text-2xl font-semibold\"\n >\n {id ? \"Edit\" : \"Create\"}\n {\" \"}\n {collectionName}\n </h1>\n <div className=\"mt-5\">\n <form onSubmit={id ? updateSubmit : createSubmit} className=\"space-y-4\">\n <ContentTypes schema={schema} formData={formData} handleChange={handleChange} handleSelectChange={handleSelectChange} />\n <div>\n <Button type=\"submit\" >\n {id ? \"Update\" : \"Create\"}\n </Button>\n </div>\n </form>\n </div>\n </div>\n );\n}\n\nexport default NewContent;"
},
{
"path": "client/components/contentManagement/TypeIcon.js",
"content": "import { Type, Hash, ToggleLeft, Calendar, Key, Image, Link, BookType } from 'lucide-react'\n\nconst TypeIcon = ({ type, size = 24 }) => {\n\n const icon = {\n 'String': <Type size={size} />,\n 'Number': <Hash size={size} />,\n 'Boolean': <ToggleLeft size={size} />,\n 'Date': <Calendar size={size} />,\n 'ObjectId': <Key size={size} />,\n 'Media': <Image size={size} />,\n 'Slug': <Link size={size} />,\n 'RichText': <BookType size={size} />\n }\n\n return (\n <div>\n {icon[type]}\n </div>\n );\n}\n\nexport default TypeIcon;"
},
{
"path": "client/components/contentManagement/contentTypes/BooleanType.js",
"content": "import { Checkbox } from \"@/components/ui/checkbox\"; // Varsayılan checkbox bileşeni\nimport { Label } from \"@/components/ui/label\";\n\nconst BooleanType = ({\n field,\n formData,\n handleChange,\n}) => {\n return (\n <div key={field.name} className=\"grid gap-2\">\n <Label htmlFor={field.name}>{field.name.charAt(0).toUpperCase() + field.name.slice(1)}</Label>\n <Checkbox\n id={field.name}\n name={field.name}\n checked={formData[field.name] || false}\n onCheckedChange={(checked) => handleChange({ target: { name: field.name, value: checked } })}\n >\n {field.name.charAt(0).toUpperCase() + field.name.slice(1)}\n </Checkbox>\n </div>\n );\n}\n\nexport default BooleanType;"
},
{
"path": "client/components/contentManagement/contentTypes/ContentTypes.js",
"content": "import StringType from \"./StringType\";\nimport ObjectId from \"./ObjectId\";\nimport MediaType from \"./MediaType\";\nimport SlugType from \"./SlugType\";\nimport BooleanType from \"./BooleanType\";\nimport dynamic from 'next/dynamic';\nimport NumberType from \"./NumberType\";\nconst RichTextType = dynamic(\n () => import('./RichTextType'),\n { ssr: false }\n);\n\nexport default function ContentTypes({\n schema,\n formData,\n handleChange,\n handleSelectChange\n}) {\n\n return (\n <>\n {schema?.fields?.map((field, index) => {\n if (field.type == \"String\" && !field.enum) {\n return <StringType key={index} field={field} formData={formData} handleChange={handleChange} />\n }\n\n if (field.type == \"ObjectId\") {\n return <ObjectId key={index} field={field} formData={formData} handleChange={handleChange} handleSelectChange={handleSelectChange} />\n }\n\n if(field.type == \"Slug\") {\n return <SlugType key={index} field={field} formData={formData} handleChange={handleChange} />\n }\n\n