1
0
Fork 0
mirror of https://git.sr.ht/~roxwize/mipilin synced 2025-01-30 18: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()
});
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", {
user: integer()
.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,
"tag": "0005_magical_swarm",
"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
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 dayjs from "dayjs";
@ -12,19 +12,20 @@ import connectPgSimple from "connect-pg-simple";
import flash from "connect-flash";
import { drizzle } from "drizzle-orm/node-postgres";
import { follows, profiles, updates, users } from "./db/schema.js";
import { and, desc, eq } from "drizzle-orm";
import { updates, users } from "./db/schema.js";
import { desc, eq } from "drizzle-orm";
// routes
import loginRoutes from "./routes/login.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!);
//! TODO: Make sure SQL queries arent being repeated too much
(async () => {
const { moods, moodsSorted } = await getMoods();
const { moods } = await getMoods();
// setup dayjs
dayjs.extend(relativeTime);
@ -43,6 +44,19 @@ const db = drizzle(process.env.DATABASE_URL!);
})
);
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) => {
const upd = db
@ -57,121 +71,38 @@ const db = drizzle(process.env.DATABASE_URL!);
.as("upd");
const recentUpdates = await db.select().from(upd).orderBy(desc(upd.date));
render(db, "index", "Home", res, req, {
users: (await db.select().from(users)).length,
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 = (
const date = dayjs();
const feedUpdates = (
await db
.select({
user: users.name,
mood: updates.mood,
desc: updates.description,
date: updates.date
date: updates.date,
desc: updates.description
})
.from(updates)
.innerJoin(
follows,
and(
eq(follows.userId, updates.user),
eq(follows.followerId, req.session["uid"])
)
)
.leftJoin(users, eq(updates.user, users.id))
.innerJoin(users, eq(updates.user, users.id))
.limit(50)
.orderBy(desc(updates.date))
.limit(25)
).map((e) => {
return {
user: e.user,
mood: moods[e.mood],
desc: e.desc,
date: dayjs().to(dayjs(e.date))
date: date.to(dayjs(e.date)),
desc: e.desc
};
});
render(db, "dashboard", "Dashboard", res, req, {
user,
moods,
moodsSorted,
moodHistory,
render(db, "index", "Home", res, req, {
users: (await db.select().from(users)).length,
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
.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");
});
//! -- TEMP DASHBOARD END --
//! TEMP Also not sanitized like at all
//! Also make sure user isnt logged in before doing this
await userRoutes(app, db);
await updateRoutes(app, db);
loginRoutes(app, db);
//! TEMP done
app.listen(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 { 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) {
app.get("/register", (req, res) => {
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 { getMoods, render } 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();
@ -57,6 +58,19 @@ export default async function (app: Express, db: NodePgDatabase) {
.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 }
});
if (!isSelf) {
userMood.mood = moods[userMood.mood as number];
}
@ -65,6 +79,7 @@ export default async function (app: Express, db: NodePgDatabase) {
user,
isSelf,
userMood,
userMoodFeed,
isFollowing
});
});

View file

@ -4,6 +4,21 @@ import { updates } from "../db/schema.js";
import { desc, eq } from "drizzle-orm";
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[];
export async function getMoods() {
if (!moods)
@ -42,7 +57,8 @@ export async function render(
session: req.session,
flashes: req.flash(),
moods,
currentMood
currentMood,
nonce
};
res.render(page, { ...o, ...stuff });
}

View file

@ -9,21 +9,6 @@ form *:last-child {
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 * {
width: 50%;
min-width: 128px;
@ -43,20 +28,51 @@ form *:last-child {
justify-content: space-between;
}
@media screen and (min-width: 900px) {
#dashboard-feed {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.feed-update {
padding: 4px;
width: 280px;
}
#journal-update {
max-width: 880px;
}
@media screen and (max-width: 900px) {
.feed-update {
border-bottom: 1px solid #afa870;
padding: 8px;
}
#journal-update .input {
display: flex;
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;
}
.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-family: "Gohufont 14";
src: url("/fonts/gohufont-14.ttf") format("truetype"),
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;")
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!
form#dashboard-update-form(action="/update", method="post", onsubmit="disable(this);")
select(name="mood")
form#dashboard-update-form(action="/update/mood", method="post", onsubmit="disable(this);")
select(name="mood", required)
//- 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
for mood of moodsSorted
@ -34,17 +34,8 @@ block content
textarea(name="desc", placeholder="mood description (max 512 chars)", rows="5", maxlength="512")
button(type="submit") Update
h1(style="margin-top:0.5em;") Feed
if recentUpdates.length > 0
#dashboard-feed
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]
include _feed.pug
+feed(recentUpdates)
script.
function disable(form) {
const btn = form.querySelector("button");

View file

@ -15,6 +15,12 @@ block page
|
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
strong #{users}
|
@ -28,3 +34,7 @@ block page
block content
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.
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
meta(charset="UTF-8")
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")
block head
title #{title} > mipilin
@ -14,10 +15,9 @@ html(lang="en")
nav
if session.loggedIn
a(href="/dashboard/") Dashboard
a(href="/feed/") Feed
a(href="/journal/") Journal
a(href="/logout/") Log out
else
a(href="/feed/") Feed
a(href="/register/") Sign up
a(href="/login") Log in
#ticker
@ -41,6 +41,6 @@ html(lang="en")
#flashes
for [type, flashGroup] of Object.entries(flashes)
for flash of flashGroup
div(class=`flash-${type}`)= flash
div(class=`flash-${type}`)!= flash
block content

View file

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