diff --git a/.env.development b/.env.development
index d82fae7b..3774c194 100644
--- a/.env.development
+++ b/.env.development
@@ -5,5 +5,7 @@ NEXT_PUBLIC_FIREBASE_PROJECT_ID=ensemble-square-dev
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=ensemble-square-dev.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="135380920875"
NEXT_PUBLIC_FIREBASE_APP_ID="1:135380920875:web:300170c3c9a3f100ce186e"
+NEXT_PUBLIC_CAPTCHA_SITE_KEY=6LePMaoqAAAAALq5rY-tApn0j_7CJL0HDjMMuqMC
+CAPTCHA_SECRET_KEY=6LePMaoqAAAAALD9YTugizQkfu0eoFXf5ynd1hhi
COOKIE_SECRET_CURRENT=50cee4fa6b5406f5636d605659c44b79c2c6c7d452f20292ae4d30b6f0e30b96
-COOKIE_SECRET_PREVIOUS=61666d418e8407274bc29a6c01a1dc13e083d71f60774e892a14b5bbc0635e5c
\ No newline at end of file
+COOKIE_SECRET_PREVIOUS=61666d418e8407274bc29a6c01a1dc13e083d71f60774e892a14b5bbc0635e5c
diff --git a/components/Homepage/CurrentEventCountdown.tsx b/components/Homepage/CurrentEventCountdown.tsx
index bd119c1e..fcd09281 100644
--- a/components/Homepage/CurrentEventCountdown.tsx
+++ b/components/Homepage/CurrentEventCountdown.tsx
@@ -196,7 +196,7 @@ function CurrentEventCountdowns({ events }: { events: Event[] }) {
{shownEvents.map((event) => (
-
+
))}
>
);
diff --git a/package.json b/package.json
index 957e8940..211ec8d1 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@tanstack/react-query": "^5.56.2",
"@types/seedrandom": "^3.0.5",
"@vercel/analytics": "^0.1.6",
+ "axios": "^1.7.9",
"cookies-next": "^2.1.1",
"country-flag-icons": "^1.5.5",
"dayjs": "^1.11.7",
@@ -58,6 +59,7 @@
"react-firebase-hooks": "^5.1.1",
"react-frame-component": "^5.2.3",
"react-gesture-responder": "^2.1.0",
+ "react-google-recaptcha": "^3.1.0",
"react-grid-drag": "^1.0.0",
"react-image-crop": "^10.0.9",
"react-infinite-scroll-component": "^6.1.0",
@@ -65,7 +67,8 @@
"seedrandom": "^3.0.5",
"strapi-sdk-js": "^2.2.0",
"swr": "^2.0.0",
- "use-debounce": "^9.0.2"
+ "use-debounce": "^9.0.2",
+ "zod": "^3.24.1"
},
"scripts": {
"dev": "next dev",
@@ -98,6 +101,7 @@
"@types/react-beautiful-dnd": "^13.1.3",
"@types/react-collapse": "^5.0.1",
"@types/react-dom": "^18.0.9",
+ "@types/react-google-recaptcha": "^2.1.9",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.29.0",
"eslint-config-next": "^13.0.7",
diff --git a/pages/[user]/components/ProfileButtons.tsx b/pages/[user]/components/ProfileButtons.tsx
index 7fbf8367..91dc0c49 100644
--- a/pages/[user]/components/ProfileButtons.tsx
+++ b/pages/[user]/components/ProfileButtons.tsx
@@ -8,7 +8,6 @@ import {
Text,
useMantineTheme,
} from "@mantine/core";
-import { showNotification, updateNotification } from "@mantine/notifications";
import {
IconPencil,
IconCopy,
@@ -114,7 +113,7 @@ function ProfileButtons({
{!isOwnProfile && (
<>
- {!user && !isFriend && !isOutgoingReq && !isIncomingReq && (
+ {user && !isFriend && !isOutgoingReq && !isIncomingReq && (
sendFriendReq()}
diff --git a/pages/api/email/validate.page.ts b/pages/api/email/validate.page.ts
new file mode 100644
index 00000000..48b4aa91
--- /dev/null
+++ b/pages/api/email/validate.page.ts
@@ -0,0 +1,25 @@
+import {
+ collection,
+ getDocs,
+ getFirestore,
+ query,
+ where,
+} from "firebase/firestore";
+import { NextApiRequest, NextApiResponse } from "next";
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const email = req.body.email;
+ const db = getFirestore();
+ const q = query(collection(db, "users"), where("email", "==", email));
+ const querySnap = await getDocs(q);
+ const emailValid = !!querySnap.size;
+
+ if (emailValid) {
+ res.status(200).send({ valid: true });
+ } else {
+ res.status(404).send({ valid: false });
+ }
+}
diff --git a/pages/api/login.page.ts b/pages/api/login/index.page.ts
similarity index 96%
rename from pages/api/login.page.ts
rename to pages/api/login/index.page.ts
index 596880c4..357eeb5c 100644
--- a/pages/api/login.page.ts
+++ b/pages/api/login/index.page.ts
@@ -6,9 +6,10 @@ import {
import { FieldValue } from "firebase-admin/firestore";
import { NextApiRequest, NextApiResponse } from "next";
-import { migrateCollection } from "./collections/migrate.page";
+import { migrateCollection } from "../collections/migrate.page";
import { initAuthentication } from "services/firebase/authentication";
+import { getAuth } from "firebase/auth";
try {
initAuthentication();
diff --git a/pages/api/login/token.page.ts b/pages/api/login/token.page.ts
new file mode 100644
index 00000000..f0ff52b6
--- /dev/null
+++ b/pages/api/login/token.page.ts
@@ -0,0 +1,85 @@
+import {
+ collection,
+ Firestore,
+ getDocs,
+ getFirestore,
+ query,
+ where,
+} from "firebase/firestore";
+import { NextApiRequest, NextApiResponse } from "next";
+import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
+import { getFirebaseAdmin } from "next-firebase-auth";
+
+enum Errors {
+ NOT_FOUND = "Could not find user with this username",
+ INCOMPLETE = "Missing credentials",
+ UNAUTHED = "Invalid credentials",
+}
+
+async function validateUsernameDb(
+ username: string | undefined
+): Promise {
+ try {
+ const db = getFirestore();
+ const q = query(
+ collection(db as unknown as Firestore, "users"),
+ where("username", "==", username)
+ );
+ const querySnap = await getDocs(q);
+ const usernameValid = !querySnap.empty;
+ return usernameValid;
+ } catch {
+ return false;
+ }
+}
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ try {
+ const auth = getAuth();
+ const admin = getFirebaseAdmin();
+ const username = req.body.username;
+ const password = req.body.password;
+ if (!username || !password) {
+ throw new TypeError(Errors.INCOMPLETE);
+ }
+ const isUsernameValid = await validateUsernameDb(username);
+ if (!isUsernameValid) throw new TypeError(Errors.NOT_FOUND);
+ const db = getFirestore();
+ const usernameQuery = query(
+ collection(db, "users"),
+ where("username", "==", username)
+ );
+ const querySnap = await getDocs(usernameQuery);
+ const userData = querySnap.docs[0];
+ const email = userData.data().email;
+ const emailUserData = await admin.auth().getUserByEmail(email);
+ if (!!!emailUserData) {
+ throw new TypeError(Errors.NOT_FOUND);
+ }
+ const uid = userData.id;
+ await signInWithEmailAndPassword(auth, email, password);
+ const customToken = await admin.auth().createCustomToken(uid);
+ res.status(200).send({ customToken });
+ } catch (error) {
+ if (error.message) {
+ switch (error.message) {
+ case Errors.NOT_FOUND:
+ res.status(404).send(error);
+ break;
+ case Errors.INCOMPLETE:
+ res.status(400).send(error);
+ break;
+ case Errors.UNAUTHED:
+ res.status(403).send(error);
+ break;
+ default:
+ res.status(500).send(error);
+ }
+ } else {
+ res.status(500).send(error);
+ }
+ }
+}
diff --git a/pages/api/username/email.page.ts b/pages/api/username/email.page.ts
new file mode 100644
index 00000000..4005bb93
--- /dev/null
+++ b/pages/api/username/email.page.ts
@@ -0,0 +1,52 @@
+import {
+ collection,
+ getDocs,
+ getFirestore,
+ query,
+ where,
+} from "firebase/firestore";
+import { NextApiRequest, NextApiResponse } from "next";
+import z from "zod";
+
+export default async function handlers(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ try {
+ const username = req.body.username;
+ const shouldBeCensored = req.body.censored;
+ const db = getFirestore();
+ const q = query(collection(db, "users"), where("username", "==", username));
+ const querySnap = await getDocs(q);
+ if (!!!querySnap.size) {
+ res
+ .status(404)
+ .send({ message: "A user with this username does not exist." });
+ }
+
+ const user = querySnap.docs[0];
+ const email: string = user.data().email as string;
+
+ z.string().email().parse(email);
+
+ if (shouldBeCensored) {
+ const splitEmail = email.split("@");
+ const username = splitEmail[0];
+ const domainName = splitEmail[1];
+ const censoredEmailUsername: string = username
+ .split("")
+ .map((chara, index) => (index < 2 ? chara : "*"))
+ .join("");
+ const censoredDomainName: string = domainName
+ .split("")
+ .map((chara, index) => (index < 1 || chara === "." ? chara : "*"))
+ .join("");
+ const censoredEmail = `${censoredEmailUsername}@${censoredDomainName}`;
+ res.status(200).send({ email: censoredEmail });
+ } else {
+ res.status(200).send({ email });
+ }
+ } catch {
+ res.status(500).send({ message: "An unknown error occurred" });
+ }
+}
diff --git a/pages/api/username/validate.page.ts b/pages/api/username/validate.page.ts
new file mode 100644
index 00000000..e8d8aa14
--- /dev/null
+++ b/pages/api/username/validate.page.ts
@@ -0,0 +1,24 @@
+import {
+ collection,
+ getDocs,
+ getFirestore,
+ query,
+ where,
+} from "firebase/firestore";
+import { NextApiRequest, NextApiResponse } from "next";
+
+export default async function handlers(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const username = req.body.username;
+ const db = getFirestore();
+ const q = query(collection(db, "users"), where("username", "==", username));
+ const querySnap = await getDocs(q);
+ const usernameValid = !!!querySnap.size;
+ if (usernameValid) {
+ res.status(200).send({ valid: true });
+ } else {
+ res.status(400).send({ valid: false });
+ }
+}
diff --git a/pages/calendar/components/CalendarHeader.tsx b/pages/calendar/components/CalendarHeader.tsx
index 00028467..0fe42a38 100644
--- a/pages/calendar/components/CalendarHeader.tsx
+++ b/pages/calendar/components/CalendarHeader.tsx
@@ -1,16 +1,26 @@
import {
+ ActionIcon,
Box,
Button,
createStyles,
+ Group,
MediaQuery,
+ Menu,
+ NativeSelect,
Stack,
Title,
} from "@mantine/core";
-import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
+import { Calendar } from "@mantine/dates";
+import {
+ IconArrowLeft,
+ IconArrowRight,
+ IconSelector,
+} from "@tabler/icons-react";
import useTranslation from "next-translate/useTranslation";
-import { ReactElement, useCallback } from "react";
+import { ReactElement, useCallback, useState } from "react";
import { useDayjs } from "services/libraries/dayjs";
+import { Birthday, Event, Scout } from "types/game";
const useStyles = createStyles((theme, _params, getRef) => ({
header: {
@@ -30,10 +40,12 @@ const useStyles = createStyles((theme, _params, getRef) => ({
function CalendarHeader({
calendarTime,
setCalendarTime,
+ events,
children,
}: {
calendarTime: string;
setCalendarTime: (a: string) => void;
+ events: Array;
children?: ReactElement;
}) {
const { t } = useTranslation("calendar");
@@ -47,6 +59,15 @@ function CalendarHeader({
[setCalendarTime, calendarTime, dayjs]
);
+ const sortedEvents = events
+ .filter((event) => !(event as Birthday).character_id)
+ .sort((a, b) => dayjs(a.end.en).diff(dayjs(b.end.en)));
+
+ const earliestEvent = sortedEvents[0];
+ const latestEvent = sortedEvents[sortedEvents.length - 1];
+
+ const [openMonths, setOpenMonths] = useState(false);
+
return (