diff --git a/.gitignore b/.gitignore index 46ccfe3c20..c2658d7d1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ node_modules/ -docker-compose.prod* diff --git a/README.md b/README.md index 4f75898f0a..bd88888a19 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ and access PeerPrep at [localhost:3000](http://localhost:3000), or the IP addres #### Developing If you are developing PeerPrep, you can use [Compose Watch](https://docs.docker.com/compose/how-tos/file-watch/) to automatically update and preview code changes: ```sh -docker compose up --watch --build +docker compose -f docker-compose.dev.yml up --watch --build ``` ### API Endpoints diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000000..400139ae1f --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,94 @@ +# docker-compose for dev +version: '3.8' + +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + args: + PUBLIC_URL: ${PUBLIC_URL} + WS_PUBLIC_URL: ${WS_PUBLIC_URL} + FRONTEND_PORT: ${FRONTEND_PORT} + QUESTION_API_PORT: ${QUESTION_API_PORT} + USER_API_PORT: ${USER_API_PORT} + MATCHING_API_PORT: ${MATCHING_API_PORT} + COLLAB_API_PORT: ${COLLAB_API_PORT} + ports: + - "${FRONTEND_PORT}:${FRONTEND_PORT}" + develop: + watch: + - action: sync + path: ./frontend + target: /app + + question: + build: + context: ./backend/question-service + dockerfile: Dockerfile.dev + ports: + - "${QUESTION_API_PORT}:2000" + develop: + watch: + - action: sync + path: ./backend/question-service + target: /app + + user: + build: + context: ./backend/user-service + dockerfile: Dockerfile.prod + env_file: + - ./backend/user-service/.env + ports: + - "${USER_API_PORT}:3001" + + zookeeper: + image: confluentinc/cp-zookeeper:7.7.1 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ports: + - "2181:2181" + healthcheck: + test: [ "CMD", "echo", "ruok", "|", "nc", "localhost", "2181", "|", "grep", "imok" ] + interval: 10s + timeout: 5s + retries: 5 + + kafka: + image: confluentinc/cp-kafka:7.7.1 + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + depends_on: + zookeeper: + condition: service_healthy + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "9092" ] + interval: 10s + timeout: 5s + retries: 10 + + matching-service: + build: + context: ./backend/matching-service + dockerfile: Dockerfile.match + environment: + KAFKA_BROKER: kafka:9092 + ports: + - "${MATCHING_API_PORT}:3002" + depends_on: + kafka: + condition: service_healthy + + collab-service: + build: + context: ./backend/collab-service + dockerfile: Dockerfile.dev + ports: + - "${COLLAB_API_PORT}:3003" diff --git a/docker-compose.yml b/docker-compose.yml index 400139ae1f..14e0ac124f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,10 @@ -# docker-compose for dev version: '3.8' services: frontend: build: context: ./frontend - dockerfile: Dockerfile.dev + dockerfile: Dockerfile.prod args: PUBLIC_URL: ${PUBLIC_URL} WS_PUBLIC_URL: ${WS_PUBLIC_URL} @@ -16,11 +15,11 @@ services: COLLAB_API_PORT: ${COLLAB_API_PORT} ports: - "${FRONTEND_PORT}:${FRONTEND_PORT}" - develop: - watch: - - action: sync - path: ./frontend - target: /app + depends_on: + - question + - user + - matching-service + - collab-service question: build: @@ -92,3 +91,4 @@ services: dockerfile: Dockerfile.dev ports: - "${COLLAB_API_PORT}:3003" + diff --git a/frontend/.gitignore b/frontend/.gitignore index 9fc5ae20ee..39d8122974 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -34,5 +34,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts - -Dockerfile.prod \ No newline at end of file diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 0000000000..db10c32868 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,54 @@ +FROM node:22-alpine3.19 AS builder +WORKDIR /app + +ARG PUBLIC_URL +ARG WS_PUBLIC_URL +ARG FRONTEND_PORT +ARG QUESTION_API_PORT +ARG USER_API_PORT +ARG MATCHING_API_PORT +ARG COLLAB_API_PORT + +ENV PUBLIC_URL=${PUBLIC_URL} +ENV WS_PUBLIC_URL=${WS_PUBLIC_URL} + +ENV FRONTEND_PORT=${FRONTEND_PORT} +ENV NEXT_PUBLIC_FRONTEND_URL=${PUBLIC_URL}:${FRONTEND_PORT} + +ENV QUESTION_API_PORT=${QUESTION_API_PORT} +ENV NEXT_PUBLIC_QUESTION_API_BASE_URL=${PUBLIC_URL}:${QUESTION_API_PORT}/questions + +ENV USER_API_PORT=${USER_API_PORT} +ENV USER_API_BASE_URL=${PUBLIC_URL}:${USER_API_PORT} +ENV NEXT_PUBLIC_USER_API_AUTH_URL=${USER_API_BASE_URL}/auth +ENV NEXT_PUBLIC_USER_API_USERS_URL=${USER_API_BASE_URL}/users +ENV NEXT_PUBLIC_USER_API_EMAIL_URL=${USER_API_BASE_URL}/email +ENV NEXT_PUBLIC_USER_API_HISTORY_URL=${USER_API_BASE_URL}/users/history + +ENV MATCHING_API_PORT=${MATCHING_API_PORT} +ENV NEXT_PUBLIC_MATCHING_API_URL=${PUBLIC_URL}:${MATCHING_API_PORT}/matching + +ENV COLLAB_API_PORT=${COLLAB_API_PORT} +ENV NEXT_PUBLIC_COLLAB_API_URL=${PUBLIC_URL}:${COLLAB_API_PORT} + +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + + +FROM node:22-alpine3.19 AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN mkdir .next +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +EXPOSE 3000 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/frontend/app/(authenticated)/layout.tsx b/frontend/app/(authenticated)/layout.tsx index 585810caf8..f0c9eb09ab 100644 --- a/frontend/app/(authenticated)/layout.tsx +++ b/frontend/app/(authenticated)/layout.tsx @@ -72,7 +72,7 @@ export default function AuthenticatedLayout({ function logout() { deleteAllCookies(); - router.push('/'); + window.location.href = '/'; } return ( diff --git a/frontend/app/(authenticated)/profile/question-history/code/page.tsx b/frontend/app/(authenticated)/profile/question-history/code/page.tsx index 59b31146dd..a89409fd77 100644 --- a/frontend/app/(authenticated)/profile/question-history/code/page.tsx +++ b/frontend/app/(authenticated)/profile/question-history/code/page.tsx @@ -145,7 +145,7 @@ function CodeViewerContent() {
{/* Left Panel: Question Details */} -

+

{questionDetails?.title || ""}

diff --git a/frontend/app/(authenticated)/profile/question-history/columns.tsx b/frontend/app/(authenticated)/profile/question-history/columns.tsx index b330005904..7258d78da9 100644 --- a/frontend/app/(authenticated)/profile/question-history/columns.tsx +++ b/frontend/app/(authenticated)/profile/question-history/columns.tsx @@ -1,12 +1,12 @@ "use client" - + import { Badge, BadgeProps } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { ColumnDef } from "@tanstack/react-table" -import { AlignLeft, ArrowUpDown, MoreHorizontal } from "lucide-react" +import { MoreHorizontal } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import Link from "next/link" +import { DataTableColumnHeader } from "../../question-repo/data-table-column-header"; export type QuestionHistory = { id: number; @@ -24,13 +24,7 @@ export const columns : ColumnDef[]= [ accessorKey: "id", header: ({ column }) => { return ( - + ) }, }, @@ -38,50 +32,14 @@ export const columns : ColumnDef[]= [ accessorKey: "title", header: ({ column }) => { return ( - + ) }, - cell: ({ row }) => { - return ( - - -
- - {row.getValue("title")} - -
-
- -
-
- - Description -
-
-

{row.original.description}

-
-
-
-
- ) - }, }, { header: ({ column }) => { return ( - + ) }, accessorKey: "categories", @@ -96,21 +54,13 @@ export const columns : ColumnDef[]= [ ), filterFn: (row, id, selectedCategories) => { const rowCategories = row.getValue(id); - console.log(selectedCategories); - console.log(rowCategories); return selectedCategories.every((category: string) => (rowCategories as string[]).includes(category)); }, }, { header: ({ column }) => { return ( - + ) }, accessorKey: "complexity", @@ -128,13 +78,7 @@ export const columns : ColumnDef[]= [ { header: ({ column }) => { return ( - + ) }, accessorKey: "attemptCount", @@ -143,13 +87,7 @@ export const columns : ColumnDef[]= [ { header: ({ column }) => { return ( - + ) }, accessorKey: "attemptTime", @@ -159,28 +97,23 @@ export const columns : ColumnDef[]= [ { header: ({ column }) => { return ( - + ) }, accessorKey: "attemptDate", cell: ({ row }) => { const attemptDate = row.original.attemptDate; return new Date(attemptDate).toLocaleString("en-GB", { - day: "2-digit", - month: "2-digit", + day: "numeric", + month: "short", year: "numeric", - hour: "2-digit", + hour: "numeric", minute: "2-digit", - second: "2-digit", - hour12: false, + hour12: true, }); }, + sortingFn: "datetime", + sortDescFirst: true, }, { id: "actions", diff --git a/frontend/app/(authenticated)/profile/question-history/page.tsx b/frontend/app/(authenticated)/profile/question-history/page.tsx index b6fbe7de79..9d8caab3ab 100644 --- a/frontend/app/(authenticated)/profile/question-history/page.tsx +++ b/frontend/app/(authenticated)/profile/question-history/page.tsx @@ -118,8 +118,19 @@ export default function UserQuestionHistory() { // Success state: Render the list of attempted questions return (
-
My Question History
- +
Question History
+
); }; \ No newline at end of file diff --git a/frontend/app/(authenticated)/question-repo/columns.tsx b/frontend/app/(authenticated)/question-repo/columns.tsx index c66160f6f4..c57c35628f 100644 --- a/frontend/app/(authenticated)/question-repo/columns.tsx +++ b/frontend/app/(authenticated)/question-repo/columns.tsx @@ -8,6 +8,7 @@ import { DataTableRowActions } from "./data-table-row-actions" import { Dispatch, SetStateAction } from "react" import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" import { AlignLeft } from "lucide-react" +import Markdown from "react-markdown" export type Question = { @@ -67,14 +68,16 @@ export const columns: (param: Dispatch>) => ColumnDef
- +
Description
-

{row.original.description}

+ + {row.original.description} +
diff --git a/frontend/app/(authenticated)/question-repo/data-table.tsx b/frontend/app/(authenticated)/question-repo/data-table.tsx index 568c8437f1..c3640a95f3 100644 --- a/frontend/app/(authenticated)/question-repo/data-table.tsx +++ b/frontend/app/(authenticated)/question-repo/data-table.tsx @@ -34,6 +34,7 @@ interface DataTableProps { setData?: React.Dispatch>; loading: boolean isVisible?: boolean + initialSorting?: SortingState } export function DataTable({ @@ -42,9 +43,10 @@ export function DataTable({ setData, loading, isVisible = true, + initialSorting = [], }: DataTableProps) { const [rowSelection, setRowSelection] = useState({}) - const [sorting, setSorting] = useState([]) + const [sorting, setSorting] = useState(initialSorting) const [columnFilters, setColumnFilters] = useState( [] ) diff --git a/frontend/app/auth/login/page.tsx b/frontend/app/auth/login/page.tsx index 6f18bc8ade..e2440e8fee 100644 --- a/frontend/app/auth/login/page.tsx +++ b/frontend/app/auth/login/page.tsx @@ -94,9 +94,9 @@ export default function Login() { setCookie('isAdmin', isAdmin.toString(), { 'max-age': '86400', 'path': '/', 'SameSite': 'Strict' }); }; - await setCookiesAsync().then(() => { - router.push('/questions'); - }); + await setCookiesAsync(); + + window.location.href = '/questions'; } catch (error) { if (!isErrorSet) { setError("Something went wrong. Please retry shortly."); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index ba0e53b060..9e0489abe0 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,8 +1,20 @@ +"use client" + import { Button } from "@/components/ui/button"; import Link from "next/link"; import Image from 'next/image'; +import { getToken } from "./utils/cookie-manager"; +import { ArrowRight } from "lucide-react"; +import { useState, useEffect } from "react"; export default function Landing() { + const [hasToken, setHasToken] = useState(false); + + useEffect(() => { + const token = getToken(); + setHasToken(!!token); + }, []); + return (
@@ -15,13 +27,21 @@ export default function Landing() {

Prep for your next interview with your Peers

- - or - + {hasToken ? ( + + ) : ( + <> + + or + + + )}
diff --git a/frontend/app/session/[id]/page.tsx b/frontend/app/session/[id]/page.tsx index 0b982b7e33..cb53266daa 100644 --- a/frontend/app/session/[id]/page.tsx +++ b/frontend/app/session/[id]/page.tsx @@ -57,16 +57,6 @@ export default function Session() { const codeProviderRef = useRef(null); const notesProviderRef = useRef(null); - useEffect(() => { - const timerInterval = setInterval(() => { - setTimeElapsed((prevTime) => prevTime + 1); - }, 1000); - - return () => { - clearInterval(timerInterval); - }; - }, []); - const minutes = Math.floor(timeElapsed / 60); const seconds = timeElapsed % 60; @@ -213,6 +203,37 @@ export default function Session() { setCalling(true); }, [isSessionEnded, params.id, questionId, router]); + // Synced timer + const sharedState = codeDocRef.current?.getMap('sharedState'); + useEffect(() => { + // Set initial start time if not set + const startTime = sharedState?.get('startTime') || Date.now(); + if (!sharedState?.get('startTime')) { + sharedState?.set('startTime', startTime); + } + + // Observe changes to startTime + const observer = () => { + const currentStartTime = sharedState?.get('startTime'); + if (currentStartTime) { + const elapsed = Math.floor((Date.now() - Number(currentStartTime)) / 1000); + setTimeElapsed(elapsed); + } + }; + + // Update timer every second + const timer = setInterval(observer, 1000); + + // Subscribe to changes in shared state + sharedState?.observe(observer); + + return () => { + clearInterval(timer); + sharedState?.unobserve(observer); + }; + }, [sharedState]); + + // Voice chat useJoin({appid: "9da9d118c6a646d1a010b4b227ca1345", channel: `voice-${params.id}`, token: null}, calling); const { localMicrophoneTrack } = useLocalMicrophoneTrack(isMicEnabled);