Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ next-env.d.ts
.vscode/*
.firebase/logs/vsce-debug.log
AGENTS.md

# github
.github/agents/**
.github/copilot-instructions.md
5 changes: 0 additions & 5 deletions app/api/hello/route.ts

This file was deleted.

5 changes: 2 additions & 3 deletions app/communities/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import React, { useEffect, useMemo } from "react";
import { Community } from "@/types/community";

/**
* Displays the communities page with the top 5 communities.
* Pressing the "See More" button will display the next 5 communities.
* @returns {React.FC} - the communities page with the top 5 communities.
* Lists communities with infinite scroll, grouped by membership state.
* @returns Communities page with moderation, joined, and discovery sections.
*/
const Communities: React.FC = () => {
const { communityStateValue } = useCommunityState();
Expand Down
11 changes: 8 additions & 3 deletions app/community/[communityId]/CommunityClientPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
"use client";

import { communityStateAtom } from "@/atoms/communitiesAtom";
import About from "@/components/community/about/About";
import CreatePostLink from "@/components/community/CreatePostLink";
import About from "@/components/community/about/About";
import CommunityHeader from "@/components/community/community-header/CommunityHeader";
import PageContent from "@/components/layout/PageContent";
import Posts from "@/components/posts/Posts";
import { Community } from "@/types/community";
import { useAtom } from "jotai";
import React, { useEffect } from "react";
import { Community } from "@/types/community";

type CommunityPageProps = {
communityData: Community;
};

/**
* Client-side community page wiring header, posts feed, and about sidebar.
* @param communityData - Community data fetched on the server.
* @returns Community layout with feed and management panels.
*/
const CommunityClientPage: React.FC<CommunityPageProps> = ({
communityData,
}) => {
Expand All @@ -29,7 +34,7 @@ const CommunityClientPage: React.FC<CommunityPageProps> = ({
}
}, [communityData, setCommunityStateValue]);

const currentCommunity =
const currentCommunity: Community =
communityStateValue.currentCommunity?.id === communityData.id
? communityStateValue.currentCommunity
: communityData;
Expand Down
18 changes: 7 additions & 11 deletions app/community/[communityId]/comments/[pid]/PostClientPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

/* eslint-disable react-hooks/exhaustive-deps */
import { communityStateAtom } from "@/atoms/communitiesAtom";
import About from "@/components/community/about/About";
import PageContent from "@/components/layout/PageContent";
Expand All @@ -9,31 +8,28 @@ import Comments from "@/components/posts/comments/Comments";
import PostItem from "@/components/posts/post-item/PostItem";
import { auth } from "@/firebase/clientApp";
import useCommunityPermissions from "@/hooks/community/useCommunityPermissions";
import usePostDeletion from "@/hooks/posts/usePostDeletion";
import usePostState from "@/hooks/posts/usePostState";
import usePostVote from "@/hooks/posts/usePostVote";
import usePostDeletion from "@/hooks/posts/usePostDeletion";
import usePostVoteSync from "@/hooks/posts/usePostVoteSync";
import { Community } from "@/types/community";
import { Post } from "@/types/post";
import { Stack } from "@chakra-ui/react";
import { User } from "firebase/auth";
import { useAtom } from "jotai";
import React, { useEffect } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { Community } from "@/types/community";
import { Post } from "@/types/post";

type PostPageProps = {
communityData: Community;
postData: Post | null;
};

/**
* Displays a single post.
* Contains:
* - PostItem component
* - About component
* - Comments component
*
* @returns {React.FC} - Single post page with all components
* Client page for a single post with voting, deletion, and threaded comments.
* @param communityData - Community context for the post.
* @param postData - Post fetched on the server; may be null if not found.
* @returns Layout with post content on the left and community about panel on the right.
*/
const PostPage: React.FC<PostPageProps> = ({ communityData, postData }) => {
const { postStateValue, setPostStateValue } = usePostState();
Expand Down
9 changes: 7 additions & 2 deletions app/community/[communityId]/comments/[pid]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { getCommunityData } from "@/lib/community/getCommunityData";
import PostClientPage from "./PostClientPage";
import { notFound } from "next/navigation";
import { getPost } from "@/lib/post/getPost";
import { notFound } from "next/navigation";
import PostClientPage from "./PostClientPage";

/**
* Server component that fetches community and post data for the comments route.
* @param params - Route params containing community and post ids.
* @returns Client comment page or an error/not-found fallback.
*/
export default async function PostPage({
params,
}: {
Expand Down
7 changes: 6 additions & 1 deletion app/community/[communityId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { getCommunityData } from "@/lib/community/getCommunityData";
import CommunityClientPage from "./CommunityClientPage";
import { notFound } from "next/navigation";
import CommunityClientPage from "./CommunityClientPage";

/**
* Server component that loads community data and renders the client view.
* @param params - Route params containing the community id.
* @returns Community client page or a not-found/error fallback.
*/
export default async function Page({
params,
}: {
Expand Down
7 changes: 6 additions & 1 deletion app/community/[communityId]/submit/SubmitPostClientPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import PageContent from "@/components/layout/PageContent";
import AuthButtons from "@/components/navbar/right-content/AuthButtons";
import NewPostForm from "@/components/posts/new-post-form/NewPostForm";
import { auth } from "@/firebase/clientApp";
import { Community } from "@/types/community";
import { Box, Stack, Text } from "@chakra-ui/react";
import { useAtom, useSetAtom } from "jotai";
import React, { useEffect } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { Community } from "@/types/community";

type SubmitPostPageProps = {
communityData: Community;
};

/**
* Client page for creating a new post inside a community.
* @param communityData - Community where the post will be published.
* @returns Post creation form with sidebar about section.
*/
const SubmitPostPage: React.FC<SubmitPostPageProps> = ({ communityData }) => {
const [user] = useAuthState(auth);
const [communityStateValue, setCommunityStateValue] =
Expand Down
7 changes: 6 additions & 1 deletion app/community/[communityId]/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { getCommunityData } from "@/lib/community/getCommunityData";
import SubmitPostClientPage from "./SubmitPostClientPage";
import { notFound } from "next/navigation";
import SubmitPostClientPage from "./SubmitPostClientPage";

/**
* Server component that prepares data for the submit-post page of a community.
* @param params - Route params containing the community id.
* @returns Client submit page or an error/not-found fallback.
*/
export default async function SubmitPostPage({
params,
}: {
Expand Down
7 changes: 6 additions & 1 deletion app/emotion-registry.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
"use client";

import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { useServerInsertedHTML } from "next/navigation";
import { useState } from "react";

/**
* Configures Emotion cache for the App Router and injects styles during SSR.
* @param children - React tree that needs Emotion styling support.
* @returns Cache provider that ensures styles render on both server and client.
*/
export default function EmotionRegistry({
children,
}: {
Expand Down
7 changes: 6 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Providers } from "./providers";
import { Metadata } from "next";
import { Providers } from "./providers";

export const metadata: Metadata = {
title: "Circus Discussions",
description: "A discussion platform",
};

/**
* Root layout wiring global providers and metadata for the App Router.
* @param children - Route content to render inside the provider tree.
* @returns HTML scaffold with Providers applied to the body.
*/
export default function RootLayout({
children,
}: {
Expand Down
4 changes: 4 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Button, Flex, Stack, Text } from "@chakra-ui/react";
import Link from "next/link";
import React from "react";

/**
* Fallback page for unknown routes with quick links back to core pages.
* @returns Centered message and navigation buttons.
*/
const PageNotFound: React.FC = () => {
return (
<Flex
Expand Down
14 changes: 8 additions & 6 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
"use client";

/* eslint-disable react-hooks/exhaustive-deps */
import CreatePostLink from "@/components/community/CreatePostLink";
import PersonalHome from "@/components/community/PersonalHome";
import Recommendations from "@/components/community/recommendations/Recommendations";
import PageContent from "@/components/layout/PageContent";
import PostLoader from "@/components/loaders/post-loader/PostLoader";
import PostItem from "@/components/posts/post-item/PostItem";
import { auth, firestore } from "@/firebase/clientApp";
import { auth } from "@/firebase/clientApp";
import useCommunityState from "@/hooks/community/useCommunityState";
import useCustomToast from "@/hooks/useCustomToast";
import usePostState from "@/hooks/posts/usePostState";
import usePostDeletion from "@/hooks/posts/usePostDeletion";
import usePostSelection from "@/hooks/posts/usePostSelection";
import usePostState from "@/hooks/posts/usePostState";
import usePostVote from "@/hooks/posts/usePostVote";
import usePostDeletion from "@/hooks/posts/usePostDeletion";
import usePostVoteSync from "@/hooks/posts/usePostVoteSync";
import usePostsFeed from "@/hooks/posts/usePostsFeed";
import useCustomToast from "@/hooks/useCustomToast";
import { Box, Spinner, Stack, Text } from "@chakra-ui/react";
import { collection, getDocs, query, where } from "firebase/firestore";
import { useEffect, useMemo } from "react";
import { useAuthState } from "react-firebase-hooks/auth";

/**
* Renders the home feed with infinite scrolling posts and a sidebar of recommendations.
* @returns Main content paired with community suggestions and personal shortcuts.
*/
export default function Home() {
const [user, loadingUser] = useAuthState(auth);
const { communityStateValue } = useCommunityState();
Expand Down Expand Up @@ -52,7 +54,7 @@
if (communityStateValue.snippetFetched) {
fetchPosts(true);
}
}, [communityStateValue.snippetFetched, user, communityIds.length]);

Check warning on line 57 in app/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'fetchPosts'. Either include it or remove the dependency array

/**
* Loads the home feed for unauthenticated users.
Expand All @@ -63,7 +65,7 @@
if (!user && !loadingUser) {
fetchPosts(true);
}
}, [user, loadingUser]);

Check warning on line 68 in app/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'fetchPosts'. Either include it or remove the dependency array

/**
* Posts need to exist before trying to fetch votes for posts
Expand All @@ -80,7 +82,7 @@
}));
};
}
}, [user, postStateValue.posts]);

Check warning on line 85 in app/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'getPostVotes' and 'setPostStateValue'. Either include them or remove the dependency array

return (
<PageContent>
Expand Down
11 changes: 8 additions & 3 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"use client";

import { ChakraProvider, Toaster } from "@chakra-ui/react";
import { theme } from "@/chakra/theme";
import Layout from "@/components/layout/Layout";
import { ColorModeProvider } from "@/components/ui/color-mode";
import { toaster } from "@/hooks/useCustomToast";
import { ChakraProvider, Toaster } from "@chakra-ui/react";
import { Provider as JotaiProvider } from "jotai";
import { useEffect, useState } from "react";
import { toaster } from "@/hooks/useCustomToast";
import EmotionRegistry from "./emotion-registry";
import { ColorModeProvider } from "@/components/ui/color-mode";

/**
* Wraps the app with global providers for state, styling, theming, and layout shell.
* @param children - Next.js page content to render inside the provider tree.
* @returns Provider hierarchy with a mounted flag to avoid hydration issues for the toaster.
*/
export function Providers({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);

Expand Down
12 changes: 4 additions & 8 deletions atoms/authModalAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@ import { AuthModalState } from "@/types/authModal";
* Describes the default state of the authentication modal.
* By default, the modal is closed and
* if no state is specified, it will open in the log in view.
* @property {boolean} open - modal is closed by default
* @property {"login"} view - log in view is displayed by default
* @property open - modal is closed by default
* @property view - log in view is displayed by default
*/
const defaultModalState: AuthModalState = {
open: false,
view: "login",
};

/**
* Atom which describes the state of the authentication modal.
*
* @requires AuthModalState - state definition
* @requires defaultModalState - default state
*
* @see https://jotai.org/docs/core/atom
* Controls whether the auth modal is visible and which screen is active.
* @returns Jotai atom with open flag and active view.
*/
export const authModalStateAtom = atom<AuthModalState>(defaultModalState);
22 changes: 9 additions & 13 deletions atoms/communitiesAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,29 @@ import { Community, CommunitySnippet } from "@/types/community";

/**
* Stores the community snippets to track the state of the communities atom.
* @property {CommunitySnippet[]} mySnippets - list of community snippets
* @property {Community} currentCommunity - the community the user is currently in
* @property {boolean} snippetFetched - whether the community snippets have been fetched or not
* @propertymySnippets - list of community snippets
* @property currentCommunity - the community the user is currently in
* @property snippetFetched - whether the community snippets have been fetched or not
*/
interface CommunityState {
mySnippets: CommunitySnippet[]; // stores a list of community snippets
currentCommunity?: Community; // user is not always in a community hence optional
mySnippets: CommunitySnippet[];
currentCommunity?: Community;
snippetFetched: boolean;
}

/**
* Initially, the array for the community state is empty.
* The community snippets have not been fetched initially hence array is empty.
* @property {CommunitySnippet[]} mySnippets - empty array
* @property {boolean} snippetFetched - false by default
* @property mySnippets - empty array
* @property snippetFetched - false by default
*/
export const defaultCommunityState: CommunityState = {
mySnippets: [],
snippetFetched: false,
};

/**
* Atom which describes the state of the community state.
*
* @requires CommunityState - state definition
* @requires defaultCommunityState - default state
*
* @see https://jotai.org/docs/core/atom
* Stores the user's community snippets and the active community context.
* @returns Jotai atom tracking membership data and whether snippets are loaded.
*/
export const communityStateAtom = atom<CommunityState>(defaultCommunityState);
20 changes: 4 additions & 16 deletions atoms/directoryMenuAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,28 @@ import { DirectoryMenuItem } from "@/types/directoryMenu";

/**
* Interface which describes the state of the directory menu.
* @property {boolean} isOpen - whether the directory menu is open or not
* @property {DirectoryMenuItem} selectedMenuItem - the menu item that is currently selected
* @property isOpen - whether the directory menu is open or not
* @property selectedMenuItem - the menu item that is currently selected
*/
interface DirectoryMenuState {
isOpen: boolean;
selectedMenuItem: DirectoryMenuItem;
}

/**
* Default menu item when no community is selected (home page).
* @property {string} displayText - "Home"
* @property {string} link - "/" (home page)
* @property {IconType} icon - TiHome (home icon)
* @property {string} iconColor - "black"
*/
export const defaultMenuItem = {
displayText: "Home",
link: "/",
icon: TiHome,
iconColor: { base: "black", _dark: "white" },
};

/**
* Default state of the directory menu.
* The directory menu is closed by default.
* @property {boolean} isOpen - false by default
* @property {DirectoryMenuItem} selectedMenuItem - default menu item (home page)
*/
export const defaultMenuState: DirectoryMenuState = {
isOpen: false,
selectedMenuItem: defaultMenuItem,
};

/**
* Atom which stores the state of the directory menu.
* Controls the navbar directory dropdown and the currently highlighted item.
* @returns Jotai atom containing open state and selected menu item.
*/
export const directoryMenuAtom = atom<DirectoryMenuState>(defaultMenuState);
Loading