├── .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 | 
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 | 
39 |
40 | Create an Object type `ToDos` with slug `todos`:
41 | 
42 |
43 | Add the switch Metafield with key `completed`.
44 | 
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 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | export function Footer() {
2 | return (
3 |
4 | Powered by
5 |
13 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------