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 != "" ? (
|
||||
<>
|
||||
<span className={globals.icon}>
|
||||
<Link
|
||||
className={globals.link}
|
||||
href="/add/message/"
|
||||
>
|
||||
send
|
||||
</Link>
|
||||
</span>
|
||||
<span className={globals.icon}>
|
||||
<Link
|
||||
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} />
|
||||
<Conditional
|
||||
condition={
|
||||
troll.class != null &&
|
||||
troll.gender != null &&
|
||||
troll.class != null ||
|
||||
troll.gender != 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) {
|
||||
const selectedCollection = mainDB.collection(collection);
|
||||
return await selectedCollection.findOneAndDelete(find);
|
||||
return await selectedCollection.deleteOne(find);
|
||||
}
|
||||
|
||||
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 ClanCard from "@/components/cards/ClanCard/ClanCard";
|
||||
import MessageCard from "@/components/cards/MessageCard/MessageCard";
|
||||
import TrollCard from "@/components/cards/TrollCard/TrollCard";
|
||||
import TrollSkeleton from "@/components/cards/TrollCard/TrollSkeleton";
|
||||
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 { ClientClan } from "@/types/clan";
|
||||
import { ThemerGetSet } from "@/types/generics";
|
||||
import { ClientMessage } from "@/types/message";
|
||||
import { ClientTroll } from "@/types/troll";
|
||||
import AuthContext from "@/utility/react/AuthContext";
|
||||
import Conditional from "@/utility/react/Conditional";
|
||||
|
@ -31,7 +33,19 @@ export default function Index({
|
|||
const [fetchedTrolls, setFetchedTrolls] = useState<ClientTroll[] | 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) {
|
||||
const res = await fetch(
|
||||
page
|
||||
|
@ -43,6 +57,7 @@ export default function Index({
|
|||
}
|
||||
useEffect(() => {
|
||||
getTroll(0);
|
||||
getMessage(messagePageNum);
|
||||
const color = clan.color?.map(x => x / 255) as [number, number, number];
|
||||
if (color != null)
|
||||
setTheme([
|
||||
|
@ -115,6 +130,35 @@ ${clan.css ?? ""}
|
|||
))
|
||||
)}
|
||||
</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>
|
||||
<div className={globals.verticalListTop}>
|
||||
<div className={globals.iconText}>
|
||||
<span className={globals.icon}>
|
||||
<span
|
||||
className={globals.icon}
|
||||
title={policies.fanart}
|
||||
>
|
||||
{policies.fanart === "yes"
|
||||
? "check"
|
||||
: policies.fanart === "ask"
|
||||
|
@ -83,7 +86,10 @@ export default function Index({
|
|||
<span className={globals.text}>Fanart</span>
|
||||
</div>
|
||||
<div className={globals.iconText}>
|
||||
<span className={globals.icon}>
|
||||
<span
|
||||
className={globals.icon}
|
||||
title={policies.fanartOthers}
|
||||
>
|
||||
{policies.fanartOthers === "yes"
|
||||
? "check"
|
||||
: policies.fanartOthers === "ask"
|
||||
|
@ -95,7 +101,10 @@ export default function Index({
|
|||
</span>
|
||||
</div>
|
||||
<div className={globals.iconText}>
|
||||
<span className={globals.icon}>
|
||||
<span
|
||||
className={globals.icon}
|
||||
title={policies.fanfiction}
|
||||
>
|
||||
{policies.fanfiction === "yes"
|
||||
? "check"
|
||||
: policies.fanfiction === "ask"
|
||||
|
@ -107,7 +116,10 @@ export default function Index({
|
|||
</span>
|
||||
</div>
|
||||
<div className={globals.iconText}>
|
||||
<span className={globals.icon}>
|
||||
<span
|
||||
className={globals.icon}
|
||||
title={policies.kinning}
|
||||
>
|
||||
{policies.kinning === "yes"
|
||||
? "check"
|
||||
: policies.kinning === "ask"
|
||||
|
@ -119,7 +131,10 @@ export default function Index({
|
|||
</span>
|
||||
</div>
|
||||
<div className={globals.iconText}>
|
||||
<span className={globals.icon}>
|
||||
<span
|
||||
className={globals.icon}
|
||||
title={policies.shipping}
|
||||
>
|
||||
{policies.shipping === "yes"
|
||||
? "check"
|
||||
: 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/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/pages/api/message/[clan]/.../index.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue