├── src ├── env.d.ts ├── components │ ├── ToDos.module.css │ ├── ToDos.jsx │ ├── ToDo.module.css │ ├── ToDo.jsx │ └── Header.astro ├── utils │ └── session.js ├── pages │ ├── api │ │ ├── get-todos.json.js │ │ ├── increment-todo.js │ │ ├── flag-todo.js │ │ └── add-todo.js │ ├── browse.astro │ ├── sign-in.astro │ ├── index.astro │ └── add.astro └── layouts │ └── Layout.astro ├── og.png ├── public └── og.png ├── tsconfig.json ├── astro.config.mjs ├── .gitignore ├── README.md ├── package.json ├── netlify.toml └── LICENSE /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitep4nth3r/findsomethingtodo/HEAD/og.png -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitep4nth3r/findsomethingtodo/HEAD/public/og.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react" 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/ToDos.module.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style: none; 3 | padding-inline-start: 0; 4 | display: flex; 5 | flex-direction: column; 6 | margin-top: 2rem; 7 | margin-bottom: 2rem; 8 | } 9 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import react from "@astrojs/react"; 3 | import netlify from "@astrojs/netlify/functions"; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | output: "server", 8 | integrations: [react()], 9 | adapter: netlify(), 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # Local Netlify folder 24 | .netlify 25 | 26 | .vscode 27 | -------------------------------------------------------------------------------- /src/utils/session.js: -------------------------------------------------------------------------------- 1 | import Clerk from "@clerk/clerk-js"; 2 | 3 | // Change this if you deploy your own! 4 | const CLERK_FRONTEND_API = "pk_test_dW5iaWFzZWQtY2hpY2tlbi03NS5jbGVyay5hY2NvdW50cy5kZXYk"; 5 | 6 | export async function getSession() { 7 | const clerk = new Clerk(CLERK_FRONTEND_API); 8 | await clerk.load({}); 9 | 10 | return clerk.session; 11 | } 12 | 13 | export function initClerk() { 14 | return new Clerk(CLERK_FRONTEND_API); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/api/get-todos.json.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | 3 | async function initDatabase() { 4 | const mongoClient = await MongoClient.connect(import.meta.env.DB_CONN_STRING, { 5 | appName: "findsomethingtodo", 6 | }); 7 | 8 | return mongoClient; 9 | } 10 | 11 | export async function GET({ params, request }) { 12 | const mongo = await initDatabase(); 13 | const todos = mongo.db("findsomethingtodo").collection("todos"); 14 | const todosNotFlagged = await todos.find({ flagged: false }).sort({ date_created: -1 }).toArray(); 15 | 16 | return new Response( 17 | JSON.stringify({ 18 | todos: todosNotFlagged, 19 | }), 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bored? Want to procrastinate and look productive? Find Something ToDo.](og.png) 2 | 3 | # Find Something TODO 4 | 5 | Bored? Want to procrastinate and look productive? 6 | 7 | Find Something ToDo! 8 | 9 | - Browse what people are ToDoing. 10 | - Sign in to add ToDos, complete ToDos, feel productive. 11 | - Fun fact, you'll never see your ToDo again. It's gone forever. You can relax, now! 12 | - Click it, if you did it! 13 | 14 | ## Built With 15 | 16 | - [Astro](https://astro.build) 17 | - [Clerk](https://clerk.com) 18 | - [MongoDB Atlas](https://www.mongodb.com/atlas) 19 | 20 | ## 4 Web Devs, 1 App 21 | 22 | This is part of the [4 Web Devs 1 App series](https://lwj.dev/4d1a). 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "findsomethingtodo", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/netlify": "^3.0.2", 14 | "@astrojs/react": "^3.0.2", 15 | "@clerk/clerk-js": "^4.58.1", 16 | "@types/react": "^18.2.23", 17 | "@types/react-dom": "^18.2.7", 18 | "@whitep4nth3r/get-random-entry": "^0.0.2", 19 | "astro": "^3.1.4", 20 | "bad-words": "^3.0.4", 21 | "mongodb": "^6.1.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0" 24 | }, 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run build" 4 | functions = "netlify/functions" 5 | publish = "dist" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | #[[redirects]] 10 | # from = "/*" 11 | # to = "/index.html" 12 | # status = 200 13 | 14 | ## (optional) Settings for Netlify Dev 15 | ## https://github.com/netlify/cli/blob/main/docs/netlify-dev.md#project-detection 16 | #[dev] 17 | # command = "yarn start" # Command to start your dev server 18 | # port = 3000 # Port that the dev server will be listening on 19 | # publish = "dist" # Folder with the static content for _redirect file 20 | 21 | ## more info on configuring this file: https://docs.netlify.com/configure-builds/file-based-configuration/ 22 | -------------------------------------------------------------------------------- /src/pages/api/increment-todo.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from "mongodb"; 2 | 3 | async function initDatabase() { 4 | const mongoClient = await MongoClient.connect(import.meta.env.DB_CONN_STRING, { 5 | appName: "findsomethingtodo", 6 | }); 7 | 8 | return mongoClient; 9 | } 10 | 11 | export const POST = async ({ request }) => { 12 | if (request.headers.get("Content-Type") === "application/json") { 13 | const body = await request.json(); 14 | const id = body.id; 15 | const objId = new ObjectId(id); 16 | 17 | const mongo = await initDatabase(); 18 | const result = await mongo 19 | .db("findsomethingtodo") 20 | .collection("todos") 21 | .findOneAndUpdate({ _id: objId }, { $inc: { times_done: 1 } }, { returnDocument: "after" }); 22 | 23 | const { times_done } = result; 24 | 25 | return new Response( 26 | JSON.stringify({ 27 | times_done, 28 | }), 29 | ); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/pages/api/flag-todo.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from "mongodb"; 2 | 3 | async function initDatabase() { 4 | const mongoClient = await MongoClient.connect(import.meta.env.DB_CONN_STRING, { 5 | appName: "findsomethingtodo", 6 | }); 7 | 8 | return mongoClient; 9 | } 10 | 11 | export const POST = async ({ request }) => { 12 | if (request.headers.get("Content-Type") === "application/json") { 13 | const body = await request.json(); 14 | const id = body.id; 15 | const objId = new ObjectId(id); 16 | 17 | const mongo = await initDatabase(); 18 | const result = await mongo 19 | .db("findsomethingtodo") 20 | .collection("todos") 21 | .findOneAndUpdate( 22 | { _id: objId }, 23 | { 24 | $set: { 25 | flagged: true, 26 | }, 27 | }, 28 | { returnDocument: "after" }, 29 | ); 30 | 31 | const { flagged } = result; 32 | 33 | return new Response( 34 | JSON.stringify({ 35 | success: flagged === true, 36 | }), 37 | ); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2023 Salma Alam-Naylor 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/components/ToDos.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { getSession } from "../utils/session.js"; 3 | import { ToDo } from "./ToDo.jsx"; 4 | import Styles from "./ToDos.module.css"; 5 | 6 | export function ToDos({ todos, limit }) { 7 | const [filteredTodos, setFilteredTodos] = useState([]); 8 | const [userId, setUserId] = useState(null); 9 | const [showActions, setShowActions] = useState(false); 10 | 11 | useEffect(() => { 12 | // declare the data fetching function 13 | const filterTodos = async () => { 14 | const session = await getSession(); 15 | const userId = session !== null ? session.user.id : "12345"; 16 | setUserId(userId); 17 | 18 | if (session !== null) { 19 | setShowActions(true); 20 | } 21 | 22 | let filtered = todos.filter((todo) => todo.user_id !== userId); 23 | 24 | if (limit !== null) { 25 | filtered = filtered.splice(0, limit); 26 | } 27 | 28 | setFilteredTodos(filtered); 29 | }; 30 | 31 | filterTodos(); 32 | return () => {}; 33 | }, []); 34 | 35 | return ( 36 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/api/add-todo.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | 3 | async function initDatabase() { 4 | const mongoClient = await MongoClient.connect(import.meta.env.DB_CONN_STRING, { 5 | appName: "findsomethingtodo", 6 | }); 7 | 8 | return mongoClient; 9 | } 10 | 11 | export const POST = async ({ request, redirect }) => { 12 | const data = await request.formData(); 13 | const user_id = data.get("user_id"); 14 | const todo = data.get("todo"); 15 | 16 | // add stuff to DB 17 | const newTodo = { 18 | description: todo, 19 | user_id: user_id, 20 | times_done: 0, 21 | date_created: new Date(), 22 | flagged: false, 23 | }; 24 | 25 | const mongo = await initDatabase(); 26 | const todos = mongo.db("findsomethingtodo").collection("todos"); 27 | 28 | const response = await todos.insertOne(newTodo); 29 | 30 | if (response.acknowledged === true) { 31 | return new Response( 32 | JSON.stringify({ 33 | success: true, 34 | }), 35 | { 36 | status: 201, 37 | headers: { 38 | "Content-Type": "application/json", 39 | }, 40 | }, 41 | ); 42 | } 43 | 44 | return new Response( 45 | JSON.stringify({ 46 | success: false, 47 | }), 48 | { 49 | status: 500, 50 | headers: { 51 | "Content-Type": "application/json", 52 | }, 53 | }, 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/pages/browse.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import { GET } from './api/get-todos.json.js' 4 | import { ToDos } from "../components/ToDos.jsx" 5 | 6 | const response = await GET(Astro) 7 | const data = await response.json() 8 | 9 | const { url } = Astro; 10 | const success = url.searchParams.get("success"); 11 | --- 12 | 13 | 14 | 26 | 27 |
28 | {success && ( 29 |

Congrats! Your ToDo will be done by somebody else.

30 | )} 31 | 32 |
33 |

Browse Something ToDo

34 |

Click it, if you did it!

35 | Add a ToDo 36 | 37 |
38 |
39 |
40 | 41 | 71 | -------------------------------------------------------------------------------- /src/components/ToDo.module.css: -------------------------------------------------------------------------------- 1 | .todo { 2 | border: 0.25rem solid var(--yellow); 3 | border-radius: 1rem; 4 | padding: 1rem; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 1rem; 8 | margin-bottom: 2rem; 9 | } 10 | 11 | .isFlagging { 12 | opacity: 0.5; 13 | } 14 | 15 | .topRow { 16 | display: grid; 17 | grid-template-columns: 3fr 1fr; 18 | grid-template-areas: 19 | "desc done" 20 | "completed completed"; 21 | gap: 1rem; 22 | } 23 | 24 | .description { 25 | grid-area: desc; 26 | font-size: 1.6rem; 27 | color: white; 28 | } 29 | 30 | .completed { 31 | grid-area: completed; 32 | font-style: italic; 33 | font-size: 1rem; 34 | line-height: 2; 35 | } 36 | 37 | .done { 38 | flex-grow: 0; 39 | grid-area: done; 40 | appearance: none; 41 | border-width: 0; 42 | background-color: var(--stone); 43 | color: var(--red); 44 | font-family: var(--font-heading); 45 | font-size: 1rem; 46 | text-decoration: none; 47 | padding: 0.5rem 1rem; 48 | border-radius: 2rem; 49 | text-align: center; 50 | height: max-content; 51 | cursor: pointer; 52 | } 53 | 54 | .done:focus, 55 | .done:focus-visible { 56 | outline: transparent; 57 | outline-style: solid; 58 | box-shadow: var(--green-dark) 0 0 0 0.08rem, var(--green-light) 0 0 0 0.32rem; 59 | transition: box-shadow 0.2s ease-in-out; 60 | } 61 | 62 | .done[disabled] { 63 | opacity: 0.8; 64 | cursor: not-allowed; 65 | } 66 | 67 | .report { 68 | appearance: none; 69 | border-width: 0; 70 | background-color: transparent; 71 | display: inline; 72 | width: max-content; 73 | color: var(--yellow); 74 | font-size: 1; 75 | font-family: var(--font-body); 76 | text-transform: uppercase; 77 | border-radius: 0.75rem; 78 | } 79 | 80 | .report:focus, 81 | .report:focus-visible { 82 | outline: transparent; 83 | outline-style: solid; 84 | box-shadow: var(--green-dark) 0 0 0 0.08rem, var(--green-light) 0 0 0 0.32rem; 85 | transition: box-shadow 0.2s ease-in-out; 86 | } 87 | -------------------------------------------------------------------------------- /src/pages/sign-in.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | --- 4 | 5 | 6 | 20 | 21 |
22 |

Sign in

23 |

Add ToDos, complete ToDos, feel productive.

24 |
27 |
28 |
29 | 30 | 70 | 71 | 87 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import { GET } from './api/get-todos.json.js' 4 | import { ToDos } from "../components/ToDos.jsx" 5 | 6 | const response = await GET(Astro) 7 | const data = await response.json() 8 | --- 9 | 10 | 11 |
12 |

Bored?

13 |

Want to procrastinate and look productive?

14 |

Find Something ToDo

15 | 16 |

What people are currently ToDoing

17 | 18 | 19 | 20 |
21 |

Want to join the party?

22 | Add your ToDo 23 | Browse all ToDos 24 |
25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /src/components/ToDo.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Styles from "./ToDo.module.css"; 3 | import { getRandomEntry } from "@whitep4nth3r/get-random-entry"; 4 | 5 | const randomYays = [ 6 | "Great job!", 7 | "Yes!", 8 | "Amazing!", 9 | "Well done!", 10 | "Awesome!", 11 | "Nice!", 12 | "Keep it up!", 13 | "Fantastic!", 14 | "Good job!", 15 | "You did it!", 16 | "You rock!", 17 | "Great!", 18 | ]; 19 | 20 | export const ToDo = ({ todo, showActions }) => { 21 | const { _id, description, times_done, date_created, user_id, flagged } = todo; 22 | 23 | const [timesDoneNo, setTimesDoneNo] = useState(times_done); 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [buttonText, setButtonText] = useState("I did this"); 26 | const [buttonDisabled, setButtonDisabled] = useState(false); 27 | const [isFlagging, setIsFlagging] = useState(false); 28 | const [isFlagged, setIsFlagged] = useState(false); 29 | 30 | async function increment() { 31 | setIsLoading(true); 32 | 33 | const response = await fetch("/api/increment-todo", { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | }, 38 | body: JSON.stringify({ id: _id }), 39 | }); 40 | 41 | const data = await response.json(); 42 | setTimesDoneNo(data.times_done); 43 | setIsLoading(false); 44 | setButtonText(getRandomEntry(randomYays)); 45 | setButtonDisabled(true); 46 | } 47 | 48 | async function report() { 49 | setIsFlagging(true); 50 | 51 | const response = await fetch("/api/flag-todo", { 52 | method: "POST", 53 | headers: { 54 | "Content-Type": "application/json", 55 | }, 56 | body: JSON.stringify({ id: _id }), 57 | }); 58 | 59 | const data = await response.json(); 60 | 61 | if (data.success === true) { 62 | setIsFlagged(true); 63 | } 64 | } 65 | 66 | return ( 67 | !isFlagged && ( 68 |
69 |
70 |

{description}

71 |

72 | Completed {timesDoneNo} {timesDoneNo === 1 ? "time" : "times"} 73 |

74 | {showActions && ( 75 | 79 | )} 80 |
81 | {/* */} 84 |
85 | ) 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { currentPath } = Astro.props; 3 | --- 4 | 5 | 33 | 34 |
35 | 36 | 37 | 43 |
44 | 45 | 89 | 90 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Header from "../components/Header.astro"; 3 | const { title, description } = Astro.props; 4 | const { pathname } = Astro.url; 5 | 6 | --- 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {title} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 125 | -------------------------------------------------------------------------------- /src/pages/add.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | --- 4 | 20 | 21 | 22 |
23 | 65 | 66 |
67 |

Add Something ToDo

68 |

Offload those tasks!

69 |
70 | 71 | 72 |

⚠️ Please don't use bad words!

73 |

Sorry, something went wrong. Please try again.

74 | 75 | 76 |
77 | 78 |

Fun fact, you'll never see your ToDo again. It's gone forever. You can relax, now!

79 |
80 |
81 |
82 | 83 | 157 | --------------------------------------------------------------------------------