├── 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 | 
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 |
37 | {filteredTodos.map((item) => (
38 | -
39 |
40 |
41 | ))}
42 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------