Messaging update (v0.2)
This commit is contained in:
parent
011e71e715
commit
e7f94445da
19 changed files with 752 additions and 10 deletions
|
@ -73,6 +73,14 @@ export default function Nav(elementProps: AnyObject) {
|
||||||
userCredentials.TROLLCALL_NAME != null &&
|
userCredentials.TROLLCALL_NAME != null &&
|
||||||
userCredentials.TROLLCALL_NAME != "" ? (
|
userCredentials.TROLLCALL_NAME != "" ? (
|
||||||
<>
|
<>
|
||||||
|
<span className={globals.icon}>
|
||||||
|
<Link
|
||||||
|
className={globals.link}
|
||||||
|
href="/add/message/"
|
||||||
|
>
|
||||||
|
send
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
<span className={globals.icon}>
|
<span className={globals.icon}>
|
||||||
<Link
|
<Link
|
||||||
href={`/clan/${userCredentials.TROLLCALL_NAME}`}
|
href={`/clan/${userCredentials.TROLLCALL_NAME}`}
|
||||||
|
|
53
src/components/cards/MessageCard/MessageCard.module.css
Normal file
53
src/components/cards/MessageCard/MessageCard.module.css
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
.MessageCard {
|
||||||
|
/* min-width: 600px; */
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard.Skeleton {
|
||||||
|
opacity: 0.25;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard .horizontal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
justify-content: start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard .horizontal .horizontalLeft {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 96px;
|
||||||
|
height: auto;
|
||||||
|
background-color: var(--pri-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard .horizontal .horizontalLeft img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard .horizontal .horizontalRight {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
/* overflow: hidden; */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageCard .horizontal .horizontalRight > * {
|
||||||
|
width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* overflow: hidden; */
|
||||||
|
}
|
80
src/components/cards/MessageCard/MessageCard.tsx
Normal file
80
src/components/cards/MessageCard/MessageCard.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Box from "@/components/Box/Box";
|
||||||
|
import globals from "@/styles/global.module.css";
|
||||||
|
import { ClientClan } from "@/types/clan";
|
||||||
|
import { ClientMessage } from "@/types/message";
|
||||||
|
import AuthContext from "@/utility/react/AuthContext";
|
||||||
|
import Conditional from "@/utility/react/Conditional";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import styles from "./MessageCard.module.css";
|
||||||
|
|
||||||
|
export default function MessageCard({
|
||||||
|
message,
|
||||||
|
recipient
|
||||||
|
}: {
|
||||||
|
message: ClientMessage;
|
||||||
|
recipient: ClientClan;
|
||||||
|
}) {
|
||||||
|
const userCredentials = useContext(AuthContext);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
properties={{
|
||||||
|
class: styles.MessageCard
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.horizontal}>
|
||||||
|
<Conditional condition={message.from.pfp != null}>
|
||||||
|
<div className={styles.horizontalLeft}>
|
||||||
|
<img
|
||||||
|
src={message.from.pfp as string}
|
||||||
|
width="96"
|
||||||
|
height="96"
|
||||||
|
alt=""
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
|
</Conditional>
|
||||||
|
<div className={styles.horizontalRight}>
|
||||||
|
<Link
|
||||||
|
href={`/clan/${message.from.name}`}
|
||||||
|
className={globals.link}
|
||||||
|
>
|
||||||
|
<p className={globals.title}>
|
||||||
|
{message.from.displayName ?? message.from.name}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
{/* <p className={globals.text}>
|
||||||
|
to {recipient.displayName ?? recipient.name}
|
||||||
|
</p> */}
|
||||||
|
<hr className={globals.sep} />
|
||||||
|
<Conditional condition={message.subject != ""}>
|
||||||
|
<p className={globals.titleSmall}>{message.subject}</p>
|
||||||
|
</Conditional>
|
||||||
|
<Conditional condition={message.body != ""}>
|
||||||
|
<p className={globals.text}>{message.body}</p>
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={globals.horizontalListLeft}>
|
||||||
|
<Conditional
|
||||||
|
condition={
|
||||||
|
message.from.name === userCredentials.TROLLCALL_NAME ||
|
||||||
|
recipient.name === userCredentials.TROLLCALL_NAME
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={globals.button}
|
||||||
|
onClick={() => {
|
||||||
|
fetch("/api/message/" + message._id, {
|
||||||
|
method: "DELETE"
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</Conditional>
|
||||||
|
<span className={globals.text}>{message._id}</span>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
29
src/components/cards/MessageCard/MessageSkeleton.tsx
Normal file
29
src/components/cards/MessageCard/MessageSkeleton.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Box from "@/components/Box/Box";
|
||||||
|
import globals from "@/styles/global.module.css";
|
||||||
|
import styles from "./MessageCard.module.css";
|
||||||
|
|
||||||
|
export default function MessageSkeleton() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
properties={{
|
||||||
|
class: styles.MessageCard + " " + styles.Skeleton
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.horizontal}>
|
||||||
|
<div className={styles.horizontalRight}>
|
||||||
|
<p className={globals.title}>Tim Sweeney</p>
|
||||||
|
{/* <p className={globals.text}>
|
||||||
|
to {recipient.displayName ?? recipient.name}
|
||||||
|
</p> */}
|
||||||
|
<hr className={globals.sep} />
|
||||||
|
<p className={globals.titleSmall}>12%</p>
|
||||||
|
<p className={globals.text}>
|
||||||
|
hey did you know we only take 12%? did you know? did
|
||||||
|
you? hey did you know we
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -101,8 +101,8 @@ export default function TrollCard({
|
||||||
<hr className={globals.invisep} />
|
<hr className={globals.invisep} />
|
||||||
<Conditional
|
<Conditional
|
||||||
condition={
|
condition={
|
||||||
troll.class != null &&
|
troll.class != null ||
|
||||||
troll.gender != null &&
|
troll.gender != null ||
|
||||||
troll.species != null
|
troll.species != null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
172
src/components/form/template/message.tsx
Normal file
172
src/components/form/template/message.tsx
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import Box from "@/components/Box/Box";
|
||||||
|
import ErrorHandler from "@/components/form/ErrorHandler/ErrorHandler";
|
||||||
|
import globals from "@/styles/global.module.css";
|
||||||
|
import form_globals from "@/styles/global_form.module.css";
|
||||||
|
import { SubmitMessage, SubmitMessageSchema } from "@/types/client/message";
|
||||||
|
import { ErrorMessage, Field, Form, Formik } from "formik";
|
||||||
|
import { NextRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function MessageFormTemplate({
|
||||||
|
router,
|
||||||
|
onSubmitURI,
|
||||||
|
on200URI,
|
||||||
|
initialValues,
|
||||||
|
method
|
||||||
|
}: {
|
||||||
|
router: NextRouter;
|
||||||
|
initialValues?: SubmitMessage;
|
||||||
|
onSubmitURI?: string;
|
||||||
|
on200URI?: string;
|
||||||
|
method?: string;
|
||||||
|
}) {
|
||||||
|
const [submitError, setSubmitError] = useState("");
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={
|
||||||
|
initialValues ??
|
||||||
|
({
|
||||||
|
to: "",
|
||||||
|
body: "",
|
||||||
|
subject: ""
|
||||||
|
} as SubmitMessage)
|
||||||
|
}
|
||||||
|
validationSchema={SubmitMessageSchema}
|
||||||
|
onSubmit={async (values, { setSubmitting, setErrors, setFieldError }) => {
|
||||||
|
fetch(onSubmitURI ?? "/api/message", {
|
||||||
|
method: method ?? "POST",
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) router.push(on200URI ?? `/clan/${values.to}`);
|
||||||
|
else setSubmitError(res.status.toString());
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ values, setFieldValue, errors, initialValues, isSubmitting, resetForm, submitForm }) => (
|
||||||
|
<Form className={form_globals.FlexNormalizer}>
|
||||||
|
<Box>
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<div className={form_globals.verticalListCrunch}>
|
||||||
|
<p className={globals.titleSmall}>Recipient</p>
|
||||||
|
<span className={globals.text}>
|
||||||
|
Who will recieve this message. Type the name of the user (the part you will find in their URL, not the display name.)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<div className={globals.horizontalListLeft}>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="to"
|
||||||
|
placeholder="jim"
|
||||||
|
className={`
|
||||||
|
${form_globals.textLikeInput}
|
||||||
|
${form_globals.textLikeInputTight}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage
|
||||||
|
name="to"
|
||||||
|
render={ErrorHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className={globals.invisep} />
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<div className={form_globals.verticalListCrunch}>
|
||||||
|
<p className={globals.titleSmall}>Subject</p>
|
||||||
|
<span className={globals.text}>What your message is about. Can be left blank.</span>
|
||||||
|
</div>
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<div className={globals.horizontalListLeft}>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="subject"
|
||||||
|
placeholder="Hello, world!"
|
||||||
|
className={`
|
||||||
|
${form_globals.textLikeInput}
|
||||||
|
${form_globals.textLikeInputTight}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage
|
||||||
|
name="subject"
|
||||||
|
render={ErrorHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className={globals.invisep} />
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<div className={form_globals.verticalListCrunch}>
|
||||||
|
<p className={globals.titleSmall}>Body</p>
|
||||||
|
<span className={globals.text}>The main text of your message.</span>
|
||||||
|
</div>
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<Field
|
||||||
|
as="textarea"
|
||||||
|
name="body"
|
||||||
|
placeholder="body"
|
||||||
|
className={`
|
||||||
|
${form_globals.textLikeInput}
|
||||||
|
${form_globals.textLikeInputMultiline}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`${globals.text} ${values.body.length > 10000 ? form_globals.render_error : ""} ${
|
||||||
|
values.body.length >= 9900 ? form_globals.render_warning : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{values.body.length <= 10000
|
||||||
|
? 10000 - values.body.length + " characters remaining"
|
||||||
|
: values.body.length - 10000 + " characters over limit"}
|
||||||
|
</span>
|
||||||
|
<ErrorMessage
|
||||||
|
name="body"
|
||||||
|
render={ErrorHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<Box properties={{ title: { text: "Form Settings" } }}>
|
||||||
|
<div className={globals.horizontalListLeft}>
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
className={globals.button}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
// onClick={() => resetForm()}
|
||||||
|
>
|
||||||
|
Reset Form
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={globals.button}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
// onClick={submitForm}
|
||||||
|
>
|
||||||
|
Submit Form
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={globals.horizontalListLeft}>{submitError != null && submitError.length > 0 ? ErrorHandler(submitError) : ""}</div>
|
||||||
|
<details>
|
||||||
|
<summary>Advanced stuff</summary>
|
||||||
|
<div className={globals.verticalListTop}>
|
||||||
|
<details>
|
||||||
|
<summary>Values (Advanced)</summary>
|
||||||
|
<p className={`${globals.mono} ${globals.blockTextKeepTabs}`}>{JSON.stringify(values, null, 2)}</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Errors (Advanced)</summary>
|
||||||
|
<p className={`${globals.mono} ${globals.blockTextKeepTabs}`}>{JSON.stringify(errors, null, 2)}</p>
|
||||||
|
</details>
|
||||||
|
<p className={globals.text}>If something is wrong, make sure to send these with your report!</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</Box>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ export async function updateOne(collection: string, find: any, update: any) {
|
||||||
|
|
||||||
export async function deleteOne(collection: string, find: any) {
|
export async function deleteOne(collection: string, find: any) {
|
||||||
const selectedCollection = mainDB.collection(collection);
|
const selectedCollection = mainDB.collection(collection);
|
||||||
return await selectedCollection.findOneAndDelete(find);
|
return await selectedCollection.deleteOne(find);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cursorToArray<T>(
|
export async function cursorToArray<T>(
|
||||||
|
|
33
src/lib/trollcall/api/message.ts
Normal file
33
src/lib/trollcall/api/message.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { ClientClan, ServerClan } from "@/types/clan";
|
||||||
|
import { ClientMessage } from "@/types/message";
|
||||||
|
import { getSingleClan } from "../clan";
|
||||||
|
import { ServerMessageToClientMessage } from "../convert/message";
|
||||||
|
import { getSingleMessage } from "../message";
|
||||||
|
import { ClanGET } from "./clan";
|
||||||
|
|
||||||
|
export async function MessageGET(
|
||||||
|
query: Partial<{ [key: string]: string | string[] }>,
|
||||||
|
recipientQuery?: string | null,
|
||||||
|
existingRecipient?: ServerClan
|
||||||
|
): Promise<ClientMessage | null> {
|
||||||
|
const to =
|
||||||
|
existingRecipient != null || query == null
|
||||||
|
? existingRecipient
|
||||||
|
: await getSingleClan({
|
||||||
|
name: recipientQuery
|
||||||
|
});
|
||||||
|
if (to == null) return null;
|
||||||
|
const message = await getSingleMessage({
|
||||||
|
...query,
|
||||||
|
"to": to._id
|
||||||
|
});
|
||||||
|
if (message == null) return null;
|
||||||
|
const from = await getSingleClan({
|
||||||
|
_id: message.from
|
||||||
|
});
|
||||||
|
if (from == null) return null;
|
||||||
|
const serverMessage = await ServerMessageToClientMessage(message);
|
||||||
|
// we know this is not null, as we passed in our own clan
|
||||||
|
serverMessage.from = (await ClanGET(null, from)) as ClientClan;
|
||||||
|
return serverMessage as ClientMessage;
|
||||||
|
}
|
26
src/lib/trollcall/convert/message.ts
Normal file
26
src/lib/trollcall/convert/message.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { SubmitMessage } from "@/types/client/message";
|
||||||
|
import { ClientMessage, ServerMessage } from "@/types/message";
|
||||||
|
|
||||||
|
export function ServerMessageToClientMessage(
|
||||||
|
serverMessage: ServerMessage
|
||||||
|
): Omit<ClientMessage, "from"> {
|
||||||
|
let clientMessage: Omit<ClientMessage, "from"> = {
|
||||||
|
_id: serverMessage._id.toString(),
|
||||||
|
date: serverMessage.date,
|
||||||
|
subject: serverMessage.subject,
|
||||||
|
body: serverMessage.body
|
||||||
|
};
|
||||||
|
|
||||||
|
return clientMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubmitMessageToServerMessage(
|
||||||
|
submitMessage: SubmitMessage
|
||||||
|
): Omit<Omit<Omit<ServerMessage, "_id">, "from">, "to"> {
|
||||||
|
let serverMessage: Omit<Omit<Omit<ServerMessage, "_id">, "from">, "to"> = {
|
||||||
|
date: new Date(Date.now()),
|
||||||
|
subject: submitMessage.subject,
|
||||||
|
body: submitMessage.body
|
||||||
|
};
|
||||||
|
return serverMessage;
|
||||||
|
}
|
89
src/lib/trollcall/message.ts
Normal file
89
src/lib/trollcall/message.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { ServerMessage } from "@/types/message";
|
||||||
|
import { Sort } from "mongodb";
|
||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
createOne,
|
||||||
|
cursorToArray,
|
||||||
|
deleteOne,
|
||||||
|
readMany,
|
||||||
|
readOne
|
||||||
|
} from "../db/crud";
|
||||||
|
|
||||||
|
const MessageSort: Sort = { _id: -1 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns one ServerMessages from the database.
|
||||||
|
* @param query A partial Find query. Can contain an ID.
|
||||||
|
* @returns A ServerMessage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getSingleMessage(
|
||||||
|
query: Filter<ServerMessage>
|
||||||
|
): Promise<ServerMessage | null> {
|
||||||
|
const message = (await readOne("messages", query)) as ServerMessage | null;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns many ServerMessages from the database using a FindCursor.
|
||||||
|
* @param query A partial Find query. Can contain an ID.
|
||||||
|
* @param func A function to run on every ServerMessages returned. Helps reduce loops.
|
||||||
|
* @returns An array of ServerMessages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getManyMessages<T>(
|
||||||
|
query: Filter<ServerMessage>,
|
||||||
|
func?: (input: any) => T
|
||||||
|
): Promise<(Awaited<T> | null)[]> {
|
||||||
|
const Message = (await cursorToArray(
|
||||||
|
readMany("messages", query, MessageSort),
|
||||||
|
func
|
||||||
|
)) as (Awaited<T> | null)[];
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns many ServerMessages from the database using a FindCursor, limited by count.
|
||||||
|
* @param query A partial Find query. Can contain an ID.
|
||||||
|
* @param func A function to run on every ServerMessage returned. Helps reduce loops.
|
||||||
|
* @returns An array of ServerMessages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getManyPagedMessages<T>(
|
||||||
|
query: Filter<ServerMessage>,
|
||||||
|
func?: (input: any) => T,
|
||||||
|
count: number = 5,
|
||||||
|
page: number = 0
|
||||||
|
): Promise<(Awaited<T> | null)[]> {
|
||||||
|
const find = readMany("messages", query, MessageSort)
|
||||||
|
.limit(count)
|
||||||
|
.skip(page * count);
|
||||||
|
const clan = (await cursorToArray(find, func)) as (Awaited<T> | null)[];
|
||||||
|
return clan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that puts one ServerMessage into the database.
|
||||||
|
* @param Message A ServerMessage.
|
||||||
|
* @returns A ServerMessage, or null, depending on if the operation succeeded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function createMessage(
|
||||||
|
message: Omit<ServerMessage, "_id">
|
||||||
|
): Promise<Omit<ServerMessage, "_id"> | null> {
|
||||||
|
const newMessage = await createOne("messages", message);
|
||||||
|
return newMessage.acknowledged ? message : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that changes one database Message with the given params.
|
||||||
|
* @param Message A ServerMessage.
|
||||||
|
* @returns A ServerMessage, or null, depending on if the operation succeeded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function deleteMessage(
|
||||||
|
message: Partial<ServerMessage>
|
||||||
|
): Promise<ServerMessage> {
|
||||||
|
const newMessage: ServerMessage = await deleteOne("messages", message);
|
||||||
|
return newMessage;
|
||||||
|
}
|
34
src/pages/add/message/index.tsx
Normal file
34
src/pages/add/message/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import Box from "@/components/Box/Box";
|
||||||
|
import MessageFormTemplate from "@/components/form/template/message";
|
||||||
|
import NotSignedIn from "@/components/template/not-signed-in";
|
||||||
|
import globals from "@/styles/global.module.css";
|
||||||
|
import { ThemerGetSet } from "@/types/generics";
|
||||||
|
import AuthContext from "@/utility/react/AuthContext";
|
||||||
|
import { defaultTheme } from "@/utility/react/Themer";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function AddMessage({
|
||||||
|
themerVars: [theme, setTheme]
|
||||||
|
}: {
|
||||||
|
themerVars: ThemerGetSet;
|
||||||
|
}) {
|
||||||
|
const userCredentials = useContext(AuthContext);
|
||||||
|
const router = useRouter();
|
||||||
|
// Prevent hydration error. Nav Auth section is a client-rendered element.
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
setTheme(defaultTheme);
|
||||||
|
}, []);
|
||||||
|
return isClient && userCredentials.TROLLCALL_NAME != null ? (
|
||||||
|
<>
|
||||||
|
<Box properties={{ title: { text: "Add Message" } }}>
|
||||||
|
<span className={globals.text}>Send a message to someone.</span>
|
||||||
|
</Box>
|
||||||
|
<MessageFormTemplate router={router} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<NotSignedIn />
|
||||||
|
);
|
||||||
|
}
|
51
src/pages/api/message/[clan]/.../[[...page]]/index.ts
Normal file
51
src/pages/api/message/[clan]/.../[[...page]]/index.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { ClanGET } from "@/lib/trollcall/api/clan";
|
||||||
|
import { getSingleClan } from "@/lib/trollcall/clan";
|
||||||
|
import { ServerMessageToClientMessage } from "@/lib/trollcall/convert/message";
|
||||||
|
import { getManyPagedMessages } from "@/lib/trollcall/message";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
const { method, query } = req;
|
||||||
|
const page = query.page ? query.page[0] : 0;
|
||||||
|
if (method === "GET") {
|
||||||
|
const clan = await getSingleClan({
|
||||||
|
name: query.clan
|
||||||
|
});
|
||||||
|
if (clan == null) return res.status(404).end();
|
||||||
|
const clientClan = await ClanGET(null, clan);
|
||||||
|
if (clientClan == null) return res.status(404).end();
|
||||||
|
|
||||||
|
const senders: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
const messages = await getManyPagedMessages(
|
||||||
|
{
|
||||||
|
"to": clan._id
|
||||||
|
},
|
||||||
|
async (message: any) => {
|
||||||
|
const thisMessage = await ServerMessageToClientMessage(message);
|
||||||
|
let clientFrom;
|
||||||
|
if (senders[message.from.toString()] != null)
|
||||||
|
clientFrom = senders[message.from.toString()];
|
||||||
|
else {
|
||||||
|
const from = await getSingleClan({
|
||||||
|
_id: message.from
|
||||||
|
});
|
||||||
|
if (from == null) return res.status(404).end();
|
||||||
|
clientFrom = await ClanGET(null, from);
|
||||||
|
if (clientFrom == null) return res.status(404).end();
|
||||||
|
senders[message.from.toString()] = clientFrom;
|
||||||
|
}
|
||||||
|
thisMessage.from = clientFrom;
|
||||||
|
|
||||||
|
return thisMessage;
|
||||||
|
},
|
||||||
|
5,
|
||||||
|
page
|
||||||
|
);
|
||||||
|
if (messages == null) return res.status(404).end();
|
||||||
|
res.json(messages);
|
||||||
|
} else return res.status(405).end();
|
||||||
|
}
|
28
src/pages/api/message/[clan]/index.ts
Normal file
28
src/pages/api/message/[clan]/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { getSingleClan } from "@/lib/trollcall/clan";
|
||||||
|
import { deleteMessage } from "@/lib/trollcall/message";
|
||||||
|
import { compareCredentials } from "@/lib/trollcall/perms";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
const { body, method, cookies, query } = req;
|
||||||
|
if (method === "DELETE") {
|
||||||
|
if (query.clan == null || Array.isArray(query.clan))
|
||||||
|
return res.status(400).end();
|
||||||
|
const checkClan = await getSingleClan({
|
||||||
|
name: cookies.TROLLCALL_NAME
|
||||||
|
});
|
||||||
|
if (checkClan == null) return res.status(404).end();
|
||||||
|
if (!(await compareCredentials(checkClan, cookies)))
|
||||||
|
return res.status(403).end();
|
||||||
|
console.log(query.clan);
|
||||||
|
const newMessage = await deleteMessage({
|
||||||
|
_id: new ObjectId(query.clan)
|
||||||
|
});
|
||||||
|
if (newMessage == null) return res.status(503).end();
|
||||||
|
res.json(newMessage);
|
||||||
|
} else return res.status(405).end();
|
||||||
|
}
|
44
src/pages/api/message/index.ts
Normal file
44
src/pages/api/message/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { getSingleClan } from "@/lib/trollcall/clan";
|
||||||
|
import { SubmitMessageToServerMessage } from "@/lib/trollcall/convert/message";
|
||||||
|
import { createMessage } from "@/lib/trollcall/message";
|
||||||
|
import { compareCredentials } from "@/lib/trollcall/perms";
|
||||||
|
import { SubmitMessageSchema } from "@/types/client/message";
|
||||||
|
import { ServerMessage } from "@/types/message";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
const { body, method, cookies, query } = req;
|
||||||
|
if (method === "POST") {
|
||||||
|
let validatedMessage;
|
||||||
|
try {
|
||||||
|
validatedMessage = await SubmitMessageSchema.validate(body, {
|
||||||
|
stripUnknown: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).send(err);
|
||||||
|
}
|
||||||
|
const checkClan = await getSingleClan({
|
||||||
|
name: cookies.TROLLCALL_NAME
|
||||||
|
});
|
||||||
|
if (checkClan == null) return res.status(404).end();
|
||||||
|
if (!(await compareCredentials(checkClan, cookies)))
|
||||||
|
return res.status(403).end();
|
||||||
|
// ok, logged in now. check for "to"
|
||||||
|
const checkRecipient = await getSingleClan({
|
||||||
|
name: validatedMessage.to
|
||||||
|
});
|
||||||
|
if (checkRecipient == null) return res.status(400).end();
|
||||||
|
// we are sure this object is full, so cast partial
|
||||||
|
let serverMessage = SubmitMessageToServerMessage(
|
||||||
|
validatedMessage
|
||||||
|
) as Omit<ServerMessage, "_id">;
|
||||||
|
serverMessage.to = checkRecipient._id;
|
||||||
|
serverMessage.from = checkClan._id;
|
||||||
|
const newMessage = await createMessage(serverMessage);
|
||||||
|
if (newMessage == null) return res.status(503).end();
|
||||||
|
res.json(newMessage);
|
||||||
|
} else return res.status(405).end();
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import Box from "@/components/Box/Box";
|
import Box from "@/components/Box/Box";
|
||||||
import ClanCard from "@/components/cards/ClanCard/ClanCard";
|
import ClanCard from "@/components/cards/ClanCard/ClanCard";
|
||||||
|
import MessageCard from "@/components/cards/MessageCard/MessageCard";
|
||||||
import TrollCard from "@/components/cards/TrollCard/TrollCard";
|
import TrollCard from "@/components/cards/TrollCard/TrollCard";
|
||||||
import TrollSkeleton from "@/components/cards/TrollCard/TrollSkeleton";
|
import TrollSkeleton from "@/components/cards/TrollCard/TrollSkeleton";
|
||||||
import { ClanGET } from "@/lib/trollcall/api/clan";
|
import { ClanGET } from "@/lib/trollcall/api/clan";
|
||||||
|
@ -8,6 +9,7 @@ import globals from "@/styles/global.module.css";
|
||||||
import { Color3 } from "@/types/assist/color";
|
import { Color3 } from "@/types/assist/color";
|
||||||
import { ClientClan } from "@/types/clan";
|
import { ClientClan } from "@/types/clan";
|
||||||
import { ThemerGetSet } from "@/types/generics";
|
import { ThemerGetSet } from "@/types/generics";
|
||||||
|
import { ClientMessage } from "@/types/message";
|
||||||
import { ClientTroll } from "@/types/troll";
|
import { ClientTroll } from "@/types/troll";
|
||||||
import AuthContext from "@/utility/react/AuthContext";
|
import AuthContext from "@/utility/react/AuthContext";
|
||||||
import Conditional from "@/utility/react/Conditional";
|
import Conditional from "@/utility/react/Conditional";
|
||||||
|
@ -31,7 +33,19 @@ export default function Index({
|
||||||
const [fetchedTrolls, setFetchedTrolls] = useState<ClientTroll[] | null>(
|
const [fetchedTrolls, setFetchedTrolls] = useState<ClientTroll[] | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [fetchedMessages, setFetchedMessages] = useState<ClientMessage[]>([]);
|
||||||
|
const [messagePageNum, setMessagePageNum] = useState(0);
|
||||||
|
const [noMore, setNoMore] = useState(false);
|
||||||
|
async function getMessage(page?: number) {
|
||||||
|
const res = await fetch(
|
||||||
|
page
|
||||||
|
? "/api/message/" + clan.name + "/.../" + page
|
||||||
|
: "/api/message/" + clan.name + "/..."
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
setFetchedMessages(fetchedMessages.concat(json));
|
||||||
|
setNoMore(json.length < 5);
|
||||||
|
}
|
||||||
async function getTroll(page?: number) {
|
async function getTroll(page?: number) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
page
|
page
|
||||||
|
@ -43,6 +57,7 @@ export default function Index({
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTroll(0);
|
getTroll(0);
|
||||||
|
getMessage(messagePageNum);
|
||||||
const color = clan.color?.map(x => x / 255) as [number, number, number];
|
const color = clan.color?.map(x => x / 255) as [number, number, number];
|
||||||
if (color != null)
|
if (color != null)
|
||||||
setTheme([
|
setTheme([
|
||||||
|
@ -115,6 +130,35 @@ ${clan.css ?? ""}
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{fetchedMessages.length > 0 ? (
|
||||||
|
<Box
|
||||||
|
properties={{
|
||||||
|
title: {
|
||||||
|
text: "Messages",
|
||||||
|
small: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fetchedMessages.map((message: ClientMessage, idx) => (
|
||||||
|
<MessageCard
|
||||||
|
message={message}
|
||||||
|
recipient={clan}
|
||||||
|
key={idx + "clan"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className={globals.button}
|
||||||
|
onClick={() => {
|
||||||
|
setMessagePageNum(messagePageNum + 1);
|
||||||
|
}}
|
||||||
|
disabled={noMore}
|
||||||
|
>
|
||||||
|
Load more
|
||||||
|
</button>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,10 @@ export default function Index({
|
||||||
<p className={globals.titleSmall}>Policies</p>
|
<p className={globals.titleSmall}>Policies</p>
|
||||||
<div className={globals.verticalListTop}>
|
<div className={globals.verticalListTop}>
|
||||||
<div className={globals.iconText}>
|
<div className={globals.iconText}>
|
||||||
<span className={globals.icon}>
|
<span
|
||||||
|
className={globals.icon}
|
||||||
|
title={policies.fanart}
|
||||||
|
>
|
||||||
{policies.fanart === "yes"
|
{policies.fanart === "yes"
|
||||||
? "check"
|
? "check"
|
||||||
: policies.fanart === "ask"
|
: policies.fanart === "ask"
|
||||||
|
@ -83,7 +86,10 @@ export default function Index({
|
||||||
<span className={globals.text}>Fanart</span>
|
<span className={globals.text}>Fanart</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={globals.iconText}>
|
<div className={globals.iconText}>
|
||||||
<span className={globals.icon}>
|
<span
|
||||||
|
className={globals.icon}
|
||||||
|
title={policies.fanartOthers}
|
||||||
|
>
|
||||||
{policies.fanartOthers === "yes"
|
{policies.fanartOthers === "yes"
|
||||||
? "check"
|
? "check"
|
||||||
: policies.fanartOthers === "ask"
|
: policies.fanartOthers === "ask"
|
||||||
|
@ -95,7 +101,10 @@ export default function Index({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={globals.iconText}>
|
<div className={globals.iconText}>
|
||||||
<span className={globals.icon}>
|
<span
|
||||||
|
className={globals.icon}
|
||||||
|
title={policies.fanfiction}
|
||||||
|
>
|
||||||
{policies.fanfiction === "yes"
|
{policies.fanfiction === "yes"
|
||||||
? "check"
|
? "check"
|
||||||
: policies.fanfiction === "ask"
|
: policies.fanfiction === "ask"
|
||||||
|
@ -107,7 +116,10 @@ export default function Index({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={globals.iconText}>
|
<div className={globals.iconText}>
|
||||||
<span className={globals.icon}>
|
<span
|
||||||
|
className={globals.icon}
|
||||||
|
title={policies.kinning}
|
||||||
|
>
|
||||||
{policies.kinning === "yes"
|
{policies.kinning === "yes"
|
||||||
? "check"
|
? "check"
|
||||||
: policies.kinning === "ask"
|
: policies.kinning === "ask"
|
||||||
|
@ -119,7 +131,10 @@ export default function Index({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={globals.iconText}>
|
<div className={globals.iconText}>
|
||||||
<span className={globals.icon}>
|
<span
|
||||||
|
className={globals.icon}
|
||||||
|
title={policies.shipping}
|
||||||
|
>
|
||||||
{policies.shipping === "yes"
|
{policies.shipping === "yes"
|
||||||
? "check"
|
? "check"
|
||||||
: policies.shipping === "ask"
|
: policies.shipping === "ask"
|
||||||
|
|
8
src/types/client/message.ts
Normal file
8
src/types/client/message.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as yup from "yup";
|
||||||
|
export const SubmitMessageSchema = yup.object({
|
||||||
|
subject: yup.string().min(0).max(500).ensure(),
|
||||||
|
body: yup.string().min(0).max(10000).ensure(),
|
||||||
|
to: yup.string().min(3).required()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SubmitMessage = yup.InferType<typeof SubmitMessageSchema>;
|
23
src/types/message.ts
Normal file
23
src/types/message.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { ObjectIdSchema } from "./assist/mongo";
|
||||||
|
import { ClientClanSchema } from "./clan";
|
||||||
|
import { SubmitMessageSchema } from "./client/message";
|
||||||
|
|
||||||
|
export const ServerMessageSchema = SubmitMessageSchema.shape({
|
||||||
|
_id: ObjectIdSchema.required(),
|
||||||
|
date: yup.date(),
|
||||||
|
from: ObjectIdSchema.required(),
|
||||||
|
to: ObjectIdSchema.required()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ServerMessage = yup.InferType<typeof ServerMessageSchema>;
|
||||||
|
|
||||||
|
export const ClientMessageSchema = yup.object({
|
||||||
|
_id: yup.string().required(),
|
||||||
|
date: yup.date(),
|
||||||
|
from: ClientClanSchema.required(),
|
||||||
|
subject: yup.string().min(0).max(500).ensure(),
|
||||||
|
body: yup.string().min(0).max(10000).ensure()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ClientMessage = yup.InferType<typeof ClientMessageSchema>;
|
|
@ -19,6 +19,11 @@
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"src/pages/api/message/[clan]/.../index.ts"
|
||||||
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue