diff --git a/.TODO b/.TODO deleted file mode 100644 index e2ff786..0000000 --- a/.TODO +++ /dev/null @@ -1,4 +0,0 @@ -[ ] An audit log -[ ] Invite code pruning -[ ] Make the journal work (i dont really remember what was left out though) -[ ] Write a 404 page diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8416870 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +- [ ] An audit log +- [ ] Invite code pruning +- [ ] Make the journal work (i dont really remember what was left out though) +- [x] Write a 404 page +- [ ] Make better login pages +- [ ] You do realize using toLocaleString on the server only makes it use your locale right +- [ ] Make recent updates also account for new journal entries +- [ ] View all previous moods and journal entries diff --git a/db/schema.ts b/db/schema.ts index 5cfb5b5..ad9ef68 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -32,6 +32,7 @@ export const journalEntries = pgTable("journal_entries", { .references(() => users.id, { onDelete: "cascade" }) .notNull(), moodChange: integer("mood-change").default(0).notNull(), + title: varchar({ length: 64 }).default("").notNull(), entry: varchar({ length: 4096 }).default("").notNull(), visibility: integer().default(1).notNull(), date: timestamp().notNull() diff --git a/drizzle/0001_messy_fallen_one.sql b/drizzle/0001_messy_fallen_one.sql new file mode 100644 index 0000000..51f99ea --- /dev/null +++ b/drizzle/0001_messy_fallen_one.sql @@ -0,0 +1 @@ +ALTER TABLE "journal_entries" ADD COLUMN "title" varchar(128) DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/drizzle/0002_special_ben_parker.sql b/drizzle/0002_special_ben_parker.sql new file mode 100644 index 0000000..ee22776 --- /dev/null +++ b/drizzle/0002_special_ben_parker.sql @@ -0,0 +1 @@ +ALTER TABLE "journal_entries" ALTER COLUMN "title" SET DATA TYPE varchar(64); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..e4d9fb8 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "a5d70822-0a93-4193-9115-c41b2ee9f5d8", + "prevId": "05baee8f-73c9-4001-b69d-ba7c46dadf5c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(22)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confers": { + "name": "confers", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..b98b82e --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "f8e1fcdc-dfa2-41cd-886a-5f29a1c091df", + "prevId": "a5d70822-0a93-4193-9115-c41b2ee9f5d8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(22)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confers": { + "name": "confers", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c4adb34..a018abb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1733952034836, "tag": "0000_hard_leader", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1735184859424, + "tag": "0001_messy_fallen_one", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1735185254730, + "tag": "0002_special_ben_parker", + "breakpoints": true } ] } \ No newline at end of file diff --git a/main.ts b/main.ts index 949c21a..4447de6 100644 --- a/main.ts +++ b/main.ts @@ -91,12 +91,13 @@ object-src 'none'; base-uri 'none';" return { user: e.user, mood: moods[e.mood], - date: date.to(dayjs(e.date)), + date: e.date, + relativeDate: date.to(dayjs(e.date)), desc: e.desc }; }); - render(db, "index", "Home", res, req, { + render(db, "index", "home", res, req, { users: (await db.select().from(users)).length, recentUpdates, feedUpdates diff --git a/routes/admin.ts b/routes/admin.ts index b1d9f79..5b169e3 100644 --- a/routes/admin.ts +++ b/routes/admin.ts @@ -36,7 +36,7 @@ export default function (app: Express, db: NodePgDatabase) { expiresString: now.to(dayjs(e.expires)) }; }); - render(db, "admin", "Admin Panel", res, req, { codes }); + render(db, "admin", "admin panel", res, req, { codes }); }); app.post("/codes/delete", async (req, res) => { diff --git a/routes/login.ts b/routes/login.ts index 0130c24..22a98e7 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -13,7 +13,7 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/"); return; } - render(db, "register", "Sign up", res, req); + render(db, "register", "sign up", res, req); }); app.post("/register", async (req, res) => { if (req.session["loggedIn"]) { @@ -49,22 +49,6 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/register"); return; } - - // invite code checking - const code = (await db.select({ expires: inviteCodes.expires, confers: inviteCodes.confers }).from(inviteCodes).where(eq(inviteCodes.token, req.body.referral)).limit(1))[0]; - if (!code) { - req.flash("error", "Invalid invite code! Make sure you pasted it in correctly WITH the hyphens."); - res.redirect("/register"); - return; - } - const expiration = code.expires.getTime(); - if (expiration > 0 && Date.now() >= expiration) { - req.flash("error", "That code is expired."); - res.redirect("/register"); - return; - } - // we're verified now so get that dumb fucker out of the database - await db.delete(inviteCodes).where(eq(inviteCodes.token, req.body.referral)); // field conflicts if ( @@ -84,6 +68,22 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/register"); return; } + + // invite code checking + const code = (await db.select({ expires: inviteCodes.expires, confers: inviteCodes.confers }).from(inviteCodes).where(eq(inviteCodes.token, req.body.referral)).limit(1))[0]; + if (!code) { + req.flash("error", "Invalid invite code! Make sure you pasted it in correctly WITH the hyphens."); + res.redirect("/register"); + return; + } + const expiration = code.expires.getTime(); + if (expiration > 0 && Date.now() >= expiration) { + req.flash("error", "That code is expired."); + res.redirect("/register"); + return; + } + // we're verified now so get that dumb fucker out of the database + await db.delete(inviteCodes).where(eq(inviteCodes.token, req.body.referral)); const hash = await bcrypt.hash(req.body.pass, 10); const { uid } = ( @@ -117,7 +117,7 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/"); return; } - render(db, "login", "Log in", res, req); + render(db, "login", "log in", res, req); }); app.post("/login", async (req, res) => { if (req.session["loggedIn"]) { diff --git a/routes/updates.ts b/routes/updates.ts index 5fadc6c..9334c9e 100644 --- a/routes/updates.ts +++ b/routes/updates.ts @@ -1,219 +1,239 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres"; import { Express } from "express"; import { - follows, - inviteCodes, - journalEntries, - profiles, - updates, - users + follows, + inviteCodes, + journalEntries, + profiles, + updates, + users } from "../db/schema.js"; import { and, count, desc, eq, sql } from "drizzle-orm"; import dayjs from "dayjs"; -import { getMoods, render } from "./util.js"; +import { getMoods, render, render404 } from "./util.js"; export default async function (app: Express, db: NodePgDatabase) { - const { moods, moodsSorted } = await getMoods(); + const { moods, moodsSorted } = await getMoods(); - // DASHBOARD - app.get("/dashboard", async (req, res) => { - if (!req.session["loggedIn"]) { - res.redirect("/login"); - return; - } - const user = ( - await db - .select({ - name: users.name, - bio: profiles.bio, - website: profiles.website //! validate this - }) - .from(users) - .where(eq(users.name, req.session["user"])) - .leftJoin(profiles, eq(users.id, profiles.user)) - )[0]; - - const now = dayjs(); - const moodHistory = ( - await db - .select({ mood: updates.mood, date: updates.date }) - .from(updates) - .where(eq(updates.user, req.session["uid"])) - .orderBy(desc(updates.date)) - .limit(10) - ).map((e) => { - return { mood: moods[e.mood], date: now.to(dayjs(e.date)) }; - }); - - const recentUpdates = ( - await db - .select({ - user: users.name, - mood: updates.mood, - desc: updates.description, - date: updates.date - }) - .from(updates) - .innerJoin( - follows, - and( - eq(follows.userId, updates.user), - eq(follows.followerId, req.session["uid"]) - ) - ) - .leftJoin(users, eq(updates.user, users.id)) - .orderBy(desc(updates.date)) - .limit(25) - ).map((e) => { - return { - user: e.user, - mood: moods[e.mood], - desc: e.desc, - date: now.to(dayjs(e.date)) - }; - }); - - // user invite codes - const codes = (await db - .select({ token: inviteCodes.token, expires: inviteCodes.expires }) - .from(inviteCodes) - .where(eq(inviteCodes.user, req.session["uid"]))).map((e) => { - return { - token: e.token, - expires: now.to(dayjs(e.expires || 0)) + // DASHBOARD + app.get("/dashboard", async (req, res) => { + if (!req.session["loggedIn"]) { + res.redirect("/login"); + return; } - }); - const { codesUsed } = ( - await db - .select({ codesUsed: count() }) - .from(inviteCodes) - .where( - and( - eq(inviteCodes.user, req.session["uid"]), - eq( - sql`extract(month from granted)`, - sql`extract(month from current_date)` - ) - ) - ) - )[0]; + const user = ( + await db + .select({ + name: users.name, + bio: profiles.bio, + website: profiles.website //! validate this + }) + .from(users) + .where(eq(users.name, req.session["user"])) + .leftJoin(profiles, eq(users.id, profiles.user)) + )[0]; - render(db, "dashboard", "Dashboard", res, req, { - user, - moods, - moodsSorted, - moodHistory, - recentUpdates, - codes, - codesUsed, - feed: [] + const now = dayjs(); + const moodHistory = ( + await db + .select({ mood: updates.mood, date: updates.date }) + .from(updates) + .where(eq(updates.user, req.session["uid"])) + .orderBy(desc(updates.date)) + .limit(10) + ).map((e) => { + return { mood: moods[e.mood], date: now.to(dayjs(e.date)) }; + }); + + const recentUpdates = ( + await db + .select({ + user: users.name, + mood: updates.mood, + desc: updates.description, + date: updates.date + }) + .from(updates) + .innerJoin( + follows, + and( + eq(follows.userId, updates.user), + eq(follows.followerId, req.session["uid"]) + ) + ) + .leftJoin(users, eq(updates.user, users.id)) + .orderBy(desc(updates.date)) + .limit(25) + ).map((e) => { + return { + user: e.user, + mood: moods[e.mood], + desc: e.desc, + date: e.date, + relativeDate: now.to(dayjs(e.date)) + }; + }); + + // user invite codes + const codes = ( + await db + .select({ + token: inviteCodes.token, + expires: inviteCodes.expires + }) + .from(inviteCodes) + .where(eq(inviteCodes.user, req.session["uid"])) + ).map((e) => { + return { + token: e.token, + expires: now.to(dayjs(e.expires || 0)) + }; + }); + const { codesUsed } = ( + await db + .select({ codesUsed: count() }) + .from(inviteCodes) + .where( + and( + eq(inviteCodes.user, req.session["uid"]), + eq( + sql`extract(month from granted)`, + sql`extract(month from current_date)` + ) + ) + ) + )[0]; + + render(db, "dashboard", "dashboard", res, req, { + user, + moods, + moodsSorted, + moodHistory, + recentUpdates, + codes, + codesUsed, + feed: [] + }); }); - }); - app.post("/update/mood", async (req, res) => { - if (!req.session["loggedIn"]) { - res.redirect("/login"); - return; - } - const moodIndex = moods.indexOf(req.body.mood.trim()); - if (moodIndex === -1) { - req.flash( - "error", - "That mood doesn't exist in the database, WTF are you trying to do??" - ); - res.redirect("/dashboard"); - return; - } - if (req.body.desc.length > 512) { - req.flash( - "error", - "Mood description can't be longer than 512 characters" - ); - res.redirect("/dashboard"); - } + app.post("/update/mood", async (req, res) => { + if (!req.session["loggedIn"]) { + res.redirect("/login"); + return; + } + const moodIndex = moods.indexOf(req.body.mood.trim()); + if (moodIndex === -1) { + req.flash( + "error", + "That mood doesn't exist in the database, WTF are you trying to do??" + ); + res.redirect("/dashboard"); + return; + } + if (req.body.desc.length > 512) { + req.flash( + "error", + "Mood description can't be longer than 512 characters" + ); + res.redirect("/dashboard"); + } - await db - .insert(updates) - // @ts-expect-error - .values({ - user: req.session["uid"], - mood: moodIndex, - description: req.body.desc, - date: new Date(Date.now()) - }); - req.flash("success", "Mood updated!"); - res.redirect("/dashboard"); - }); + await db + .insert(updates) + // @ts-expect-error + .values({ + user: req.session["uid"], + mood: moodIndex, + description: req.body.desc, + date: new Date(Date.now()) + }); + req.flash("success", "Mood updated!"); + res.redirect("/dashboard"); + }); - // JOURNAL - app.get("/journal", async (req, res) => { - render(db, "journal", "Journal", res, req); - }); - app.get("/journal/:id", async (req, res) => { - const entry = ( - await db - .select({ - uname: users.name, - content: journalEntries.entry, - date: journalEntries.date - }) - .from(journalEntries) - .where(eq(journalEntries.id, parseInt(req.params.id))) - .leftJoin(users, eq(journalEntries.user, users.id)) - )[0]; - if (!entry) { - //! TODO write a 404 page - res.statusCode = 404; - res.write("404 not found?? :("); - return; - } - render(db, "journal_view", "Journal Entry", res, req, { entry }); - }); - app.post("/update/journal", async (req, res) => { - if (!req.session["loggedIn"]) { - res.redirect("/login"); - return; - } - if (req.body.description.length > 4096) { - req.flash("error", "Entry too long!"); - res.redirect("/journal"); - return; - } + // JOURNAL + app.get("/journal", async (req, res) => { + render(db, "journal", "your journal", res, req); + }); + app.get("/journal/:id", async (req, res) => { + const entry = ( + await db + .select({ + uname: users.name, + title: journalEntries.title, + content: journalEntries.entry, + date: journalEntries.date + }) + .from(journalEntries) + .where(eq(journalEntries.id, parseInt(req.params.id))) + .leftJoin(users, eq(journalEntries.user, users.id)) + )[0]; + if (!entry) { + render404(db, res, req); + return; + } - const moodChange = parseInt(req.body.moodDelta); - const visibility = parseInt(req.body.visibility); - if (isNaN(moodChange) || isNaN(visibility)) { - req.flash("error", "One of the values was improperly specified."); - res.redirect("/journal"); - return; - } + const entryTimestamp = dayjs(entry.date).fromNow(); + render(db, "journal_view", entry.title, res, req, { + entry, + entryTimestamp + }); + }); + app.post("/update/journal", async (req, res) => { + if (!req.session["loggedIn"]) { + res.redirect("/login"); + return; + } + if (req.body.title.length > 64) { + req.flash("error", "Title too long!"); + res.redirect("/journal"); + return; + } + if (req.body.description.length > 4096) { + req.flash("error", "Entry too long!"); + res.redirect("/journal"); + return; + } - let id: number; - try { - const entry = await db - .insert(journalEntries) - // @ts-expect-error - .values({ - user: req.session["uid"], - moodChange, - visibility, - entry: req.body.description, - date: new Date(Date.now()) - }) - .returning({ id: journalEntries.id }); - id = entry[0].id; - } catch (err) { - req.flash( - "error", - "Failed to create your entry. Try again later or send these logs to roxwize so she can know what's up:

" + - err - ); - res.redirect("/journal"); - return; - } - req.flash( - "success", - `Your journal entry is now available as #${id}!` - ); - res.redirect("/journal"); - }); + const moodChange = parseInt(req.body.moodDelta); + const visibility = parseInt(req.body.visibility); + if (isNaN(moodChange) || isNaN(visibility)) { + req.flash("error", "One of the values was improperly specified."); + res.redirect("/journal"); + return; + } + + let id: number; + try { + const entry = await db + .insert(journalEntries) + // @ts-expect-error + .values({ + user: req.session["uid"], + moodChange, + visibility, + title: req.body.title, + entry: req.body.description + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("\n", "
"), + date: new Date(Date.now()) + }) + .returning({ id: journalEntries.id }); + id = entry[0].id; + } catch (err) { + req.flash( + "error", + "Failed to create your entry. Try again later or send these logs to roxwize so she can know what's up:

" + + err + ); + res.redirect("/journal"); + return; + } + req.flash( + "success", + `Your journal entry is now available as #${id}!` + ); + res.redirect("/journal"); + }); } diff --git a/routes/users.ts b/routes/users.ts index b4c6125..e7a9413 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -1,168 +1,216 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres"; import { Express } from "express"; -import { follows, profiles, updates, users } from "../db/schema.js"; +import { + follows, + journalEntries, + profiles, + updates, + users +} from "../db/schema.js"; import { and, desc, eq } from "drizzle-orm"; -import { getMoods, render, UserStatus } from "./util.js"; +import { getMoods, render, render404, UserStatus } from "./util.js"; import { PgColumn } from "drizzle-orm/pg-core"; import dayjs from "dayjs"; export default async function (app: Express, db: NodePgDatabase) { - const { moods } = await getMoods(); - app.get("/users/:user", async (req, res) => { - const isSelf = req.params.user === req.session["user"]; + const { moods } = await getMoods(); + app.get("/users/:user", async (req, res) => { + const isSelf = req.params.user === req.session["user"]; - const user = ( - await db - .select({ - id: users.id, - name: users.name, - bio: profiles.bio, - website: profiles.website - }) - .from(users) - .where(eq(users.name, req.params.user)) - .leftJoin(profiles, eq(profiles.user, users.id)) - )[0]; - if (!user) { - req.flash("error", `User ${req.params.user} does not exist`); - res.redirect("/"); - return; - } + const user: { + id: number; + name: string; + registered: Date; + relativeRegistered?: string; + bio: string; + website: string; + } = ( + await db + .select({ + id: users.id, + name: users.name, + registered: users.registered, + bio: profiles.bio, + website: profiles.website + }) + .from(users) + .where(eq(users.name, req.params.user)) + .leftJoin(profiles, eq(profiles.user, users.id)) + )[0]; + if (!user) { + render404(db, res, req); + return; + } + user.relativeRegistered = dayjs(user.registered).fromNow(); - // follows - const isFollowing = !!( - await db - .select() - .from(follows) - .where( - and( - eq(follows.followerId, req.session["uid"]), - eq(follows.userId, user.id) - ) - ) - .limit(1) - )[0]; + // follows + const isFollowing = !!( + await db + .select() + .from(follows) + .where( + and( + eq(follows.followerId, req.session["uid"]), + eq(follows.userId, user.id) + ) + ) + .limit(1) + )[0]; - // mood - let moodSelection: { [k: string]: PgColumn } = { - desc: updates.description, - date: updates.date - }; - if (!isSelf) moodSelection.mood = updates.mood; - const userMood: { [k: string]: string | number | Date } = ( - await db - .select(moodSelection) - .from(updates) - .where(eq(updates.user, user.id)) - .orderBy(desc(updates.date)) - .limit(1) - )[0]; + // mood + let moodSelection: { [k: string]: PgColumn } = { + desc: updates.description, + date: updates.date + }; + if (!isSelf) moodSelection.mood = updates.mood; + const userMood: { [k: string]: string | number | Date } = ( + await db + .select(moodSelection) + .from(updates) + .where(eq(updates.user, user.id)) + .orderBy(desc(updates.date)) + .limit(1) + )[0]; - // feed - const now = dayjs(); - const userMoodFeed = (await db - .select({ - mood: updates.mood, - date: updates.date, - desc: updates.description - }) - .from(updates) - .where(eq(updates.user, user.id))).map((e) => { - return { user: user.name, mood: moods[e.mood], date: now.to(dayjs(e.date)), desc: e.desc } - }); + // journal entries + const now = dayjs(); + const userJournalEntries = ( + await db + .select({ + id: journalEntries.id, + title: journalEntries.title, + date: journalEntries.date + }) + .from(journalEntries) + .where(eq(journalEntries.user, user.id)) + .orderBy(desc(journalEntries.date)) + .limit(5) + ).map((e) => { + return { + id: e.id, + title: e.title, + date: e.date, + relativeDate: now.to(e.date) + }; + }); - if (!isSelf) { - userMood.mood = moods[userMood.mood as number]; - } + // feed + const userMoodFeed = ( + await db + .select({ + mood: updates.mood, + date: updates.date, + desc: updates.description + }) + .from(updates) + .where(eq(updates.user, user.id)) + .orderBy(desc(updates.date)) + ).map((e) => { + return { + user: user.name, + mood: moods[e.mood], + date: e.date, + relativeDate: now.to(dayjs(e.date)), + desc: e.desc + }; + }); - render(db, "user", `${req.params.user}'s Profile`, res, req, { - user, - isSelf, - userMood, - userMoodFeed, - isFollowing + if (!isSelf) { + userMood.mood = moods[userMood.mood as number]; + } + + render(db, "user", `${req.params.user}'s profile`, res, req, { + user, + isSelf, + userMood, + userMoodFeed, + userJournalEntries, + isFollowing + }); }); - }); - app.post("/users/:user/edit", async (req, res) => { - if (!req.session["loggedIn"]) { - res.redirect("/login"); - return; - } - const { uname } = ( - await db - .select({ uname: users.name }) - .from(users) - .where(eq(users.name, req.params.user)) - )[0]; - if ((uname || "") !== req.session["user"] && !(req.session["status"] & UserStatus.MODERATOR)) { - res.redirect("back"); - return; - } + app.post("/users/:user/edit", async (req, res) => { + if (!req.session["loggedIn"]) { + res.redirect("/login"); + return; + } + const { uname } = ( + await db + .select({ uname: users.name }) + .from(users) + .where(eq(users.name, req.params.user)) + )[0]; + if ( + (uname || "") !== req.session["user"] && + !(req.session["status"] & UserStatus.MODERATOR) + ) { + res.redirect("back"); + return; + } - await db //! no sanitization here either BROOOOOOO - .update(profiles) - .set({ - // @ts-expect-error - bio: req.body.bio, - website: req.body.website - }) - .where(eq(profiles.user, req.session["uid"])); - req.flash("success", "Profile updated!"); - res.redirect("/dashboard"); - }); - app.post("/users/:user/follow", async (req, res) => { - if (!req.session["loggedIn"]) { - res.redirect("/login"); - return; - } - if (req.session["user"] === req.params.user) { - req.flash("error", "Can't Follow Yourself Dummy"); - res.redirect(`/users/${req.params.user}`); - return; - } + await db //! no sanitization here either BROOOOOOO + .update(profiles) + .set({ + // @ts-expect-error + bio: req.body.bio, + website: req.body.website + }) + .where(eq(profiles.user, req.session["uid"])); + req.flash("success", "Profile updated!"); + res.redirect("/dashboard"); + }); + app.post("/users/:user/follow", async (req, res) => { + if (!req.session["loggedIn"]) { + res.redirect("/login"); + return; + } + if (req.session["user"] === req.params.user) { + req.flash("error", "Can't Follow Yourself Dummy"); + res.redirect(`/users/${req.params.user}`); + return; + } - const { uid } = ( - await db - .select({ uid: users.id }) - .from(users) - .where(eq(users.name, req.params.user)) - )[0]; - if (!uid) { - req.flash( - "error", - "It looks like you're trying to follow a user that doesn't exist anymore." - ); - res.redirect("/"); - return; - } - const isFollowing = !!( - await db - .select() - .from(follows) - .where( - and( - eq(follows.followerId, req.session["uid"]), - eq(follows.userId, uid) - ) - ) - .limit(1) - )[0]; - if (isFollowing) { - // unfollow - await db - .delete(follows) - .where( - and( - eq(follows.followerId, req.session["uid"]), - eq(follows.userId, uid) - ) - ); - } else { - await db.insert(follows).values({ - userId: uid, - followerId: req.session["uid"] - }); - } - res.redirect(`/users/${req.params.user}`); - }); + const { uid } = ( + await db + .select({ uid: users.id }) + .from(users) + .where(eq(users.name, req.params.user)) + )[0]; + if (!uid) { + req.flash( + "error", + "It looks like you're trying to follow a user that doesn't exist anymore." + ); + res.redirect("/"); + return; + } + const isFollowing = !!( + await db + .select() + .from(follows) + .where( + and( + eq(follows.followerId, req.session["uid"]), + eq(follows.userId, uid) + ) + ) + .limit(1) + )[0]; + if (isFollowing) { + // unfollow + await db + .delete(follows) + .where( + and( + eq(follows.followerId, req.session["uid"]), + eq(follows.userId, uid) + ) + ); + } else { + await db.insert(follows).values({ + userId: uid, + followerId: req.session["uid"] + }); + } + res.redirect(`/users/${req.params.user}`); + }); } diff --git a/routes/util.ts b/routes/util.ts index 910d062..16c2432 100644 --- a/routes/util.ts +++ b/routes/util.ts @@ -5,92 +5,113 @@ import { count, desc, eq } from "drizzle-orm"; import fs from "node:fs/promises"; export enum UserStatus { - MODERATOR = 0b001, - BANNED = 0b010, - TRUSTED = 0b100 -}; + MODERATOR = 0b001, + BANNED = 0b010, + TRUSTED = 0b100 +} const nonceChars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"; + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"; let nonce: string; export function setNonce() { - nonce = ""; - for (let i = 0; i < 32; i++) - nonce += nonceChars[Math.floor(Math.random() * nonceChars.length)]; - return nonce; + nonce = ""; + for (let i = 0; i < 32; i++) + nonce += nonceChars[Math.floor(Math.random() * nonceChars.length)]; + return nonce; } export function getNonce() { - if (!nonce) - throw new Error("Nonce doesn't exist"); - return nonce; + if (!nonce) throw new Error("Nonce doesn't exist"); + return nonce; } let moods: string[], moodsSorted: string[]; export async function getMoods() { - if (!moods) - moods = (await fs.readFile("./static/moods.txt")) - .toString("utf-8") - .split(";"); - if (!moodsSorted) moodsSorted = Array.from(moods).sort(); - return { moods, moodsSorted }; + if (!moods) + moods = (await fs.readFile("./static/moods.txt")) + .toString("utf-8") + .split(";"); + if (!moodsSorted) moodsSorted = Array.from(moods).sort(); + return { moods, moodsSorted }; } export async function render( - db: NodePgDatabase, - page: string, - title: string, - res: Response, - req: Request, - stuff?: Object + db: NodePgDatabase, + page: string, + title: string, + res: Response, + req: Request, + stuff?: Object ) { - //? maybe you should cache this and save the current mood to the session until it's changed - const { moods } = await getMoods(); - let currentMood: string; - if (req.session["loggedIn"]) { - const update = ( - await db - .select({ mood: updates.mood }) - .from(updates) - .where(eq(updates.user, req.session["uid"])) - .orderBy(desc(updates.date)) - .limit(1) - )[0]; - currentMood = moods[update?.mood]; - } - - const o = { - title, - session: req.session, - flashes: req.flash(), - moods, - currentMood, - nonce - }; - res.render(page, { ...o, ...stuff }); -} - -const inviteCodeChars = "abcdefghijklmnopqrstuvwxyz0123456789" -export async function createInviteCode(db: NodePgDatabase, user: number, expires: Date, confers = 0) { - let existingToken = 1, token: string; - while (existingToken) { - token = user.toString().padStart(4, "0") + "-" - for (let i = 0; i < 17; i++) { - if ((i + 1) % 6 === 0) { - token += "-"; - continue; - } - token += inviteCodeChars[Math.floor(Math.random() * inviteCodeChars.length)]; + //? maybe you should cache this and save the current mood to the session until it's changed + const { moods } = await getMoods(); + let currentMood: string; + if (req.session["loggedIn"]) { + const update = ( + await db + .select({ mood: updates.mood }) + .from(updates) + .where(eq(updates.user, req.session["uid"])) + .orderBy(desc(updates.date)) + .limit(1) + )[0]; + currentMood = moods[update?.mood]; } - existingToken = (await db.select({ value: count() }).from(inviteCodes).where(eq(inviteCodes.token, token)))[0].value; - } - //@ts-expect-error - await db.insert(inviteCodes).values({ - token, - user: user || undefined, - granted: new Date(Date.now()), - expires, - confers - }); - return token; + const o = { + title, + session: req.session, + flashes: req.flash(), + moods, + currentMood, + nonce + }; + res.render(page, { ...o, ...stuff }); +} +export async function render404( + db: NodePgDatabase, + res: Response, + req: Request +) { + res.statusCode = 404; + render(db, "404", "not found", res, req); +} + +const inviteCodeChars = "abcdefghijklmnopqrstuvwxyz0123456789"; +export async function createInviteCode( + db: NodePgDatabase, + user: number, + expires: Date, + confers = 0 +) { + let existingToken = 1, + token: string; + while (existingToken) { + token = user.toString().padStart(4, "0") + "-"; + for (let i = 0; i < 17; i++) { + if ((i + 1) % 6 === 0) { + token += "-"; + continue; + } + token += + inviteCodeChars[ + Math.floor(Math.random() * inviteCodeChars.length) + ]; + } + existingToken = ( + await db + .select({ value: count() }) + .from(inviteCodes) + .where(eq(inviteCodes.token, token)) + )[0].value; + } + + //@ts-expect-error + await db.insert(inviteCodes).values({ + token, + user: user || undefined, + granted: new Date(Date.now()), + expires, + confers + }); + return token; } diff --git a/static/css/main.css b/static/css/main.css index 781a589..5f1a3c1 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -8,6 +8,8 @@ body { justify-content: center; margin: 0; background-color: #21211d; + background-image: url("/img/bg.png"); + background-size: cover; width: 100vw; height: 100vh; font-family: "Gohufont 14", sans-serif; @@ -17,6 +19,7 @@ body { main { background-color: #faf8da; width: 95%; + max-width: 953px; height: 95%; } diff --git a/static/img/bg.png b/static/img/bg.png new file mode 100644 index 0000000..4cf4230 Binary files /dev/null and b/static/img/bg.png differ diff --git a/views/404.pug b/views/404.pug new file mode 100644 index 0000000..9f92e7f --- /dev/null +++ b/views/404.pug @@ -0,0 +1,5 @@ +extends site.pug + +block content + h1 Not Found + p No such thing is to be found here!! Perhaps, you would be better off, locating something that exists. diff --git a/views/_feed.pug b/views/_feed.pug index 6ee981e..46c0b63 100644 --- a/views/_feed.pug +++ b/views/_feed.pug @@ -9,6 +9,6 @@ mixin feed(feed, hideUser) | #{update.user} strong= update.mood div= update.desc || "[no mood description provided]" - div= update.date + div(title=update.date.toLocaleString())= update.relativeDate else span [no updates] diff --git a/views/journal.pug b/views/journal.pug index ab29489..62d2f5c 100644 --- a/views/journal.pug +++ b/views/journal.pug @@ -30,6 +30,9 @@ block content input.ovm-input(type="radio", name="moodDelta", id="moodDelta-mw", value="-2", required) label.ovm-input(for="moodDelta-mw", title="Much worse") img(src="/img/downdown.svg", alt="Much worse") + .input + label(for="title") Title + input(type="text", name="title", id="title", placeholder="max 64 chars", maxlength="64", style="width:66ch;") .input label(for="description") Journal entry for today textarea(name="description", id="description", placeholder="max 4096 chars", maxlength="4096", cols="60", rows="12") diff --git a/views/journal_view.pug b/views/journal_view.pug index e759ed9..2cfecad 100644 --- a/views/journal_view.pug +++ b/views/journal_view.pug @@ -1,13 +1,12 @@ extends site.pug block content - h1= entry.date.toLocaleDateString() + h1= entry.title span - | by - | + | by a(href=`/users/${entry.uname}`)= entry.uname - | - | at #{entry.date.toLocaleTimeString()} + | + span(title=entry.date.toLocaleString()) #{entryTimestamp} br br - div= entry.content + div!= entry.content diff --git a/views/site.pug b/views/site.pug index 46a2bf2..3a58ab7 100644 --- a/views/site.pug +++ b/views/site.pug @@ -11,7 +11,7 @@ html(lang="en") main header a(href="/") - img#header-logo(src="/img/logo.svg", alt="logo") + img#header-logo(src="/img/logo.svg", alt="logo", title="meow") nav if session.loggedIn if session.status & 0b001 diff --git a/views/user.pug b/views/user.pug index e515e48..4b2d390 100644 --- a/views/user.pug +++ b/views/user.pug @@ -2,20 +2,21 @@ extends site.pug //- Display mood here but keep yourself dry (i.e. dont request user mood if the profile being viewed is the profile of the currently logged-in user) block content - h1= user.name - if !isSelf - form(action=`/users/${user.name}/follow`, method="post") - button(type="submit")= isFollowing ? "unfollow" : "follow" - br - span= user.bio || "[no bio]" + div(style="display:flex;align-items:center;gap:8px;") + h1= user.name + if !isSelf + form(action=`/users/${user.name}/follow`, method="post") + button(type="submit")= isFollowing ? "unfollow" : "follow" + br + div(style="margin-bottom:1em;") + | Joined + span(title=user.registered.toLocaleString())= user.relativeRegistered + div= user.bio || "[no bio]" if user.website - br a(href=user.website)= user.website - br - br - h2 Current mood + h2(style="margin-top:1em;") Current mood if userMood - span + div | Feeling | strong @@ -23,11 +24,20 @@ block content | #{currentMood} else | #{userMood.mood} - br div(style="margin-left:2ch;word-wrap:break-word;")= userMood.desc || "[no mood description]" else - span User has not yet set a mood! + div User has not yet set a mood! br - h2 Recent updates + h2 Recent journal entries + if userJournalEntries.length > 0 + ul + for entry of userJournalEntries + li + a(href=`/journal/${entry.id}`)= entry.title + span.subtle(title=entry.date.toLocaleString()) (#{entry.relativeDate}) + else + div [no entries] + br + h2 Recent mood updates include _feed.pug +feed(userMoodFeed, true)