Skip to content
Open
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
6 changes: 3 additions & 3 deletions .env.template
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've rotated the keys, but these changes are not correct. I cannot merge until you remove this.

Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ AWS_WORKGROUP=_
AWS_ACCESS_KEY_ID=_
AWS_SECRET_ACCESS_KEY=_

GOOGLE_CLIENT_ID=_
GOOGLE_CLIENT_SECRET=_
SESSION_SECRET=_
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
SESSION_SECRET=

S3_ENDPOINT=http://minio:9000
S3_IMAGES_ACCESS_URL=http://localhost:9000/images
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
.root {
--bookmarks-padding-x: 0.9rem;
position: relative;
display: flex;
flex-direction: column;
background: var(--background-color);
color: var(--heading-color);
width: 384px; /* 20% wider than 320px */
min-width: 384px;
overflow: hidden;
border-left: 1px solid var(--border-color);
}

.header {
display: flex;
flex-direction: column;
flex-shrink: 0;
gap: 2rem;
padding: 1.2rem var(--bookmarks-padding-x);
border-bottom: 1px solid var(--border-color);
}

.topRow {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}

.closeSidebarButton {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
color: var(--heading-color);
border-radius: 6px;
transition: background-color 0.2s ease;

&:hover {
background-color: var(--button-hover-color);
}
}

.bookmarksButton {
flex-shrink: 0;
}

.closeSidebarIcon {
width: 1.25rem;
height: 1.25rem;
}

.headerRow {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
}

.headerLeft {
display: flex;
align-items: center;
gap: 0.45rem;
flex: 1;
min-width: 0;
}

.headerTitle {
font-size: var(--text-18);
font-weight: var(--font-semibold);
margin: 0;
padding-left: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

&.headerTitleRight {
flex: 1;
text-align: right;
}
}

.backButton {
flex-shrink: 0;
}

.closeButton {
flex-shrink: 0;
}

.content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 1.2rem var(--bookmarks-padding-x);
background-color: var(--semesterblock-color);
scrollbar-width: thin;
scrollbar-color: var(--paragraph-color) var(--border-color);
}

.collectionCards {
display: flex;
flex-direction: column;
gap: 1.2rem;
padding: 0.3rem 0;
}

.loading,
.empty {
font-size: 1.05rem;
color: var(--paragraph-color);
padding: 0.6rem 0;
}

/* Collection detail view (classes as catalog-style cards) */
.classList {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0.3rem 0;
}

.draggableClassWrapper {
cursor: grab;

&:active {
cursor: grabbing;
}
}

.classEmpty {
font-size: 1.05rem;
color: var(--paragraph-color);
padding: 1.2rem 0;
text-align: center;
}
224 changes: 224 additions & 0 deletions apps/frontend/src/app/GradTrak/Dashboard/BookmarksSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { useMemo, useState } from "react";

import { Bookmark, NavArrowRight } from "iconoir-react";

import { Button, Color } from "@repo/theme";

import { CollectionCard } from "@/app/Profile/Bookmarks/CollectionCard";
import ClassCard from "@/components/ClassCard";
import { useGetAllCollectionsWithPreview } from "@/hooks/api/collections";
import { Collection, CollectionPreviewClass } from "@/types/collection";

import styles from "./BookmarksSidebar.module.scss";

interface BookmarksSidebarProps {
onClose: () => void;
}

const GRADTRAK_BOOKMARK_DRAG_TYPE = "application/x-gradtrak-bookmark-class";

export default function BookmarksSidebar({ onClose }: BookmarksSidebarProps) {
const [selectedCollectionId, setSelectedCollectionId] = useState<
string | null
>(null);

const { data: apiCollections, loading } = useGetAllCollectionsWithPreview();

const collections = useMemo<Collection[]>(() => {
if (!apiCollections) return [];
return apiCollections
.map((c) => {
const previewClasses: CollectionPreviewClass[] = [];
for (const entry of c.classes ?? []) {
if (previewClasses.length >= 2) break;
if (!entry.class) continue;
const cls = entry.class;
previewClasses.push({
subject: cls.subject,
courseNumber: cls.courseNumber,
number: cls.number,
title: cls.title ?? cls.course?.title ?? null,
gradeAverage:
cls.gradeDistribution?.average ??
cls.course?.gradeDistribution?.average ??
null,
enrolledCount:
cls.primarySection?.enrollment?.latest?.enrolledCount ?? null,
maxEnroll:
cls.primarySection?.enrollment?.latest?.maxEnroll ?? null,
unitsMin: cls.unitsMin ?? 0,
unitsMax: cls.unitsMax ?? 0,
hasReservedSeats:
(cls.primarySection?.enrollment?.latest
?.activeReservedMaxCount ?? 0) > 0,
});
}
return {
id: c._id,
name: c.name,
classCount: c.classes?.length ?? 0,
isPinned: !!c.pinnedAt,
pinnedAt: c.pinnedAt ? new Date(c.pinnedAt).getTime() : null,
isSystem: c.isSystem ?? false,
color: (c.color ?? null) as Color | null,
lastAdd: new Date(c.lastAdd).getTime(),
previewClasses,
};
})
.sort((a, b) => {
if (a.isSystem && !b.isSystem) return -1;
if (!a.isSystem && b.isSystem) return 1;
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
if (a.isPinned && b.isPinned && a.pinnedAt != null && b.pinnedAt != null) {
return b.pinnedAt - a.pinnedAt;
}
return b.lastAdd - a.lastAdd;
});
}, [apiCollections]);

const selectedCollection = useMemo(
() => collections.find((c) => c.id === selectedCollectionId) ?? null,
[collections, selectedCollectionId]
);

const selectedCollectionApi = useMemo(
() =>
apiCollections?.find((c) => c._id === selectedCollectionId) ?? null,
[apiCollections, selectedCollectionId]
);

const handleBack = () => {
setSelectedCollectionId(null);
};

return (
<div className={styles.root}>
<div className={styles.header}>
<div className={styles.topRow}>
<button
type="button"
className={styles.closeSidebarButton}
onClick={onClose}
aria-label="Close sidebar"
>
<NavArrowRight className={styles.closeSidebarIcon} />
</button>
<Button
variant="secondary"
onClick={onClose}
className={styles.bookmarksButton}
>
<Bookmark />
Bookmarks
</Button>
</div>
<div className={styles.headerRow}>
<div className={styles.headerLeft}>
{selectedCollection ? (
<>
<Button
variant="secondary"
onClick={handleBack}
className={styles.backButton}
>
Back
</Button>
<h2
className={`${styles.headerTitle} ${styles.headerTitleRight}`}
>
{selectedCollection.name}
</h2>
</>
) : (
<h2 className={styles.headerTitle}>Bookmarks</h2>
)}
</div>
{!selectedCollection && (
<Button
variant="secondary"
onClick={onClose}
className={styles.closeButton}
>
Close
</Button>
)}
</div>
</div>

<div className={styles.content}>
{selectedCollection && selectedCollectionApi ? (
<>
{selectedCollectionApi.classes?.length === 0 ? (
<div className={styles.classEmpty}>
No classes in this collection
</div>
) : (
<div className={styles.classList}>
{selectedCollectionApi.classes?.map((entry) => {
const c = entry?.class;
if (!c) return null;
return (
<div
key={`${c.subject}-${c.courseNumber}-${c.number}`}
className={styles.draggableClassWrapper}
draggable
onDragStart={(e) => {
e.dataTransfer.setData(
GRADTRAK_BOOKMARK_DRAG_TYPE,
JSON.stringify({
source: "bookmarks",
class: {
subject: c.subject,
courseNumber: c.courseNumber,
number: c.number,
title: c.title ?? c.course?.title ?? "",
unitsMin: c.unitsMin ?? 0,
unitsMax: c.unitsMax ?? 0,
},
})
);
e.dataTransfer.effectAllowed = "copy";
}}
>
<ClassCard
class={c}
style={{ cursor: "grab" }}
/>
</div>
);
})}
</div>
)}
</>
) : (
<>
{loading ? (
<div className={styles.loading}>Loading...</div>
) : collections.length === 0 ? (
<div className={styles.empty}>No collections yet.</div>
) : (
<div className={styles.collectionCards}>
{collections.map((collection) => (
<CollectionCard
key={collection.id}
name={collection.name}
classCount={collection.classCount}
isPinned={collection.isPinned}
isSystem={collection.isSystem}
color={collection.color}
previewClasses={collection.previewClasses}
onClick={() => setSelectedCollectionId(collection.id)}
width={346}
height={204}
showPin={!collection.isSystem}
/>
))}
</div>
)}
</>
)}
</div>
</div>
);
}
Loading