├── .eslintrc.json ├── bun.lockb ├── .env.copy ├── app ├── favicon.ico ├── layout.tsx ├── globals.css └── page.tsx ├── next.config.mjs ├── postcss.config.mjs ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── package.json ├── tsconfig.json ├── cosmic └── client.ts ├── actions └── index.tsx ├── components ├── ToDoForm.tsx ├── ToDos.tsx ├── GitHubLink.tsx └── Footer.tsx └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-next-todo/main/bun.lockb -------------------------------------------------------------------------------- /.env.copy: -------------------------------------------------------------------------------- 1 | # .env.local 2 | COSMIC_BUCKET_SLUG= 3 | COSMIC_READ_KEY= 4 | COSMIC_WRITE_KEY= -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-next-todo/main/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | // app/layout.tsx 2 | import "./globals.css"; 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: ["./pages/**/*.{js,ts,jsx,tsx,mdx}","./components/**/*.{js,ts,jsx,tsx,mdx}","./app/**/*.{js,ts,jsx,tsx,mdx}","./cosmic/**/*.{ts,tsx,js,jsx}"], 5 | theme: { 6 | extend: { 7 | backgroundImage: { 8 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 9 | "gradient-conic": 10 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | export default config; 17 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@cosmicjs/sdk": "^1.0.11", 13 | "next": "14.2.3", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "typescript": "^5", 19 | "@types/node": "^20", 20 | "@types/react": "^18", 21 | "@types/react-dom": "^18", 22 | "postcss": "^8", 23 | "tailwindcss": "^3.4.1", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.2.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | // app/page.tsx 2 | export const dynamic = "force-dynamic"; 3 | 4 | import { ToDos } from "@/components/ToDos"; 5 | import { getToDos } from "@/actions"; 6 | import { Footer } from "@/components/Footer"; 7 | import { GitHubLink } from "@/components/GitHubLink"; 8 | export default async function ToDoPage() { 9 | let todos = []; 10 | try { 11 | todos = await getToDos(); 12 | } catch (e) {} 13 | return ( 14 | <> 15 | 16 |
17 |

My ToDos

18 |
19 | 20 |
21 |
22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /cosmic/client.ts: -------------------------------------------------------------------------------- 1 | import { createBucketClient } from "@cosmicjs/sdk"; 2 | 3 | if (!process.env.COSMIC_BUCKET_SLUG) 4 | console.error( 5 | "Error: Environment variables missing. You need to create an environment variable file and include COSMIC_BUCKET_SLUG, COSMIC_READ_KEY, and COSMIC_WRITE_KEY environment variables." 6 | ); 7 | // Make sure to add/update your ENV variables 8 | export const cosmic = createBucketClient({ 9 | bucketSlug: 10 | process.env.COSMIC_BUCKET_SLUG || 11 | "You need to add your COSMIC_BUCKET_SLUG environment variable.", 12 | readKey: 13 | process.env.COSMIC_READ_KEY || 14 | "You need to add your COSMIC_READ_KEY environment variabl.", 15 | writeKey: 16 | process.env.COSMIC_WRITE_KEY || 17 | "You need to add your COSMIC_WRITE_KEY environment variable.", 18 | }); 19 | -------------------------------------------------------------------------------- /actions/index.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cosmic } from "@/cosmic/client"; 4 | 5 | export async function getToDos() { 6 | const { objects: todos } = await cosmic.objects 7 | .find({ type: "todos" }) 8 | .props("id,title,metadata.completed") 9 | .sort("created_at"); 10 | return todos; 11 | } 12 | 13 | export async function addToDo(title: string) { 14 | await cosmic.objects.insertOne({ 15 | type: "todos", 16 | title, 17 | metadata: { 18 | completed: false, 19 | }, 20 | }); 21 | } 22 | 23 | export async function updateToDo(id: string, completed: boolean) { 24 | await cosmic.objects.updateOne(id, { 25 | metadata: { 26 | completed, 27 | }, 28 | }); 29 | } 30 | 31 | export async function deleteToDo(id: string) { 32 | await cosmic.objects.deleteOne(id); 33 | } 34 | -------------------------------------------------------------------------------- /components/ToDoForm.tsx: -------------------------------------------------------------------------------- 1 | export function ToDoForm({ 2 | handleTitleChange, 3 | handleAddToDo, 4 | todoTitle, 5 | }: { 6 | handleTitleChange: (title: string) => void; 7 | handleAddToDo: (title: string) => void; 8 | todoTitle: string; 9 | }) { 10 | return ( 11 |
12 | ) => { 16 | handleTitleChange(e.currentTarget?.value); 17 | }} 18 | onKeyDown={(e: React.KeyboardEvent) => { 19 | if (e.key === "Enter") { 20 | handleAddToDo(todoTitle); 21 | } 22 | }} 23 | placeholder="Enter todo task" 24 | autoFocus 25 | value={todoTitle} 26 | /> 27 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmic Next ToDo 2 | ![ToDo app screenshot](https://imgix.cosmicjs.com/f082fc30-16c1-11ef-9eca-7d347081a9fb-CleanShot-2024-05-20-at-08.58.322x.png) 3 | 4 | [[View Live Demo](https://cosmic-next-todo.vercel.app/)] 5 | 6 | A ToDo app example that demontrates how to use Cosmic create, read, update, and delete methods using the [Cosmic JavaScript SDK](https://www.npmjs.com/package/@cosmicjs/sdk) and React Server Actions. 7 | 8 | ## Features 9 | 10 | - React Server Components 11 | - Server Actions (No exposed API keys) 12 | - Tailwind CSS 13 | 14 | ## Getting Started 15 | 16 | First, clone this repo. 17 | 18 | ```bash 19 | git clone https://github.com/cosmicjs/cosmic-next-todo 20 | cd cosmic-next-todo 21 | ``` 22 | 23 | Then install packages. 24 | 25 | ```bash 26 | npm i 27 | # or 28 | yarn 29 | # or 30 | pnpm 31 | # or 32 | bun i 33 | ``` 34 | 35 | ## Create Project in Cosmic 36 | 37 | Log in to the [Cosmic dashboard](https://app.cosmicjs.com/) and create a new empty Project. 38 | ![Create Project](https://imgix.cosmicjs.com/8e311430-0bd7-11ef-9eca-7d347081a9fb-create-new-project.png?w=2000&auto=forat,compression) 39 | 40 | Create an Object type `ToDos` with slug `todos`: 41 | ![Create Object Type](https://imgix.cosmicjs.com/e457e220-160f-11ef-9eca-7d347081a9fb-CleanShot-2024-05-19-at-11.44.112x.png?w=2000&auto=forat,compression) 42 | 43 | Add the switch Metafield with key `completed`. 44 | ![Add completed Metafield](https://imgix.cosmicjs.com/e5873a60-160f-11ef-9eca-7d347081a9fb-CleanShot-2024-05-19-at-11.43.322x.png?w=2000&auto=forat,compression) 45 | 46 | Then copy the `.env.copy` to a new `.env.local` file. And add your API keys found in the Cosmic dashboard at _Project / API keys_. 47 | 48 | ``` 49 | # .env.local 50 | COSMIC_BUCKET_SLUG=your_bucket_slug 51 | COSMIC_READ_KEY=your_bucket_read_key 52 | COSMIC_WRITE_KEY=your_bucket_write_key 53 | ``` 54 | 55 | ## Run the app 56 | 57 | Then run the development server: 58 | 59 | ```bash 60 | npm run dev 61 | # or 62 | yarn dev 63 | # or 64 | pnpm dev 65 | # or 66 | bun dev 67 | ``` 68 | 69 | Open [http://localhost:3000](http://localhost:3000) with your browser to see your ToDo list. Add / edit / delete ToDo items. See your ToDos in the Cosmic dashboard as well. 70 | 71 | ## Contributing 72 | Contributions welcome! 73 | -------------------------------------------------------------------------------- /components/ToDos.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { getToDos, addToDo, updateToDo, deleteToDo } from "@/actions"; 5 | import { ToDoForm } from "@/components/ToDoForm"; 6 | 7 | export type ToDoType = { 8 | id: string; 9 | title: string; 10 | metadata: { completed: boolean }; 11 | }; 12 | 13 | export function ToDos({ todos }: { todos: ToDoType[] | [] }) { 14 | const [clientTodos, setClientTodos] = useState(todos); 15 | const [todoTitle, setTodoTitle] = useState(""); 16 | const [disabled, setDisabled] = useState(false); 17 | function handleStatusChange(todo: ToDoType) { 18 | updateToDo(todo.id, !todo.metadata.completed); 19 | const updatedTodos = clientTodos.map((updatedTodo) => { 20 | if (updatedTodo.id === todo.id) 21 | updatedTodo.metadata.completed = !updatedTodo.metadata.completed; 22 | return updatedTodo; 23 | }); 24 | setClientTodos(updatedTodos); 25 | } 26 | async function handleAddToDo(title: string) { 27 | if (disabled) return; 28 | if (!title.trim()) return; 29 | addToDo(title); 30 | // Reset title field 31 | setTodoTitle(""); 32 | // Instant data change 33 | setClientTodos([ 34 | ...clientTodos, 35 | { id: "temporary-id", title, metadata: { completed: false } }, 36 | ]); 37 | // Disable add 38 | setDisabled(true); 39 | // Refetch real data 40 | const todos = await getToDos(); 41 | setClientTodos(todos); 42 | setDisabled(false); 43 | } 44 | async function handleDeleteClick(id: string) { 45 | deleteToDo(id); 46 | // Instant data change 47 | setClientTodos(clientTodos?.filter((todo) => todo.id !== id)); 48 | } 49 | function handleTitleChange(title: string) { 50 | setTodoTitle(title); 51 | } 52 | return ( 53 | <> 54 |
55 | {clientTodos.map((todo: ToDoType) => { 56 | return ( 57 |
58 | handleStatusChange(todo)} 60 | type="checkbox" 61 | checked={todo.metadata.completed} 62 | className="cursor-pointer" 63 | /> 64 |   65 |
handleStatusChange(todo)} 67 | className={ 68 | todo.metadata.completed 69 | ? "cursor-pointer line-through" 70 | : "cursor-pointer" 71 | } 72 | > 73 | {todo.title} 74 |
75 |
handleDeleteClick(todo.id)} 78 | > 79 | delete 80 |
81 |
82 | ); 83 | })} 84 |
85 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /components/GitHubLink.tsx: -------------------------------------------------------------------------------- 1 | export function GitHubLink() { 2 | return ( 3 | 7 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export function Footer() { 2 | return ( 3 | 4 | Powered by 5 | 13 | 22 | 26 | 30 | 34 | 38 | 42 | 46 | 50 | 54 | 58 | 62 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | --------------------------------------------------------------------------------