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")}
-
-
-
-
-
-
-
-
{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
-
+
-
{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);