1
0
Fork 0
mirror of https://git.sr.ht/~roxwize/mipilin synced 2025-01-31 02:53:36 +00:00

i dont really know what i added

Signed-off-by: roxwize <rae@roxwize.xyz>
This commit is contained in:
Rae 5e 2024-11-17 14:16:27 -05:00
parent e3c09d7f0d
commit 7b563f5c31
Signed by: rae
GPG key ID: 5B1A0FAB9BAB81EE
24 changed files with 797 additions and 151 deletions

View file

@ -27,6 +27,15 @@ export const updates = pgTable("updates", {
date: timestamp().notNull() date: timestamp().notNull()
}); });
export const journalEntries = pgTable("journal_entries", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
user: integer().references(() => users.id, { onDelete: "cascade" }).notNull(),
moodChange: integer("mood-change").default(0).notNull(),
entry: varchar({ length: 4096 }).default("").notNull(),
visibility: integer().default(1).notNull(),
date: timestamp().notNull()
});
export const profiles = pgTable("profiles", { export const profiles = pgTable("profiles", {
user: integer() user: integer()
.references(() => users.id, { onDelete: "cascade" }) .references(() => users.id, { onDelete: "cascade" })

View file

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS "journal_entries" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "journal_entries_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"user" integer NOT NULL,
"mood-change" integer DEFAULT 0 NOT NULL,
"entry" varchar(4096) DEFAULT '' NOT NULL,
"visibility" integer DEFAULT 1 NOT NULL,
"date" timestamp NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "journal_entries" ADD CONSTRAINT "journal_entries_user_users_id_fk" FOREIGN KEY ("user") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,356 @@
{
"id": "73a239ee-a945-48e0-a021-d415815a830b",
"prevId": "f8ae31ec-68e9-4857-93b2-abd7f834e580",
"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.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
},
"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
},
"moderator": {
"name": "moderator",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"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": {}
}
}

View file

@ -43,6 +43,13 @@
"when": 1731393095652, "when": 1731393095652,
"tag": "0005_magical_swarm", "tag": "0005_magical_swarm",
"breakpoints": true "breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1731868543611,
"tag": "0006_lame_grey_gargoyle",
"breakpoints": true
} }
] ]
} }

133
main.ts
View file

@ -1,5 +1,5 @@
//! TODO: There is like no error checking in queries at all //! TODO: There is like no error checking in queries at all
import type { Response, Request } from "express"; //! TODO: Also no API? seriously? also when a form errors it just redirects with a 301 like everything is okay when it ISNT and it NEVER HAS BEEN
import "dotenv/config"; import "dotenv/config";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -12,19 +12,20 @@ import connectPgSimple from "connect-pg-simple";
import flash from "connect-flash"; import flash from "connect-flash";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { follows, profiles, updates, users } from "./db/schema.js"; import { updates, users } from "./db/schema.js";
import { and, desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
// routes // routes
import loginRoutes from "./routes/login.js"; import loginRoutes from "./routes/login.js";
import userRoutes from "./routes/users.js"; import userRoutes from "./routes/users.js";
import { getMoods, render } from "./routes/util.js"; import updateRoutes from "./routes/updates.js";
import { getMoods, render, setNonce } from "./routes/util.js";
const db = drizzle(process.env.DATABASE_URL!); const db = drizzle(process.env.DATABASE_URL!);
//! TODO: Make sure SQL queries arent being repeated too much //! TODO: Make sure SQL queries arent being repeated too much
(async () => { (async () => {
const { moods, moodsSorted } = await getMoods(); const { moods } = await getMoods();
// setup dayjs // setup dayjs
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -43,6 +44,19 @@ const db = drizzle(process.env.DATABASE_URL!);
}) })
); );
app.use(flash()); app.use(flash());
//== Content Security Policy
app.use((_, res, next) => {
res.setHeader("X-Powered-By", "Lots and lots of milk");
res.setHeader(
"Content-Security-Policy",
"\
script-src 'nonce-" +
setNonce() +
"';\
object-src 'none'; base-uri 'none';"
);
return next();
});
app.get("/", async (req, res) => { app.get("/", async (req, res) => {
const upd = db const upd = db
@ -57,121 +71,38 @@ const db = drizzle(process.env.DATABASE_URL!);
.as("upd"); .as("upd");
const recentUpdates = await db.select().from(upd).orderBy(desc(upd.date)); const recentUpdates = await db.select().from(upd).orderBy(desc(upd.date));
render(db, "index", "Home", res, req, { const date = dayjs();
users: (await db.select().from(users)).length, const feedUpdates = (
recentUpdates
});
});
await userRoutes(app, db);
//! -- TEMP DASHBOARD START --
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 await db
.select({ .select({
user: users.name, user: users.name,
mood: updates.mood, mood: updates.mood,
desc: updates.description, date: updates.date,
date: updates.date desc: updates.description
}) })
.from(updates) .from(updates)
.innerJoin( .innerJoin(users, eq(updates.user, users.id))
follows, .limit(50)
and(
eq(follows.userId, updates.user),
eq(follows.followerId, req.session["uid"])
)
)
.leftJoin(users, eq(updates.user, users.id))
.orderBy(desc(updates.date)) .orderBy(desc(updates.date))
.limit(25)
).map((e) => { ).map((e) => {
return { return {
user: e.user, user: e.user,
mood: moods[e.mood], mood: moods[e.mood],
desc: e.desc, date: date.to(dayjs(e.date)),
date: dayjs().to(dayjs(e.date)) desc: e.desc
}; };
}); });
render(db, "dashboard", "Dashboard", res, req, { render(db, "index", "Home", res, req, {
user, users: (await db.select().from(users)).length,
moods,
moodsSorted,
moodHistory,
recentUpdates, recentUpdates,
feed: [] feedUpdates
}); });
}); });
app.post("/update", 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 await userRoutes(app, db);
.insert(updates) await updateRoutes(app, db);
// @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");
});
//! -- TEMP DASHBOARD END --
//! TEMP Also not sanitized like at all
//! Also make sure user isnt logged in before doing this
loginRoutes(app, db); loginRoutes(app, db);
//! TEMP done
app.listen(1337, () => { app.listen(1337, () => {
console.log("Listening on http://127.0.0.1:1337/"); console.log("Listening on http://127.0.0.1:1337/");

View file

@ -5,6 +5,8 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
import { profiles, users } from "../db/schema.js"; import { profiles, users } from "../db/schema.js";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
//! TEMP Also not sanitized like at all
//! Also make sure user isnt logged in before doing this
export default function(app: Express, db: NodePgDatabase) { export default function(app: Express, db: NodePgDatabase) {
app.get("/register", (req, res) => { app.get("/register", (req, res) => {
if (req.session["loggedIn"]) { if (req.session["loggedIn"]) {

161
routes/updates.ts Normal file
View file

@ -0,0 +1,161 @@
import { NodePgDatabase } from "drizzle-orm/node-postgres";
import { Express } from "express";
import {
follows,
journalEntries,
profiles,
updates,
users
} from "../db/schema.js";
import { and, desc, eq } from "drizzle-orm";
import dayjs from "dayjs";
import { getMoods, render } from "./util.js";
export default async function (app: Express, db: NodePgDatabase) {
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))
};
});
render(db, "dashboard", "Dashboard", res, req, {
user,
moods,
moodsSorted,
moodHistory,
recentUpdates,
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");
}
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.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;
}
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 {
// @ts-expect-error
const entry = await db.insert(journalEntries).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:<br><br>"+err);
res.redirect("/journal");
return;
}
req.flash("success", `Your journal entry is now available as <a href="/journal/view?id=${id}">#${id}</a>!`);
res.redirect("/journal");
});
}

View file

@ -4,6 +4,7 @@ import { follows, profiles, updates, users } from "../db/schema.js";
import { and, desc, eq } from "drizzle-orm"; import { and, desc, eq } from "drizzle-orm";
import { getMoods, render } from "./util.js"; import { getMoods, render } from "./util.js";
import { PgColumn } from "drizzle-orm/pg-core"; import { PgColumn } from "drizzle-orm/pg-core";
import dayjs from "dayjs";
export default async function (app: Express, db: NodePgDatabase) { export default async function (app: Express, db: NodePgDatabase) {
const { moods } = await getMoods(); const { moods } = await getMoods();
@ -57,6 +58,19 @@ export default async function (app: Express, db: NodePgDatabase) {
.limit(1) .limit(1)
)[0]; )[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 }
});
if (!isSelf) { if (!isSelf) {
userMood.mood = moods[userMood.mood as number]; userMood.mood = moods[userMood.mood as number];
} }
@ -65,6 +79,7 @@ export default async function (app: Express, db: NodePgDatabase) {
user, user,
isSelf, isSelf,
userMood, userMood,
userMoodFeed,
isFollowing isFollowing
}); });
}); });

View file

@ -4,6 +4,21 @@ import { updates } from "../db/schema.js";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
const nonceChars =
"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;
}
export function getNonce() {
if (!nonce)
throw new Error("Nonce doesn't exist");
return nonce;
}
let moods: string[], moodsSorted: string[]; let moods: string[], moodsSorted: string[];
export async function getMoods() { export async function getMoods() {
if (!moods) if (!moods)
@ -42,7 +57,8 @@ export async function render(
session: req.session, session: req.session,
flashes: req.flash(), flashes: req.flash(),
moods, moods,
currentMood currentMood,
nonce
}; };
res.render(page, { ...o, ...stuff }); res.render(page, { ...o, ...stuff });
} }

View file

@ -9,21 +9,6 @@ form *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.feed-update {
background-color: #d5d4bb;
height: fit-content;
word-wrap: break-word;
}
.feed-update div:first-child {
display: flex;
justify-content: space-between;
}
.feed-update div:last-child {
text-align: right;
color: #3f3f38;
font-style: italic;
}
#dashboard-update-form * { #dashboard-update-form * {
width: 50%; width: 50%;
min-width: 128px; min-width: 128px;
@ -43,20 +28,51 @@ form *:last-child {
justify-content: space-between; justify-content: space-between;
} }
@media screen and (min-width: 900px) { #journal-update {
#dashboard-feed { max-width: 880px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.feed-update {
padding: 4px;
width: 280px;
}
} }
@media screen and (max-width: 900px) {
.feed-update { #journal-update .input {
border-bottom: 1px solid #afa870; display: flex;
padding: 8px; align-items: center;
} justify-content: space-between;
}
#journal-update .input span {
cursor: default;
}
#ovm {
display: flex;
}
#visibility-control * {
display: inline;
margin: 4px;
}
input.ovm-input {
opacity: 0;
width: 0;
}
input.ovm-input:first-child {
position: absolute;
width: 1px;
}
label.ovm-input > img {
border-width: 1px 0 1px 1px;
border-style: solid;
border-color: #afa870;
background-color: #d5d4bb;
cursor: pointer;
padding: 8px;
width: 64px;
aspect-ratio: 1/1;
}
input.ovm-input:checked+label > img {
background-color: #999883;
filter: invert(1);
}
label.ovm-input:last-child > img {
border-right-width: 1px;
} }

View file

@ -169,8 +169,42 @@ select {
cursor: pointer; cursor: pointer;
} }
.feed-update {
background-color: #d5d4bb;
height: fit-content;
word-wrap: break-word;
}
.feed-update div:first-child {
display: flex;
justify-content: space-between;
}
.feed-update div:last-child {
text-align: right;
color: #3f3f38;
font-style: italic;
}
@font-face { @font-face {
font-family: "Gohufont 14"; font-family: "Gohufont 14";
src: url("/fonts/gohufont-14.ttf") format("truetype"), src: url("/fonts/gohufont-14.ttf") format("truetype"),
url("/fonts/gohufont-14.woff") format("woff"); url("/fonts/gohufont-14.woff") format("woff");
font-display: swap;
}
@media screen and (min-width: 900px) {
#feed {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.feed-update {
padding: 4px;
width: 280px;
}
}
@media screen and (max-width: 900px) {
.feed-update {
border-bottom: 1px solid #afa870;
padding: 8px;
}
} }

3
static/img/down.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 6">
<path d="M1-6v2H3L0 0-3-4h2V-6Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 119 B

3
static/img/downdown.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 7.5">
<path d="M-3-2.5h.75L0 .5l2.25-3H3l-3 4ZM1-6v2H3L0 0-3-4h2V-6Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

3
static/img/line.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -0.5 6 1">
<path d="M-3-.5H3v1H-3Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 113 B

View file

@ -1 +1,5 @@
<svg width="88.8" height="41.8" style="fill:none" viewBox="0 0 444 209" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;stroke:#faf8da;stroke-width:24;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" d="M342 192v0l-90-90V42l30-30h30l30 30 30-30h30l30 30v60l-90 90"/><path style="fill:none;stroke:#faf8da;stroke-width:24.2904;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:1" d="M12 197v0V12h180M12 102v0h150H12m180 0v0-90m0 90v0h-30"/></svg> <svg width="88.8" height="41.8" style="fill:none" viewBox="0 0 444 209"
xmlns="http://www.w3.org/2000/svg">
<path style="fill:none;stroke:#faf8da;stroke-width:24;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" d="M342 192v0l-90-90V42l30-30h30l30 30 30-30h30l30 30v60l-90 90"/>
<path style="fill:none;stroke:#faf8da;stroke-width:24.2904;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:1" d="M12 197v0V12h180M12 102v0h150H12m180 0v0-90m0 90v0h-30"/>
</svg>

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 486 B

3
static/img/up.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 6">
<path d="M1 0V-2H3L0-6-3-2h2V0Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 119 B

3
static/img/upup.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -7.5 6 7.5">
<path d="M1 0V-2H3L0-6-3-2h2V0ZM0-7.5l-3 4h.75L0-6.5l2.25 3H3Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 154 B

File diff suppressed because one or more lines are too long

14
views/_feed.pug Normal file
View file

@ -0,0 +1,14 @@
mixin feed(feed, hideUser)
if feed.length > 0
#feed
for update of feed
.feed-update
div
a(href=`/users/${update.user}`)
if !hideUser
| #{update.user}
strong= update.mood
div= update.desc || "[no mood description provided]"
div= update.date
else
span [no updates]

View file

@ -25,8 +25,8 @@ block content
details(style="margin-bottom:1em;") details(style="margin-bottom:1em;")
summary Moderation summary Moderation
p If you're seeing this, you are part of the exclusive club of moderators! You are now entitled to pizza for the rest of your life! And the ability to ban people and create new moods! For FREE! p If you're seeing this, you are part of the exclusive club of moderators! You are now entitled to pizza for the rest of your life! And the ability to ban people and create new moods! For FREE!
form#dashboard-update-form(action="/update", method="post", onsubmit="disable(this);") form#dashboard-update-form(action="/update/mood", method="post", onsubmit="disable(this);")
select(name="mood") select(name="mood", required)
//- Maybe put the index of the mood in the value of the option element //- Maybe put the index of the mood in the value of the option element
//- so that indexOf (slow) wont have to be used everytime mood is updated //- so that indexOf (slow) wont have to be used everytime mood is updated
for mood of moodsSorted for mood of moodsSorted
@ -34,17 +34,8 @@ block content
textarea(name="desc", placeholder="mood description (max 512 chars)", rows="5", maxlength="512") textarea(name="desc", placeholder="mood description (max 512 chars)", rows="5", maxlength="512")
button(type="submit") Update button(type="submit") Update
h1(style="margin-top:0.5em;") Feed h1(style="margin-top:0.5em;") Feed
if recentUpdates.length > 0 include _feed.pug
#dashboard-feed +feed(recentUpdates)
for update of recentUpdates
.feed-update
div
a(href=`/users/${update.user}`)= update.user
strong= update.mood
div= update.desc || "[no mood description provided]"
div= update.date
else
span [no updates]
script. script.
function disable(form) { function disable(form) {
const btn = form.querySelector("button"); const btn = form.querySelector("button");

View file

@ -15,6 +15,12 @@ block page
| |
a(href="/dashboard") the dashboard a(href="/dashboard") the dashboard
| ! | !
if session.loggedIn
p
| You can record your daily thoughts and feelings, public or private, at your
|
a(href="/journal/") personal journal
| .
p p
strong #{users} strong #{users}
| |
@ -28,3 +34,7 @@ block page
block content block content
p p
| Hi, this is MiPilin! It lets you tell your friends how you're feeling, as well as keep a journal of your current mood and their trends over time! Due respect goes to imood, from which I borrowed many ideas and basically all of the moods. | Hi, this is MiPilin! It lets you tell your friends how you're feeling, as well as keep a journal of your current mood and their trends over time! Due respect goes to imood, from which I borrowed many ideas and basically all of the moods.
h1 Global Feed
p Look at how all these people are doing!!!
include _feed.pug
+feed(feedUpdates)

48
views/journal.pug Normal file
View file

@ -0,0 +1,48 @@
extends site.pug
block head
link(rel="stylesheet", href="/css/dashboard.css")
script(type="module", src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js", nonce=nonce)
block content
h1 Your Journal
p This is where you can log your overall mood every day, and get a glimpse at how your life is going so far!
form#journal-update(action="/update/journal", method="post")
.input
span Overall mood change (how do you feel compared to yesterday?)
#ovm
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-mb", value="2", required)
label.ovm-input(for="moodDelta-mb", title="Much better")
img(src="/img/upup.svg", alt="Much better")
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-b", value="1", required)
label.ovm-input(for="moodDelta-b", title="Better")
img(src="/img/up.svg", alt="Better")
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-nc", value="0", required checked)
label.ovm-input(for="moodDelta-nc", title="About the same")
img(src="/img/line.svg", alt="About the same")
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-w", value="-1", required)
label.ovm-input(for="moodDelta-w", title="Worse")
img(src="/img/down.svg", alt="Worse")
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="description") Journal entry for today
textarea(name="description", id="description", placeholder="max 4096 chars", maxlength="4096", cols="60", rows="12")
.input
span Visibility
div#visibility-control
input(type="radio", name="visibility", id="visibility-public", value="1", checked)
label(for="visibility-public") Public
br
input(type="radio", name="visibility", id="visibility-private", value="0")
label(for="visibility-private") Private
br
input(type="radio", name="visibility", id="visibility-moodChange-only", value="2")
label(for="visibility-moodChange-only") Mood only
button(type="submit") Submit

View file

@ -3,6 +3,7 @@ html(lang="en")
head head
meta(charset="UTF-8") meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(name="description", content="Show your mood to the world!")
link(rel="stylesheet", href="/css/main.css") link(rel="stylesheet", href="/css/main.css")
block head block head
title #{title} > mipilin title #{title} > mipilin
@ -14,10 +15,9 @@ html(lang="en")
nav nav
if session.loggedIn if session.loggedIn
a(href="/dashboard/") Dashboard a(href="/dashboard/") Dashboard
a(href="/feed/") Feed a(href="/journal/") Journal
a(href="/logout/") Log out a(href="/logout/") Log out
else else
a(href="/feed/") Feed
a(href="/register/") Sign up a(href="/register/") Sign up
a(href="/login") Log in a(href="/login") Log in
#ticker #ticker
@ -41,6 +41,6 @@ html(lang="en")
#flashes #flashes
for [type, flashGroup] of Object.entries(flashes) for [type, flashGroup] of Object.entries(flashes)
for flash of flashGroup for flash of flashGroup
div(class=`flash-${type}`)= flash div(class=`flash-${type}`)!= flash
block content block content

View file

@ -27,4 +27,7 @@ block content
div(style="margin-left:2ch;word-wrap:break-word;")= userMood.desc || "[no mood description]" div(style="margin-left:2ch;word-wrap:break-word;")= userMood.desc || "[no mood description]"
else else
span User has not yet set a mood! span User has not yet set a mood!
br
h2 Recent updates
include _feed.pug
+feed(userMoodFeed, true)