Skip to content

Feat: related subject tags #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: prod
Choose a base branch
from
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
41 changes: 41 additions & 0 deletions src/app/api/related-subject/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextResponse, type NextRequest } from "next/server";
import { connectToDatabase } from "@/lib/mongoose";
import { IRelatedSubject } from "@/interface";
import RelatedSubject from "@/db/relatedSubjects";

export const dynamic = "force-dynamic";

export async function GET(req: NextRequest) {
try {
await connectToDatabase();
const url = req.nextUrl.searchParams;
const subject = url.get("subject");
const escapeRegExp = (text: string) => {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const escapedSubject = escapeRegExp(subject ?? "");

if (!subject) {
return NextResponse.json(
{ message: "Subject query parameter is required" },
{ status: 400 },
);
}
const subjects: IRelatedSubject[] = await RelatedSubject.find({
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
});
console.log("realted", subjects);

return NextResponse.json(
{
related_subjects: subjects[0]?.related_subjects
},
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
{ message: "Failed to fetch related subject", error },
{ status: 500 },
);
}
}
73 changes: 56 additions & 17 deletions src/components/CatalogueContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import axios, { type AxiosError } from "axios";
import { Button } from "@/components/ui/button";
import { type IPaper, type Filters } from "@/interface";
import { type IPaper, type Filters, IRelatedSubject, StoredSubjects } from "@/interface";
import Card from "./Card";
import { useRouter } from "next/navigation";
import Loader from "./ui/loader";
Expand All @@ -13,8 +13,8 @@ import Error from "./Error";
import { Filter } from "lucide-react";
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
import { Pin } from "lucide-react";
import { StoredSubjects } from "@/interface";
import { getSecureUrl, generateFileName, downloadFile } from "@/util/download";
import Link from "next/link";

const CatalogueContent = () => {
const router = useRouter();
Expand All @@ -39,6 +39,29 @@ const CatalogueContent = () => {
const [filtersPulled, setFiltersPulled] = useState<boolean>(false);
const [appliedFilters, setAppliedFilters] = useState<boolean>(false);
const [pinned, setPinned] = useState<boolean>(false);
const [relatedSubjects, setRelatedSubjects] = useState<string[]>([]);
// Fetch related subjects when subject changes
useEffect(() => {
if (!subject) return;
const fetchRelatedSubjects = async () => {
try {
const res = await axios.get<{related_subjects: string []}>("/api/related-subject", {
params: { subject },
});
console.log(res.data)
const data = res.data.related_subjects;
console.log("data" , data[0], data[1]);
if (data && data.length > 0) {
setRelatedSubjects(data);
} else {
setRelatedSubjects([]);
}
} catch (e) {
setRelatedSubjects([]);
}
};
void fetchRelatedSubjects();
}, [subject]);

// Set initial state from searchParams on client-side mount
useEffect(() => {
Expand Down Expand Up @@ -317,22 +340,38 @@ const CatalogueContent = () => {
</SheetContent>
</Sheet>

<div className="flex items-center gap-2 p-7">
<div>
<p className="text-s font-semibold text-gray-700 dark:text-white/80">
{subject?.split("[")[1]?.replace("]", "")}
</p>
<h2 className="text-2xl font-extrabold text-gray-700 dark:text-white md:text-3xl">
{subject?.split(" [")[0]}
</h2>
</div>
<div className="mt-7">
<button onClick={handlePinToggle}>
<Pin
className={`h-7 w-7 ${pinned ? "fill-[#A78BFA]" : ""} stroke-gray-700 dark:stroke-white`}
/>
</button>
<div className="p-7">
<div className="flex items-center gap-2">
<div>
<p className="text-s font-semibold text-gray-700 dark:text-white/80">
{subject?.split("[")[1]?.replace("]", "")}
</p>
<h2 className="text-2xl font-extrabold text-gray-700 dark:text-white md:text-3xl">
{subject?.split(" [")[0]}
</h2>
</div>
<div className="mt-7">
<button onClick={handlePinToggle}>
<Pin
className={`h-7 w-7 ${pinned ? "fill-[#A78BFA]" : ""} stroke-gray-700 dark:stroke-white`}
/>
</button>
</div>
</div>
{relatedSubjects.length > 0 && (
<div className="mt-3 flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-gray-500 dark:text-gray-300 mr-2">Related subjects:</span>
{relatedSubjects.map((sub) => (
<Link
key={sub}
href={`/catalogue?subject=${encodeURIComponent(sub)}`}
className="rounded-full bg-violet-100 px-3 py-1 text-sm font-semibold text-violet-700 transition-colors hover:bg-violet-200 dark:bg-violet-900 dark:text-violet-200 dark:hover:bg-violet-800"
>
{sub}
</Link>
))}
</div>
)}
</div>

{loading ? (
Expand Down
14 changes: 14 additions & 0 deletions src/db/relatedSubjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoose, { Schema, type Model } from "mongoose";
import { type IRelatedSubject } from "@/interface";


const relatedSubjectSchema = new Schema<IRelatedSubject>({
subject: { type: String, required: true },
related_subjects: { type: [String], required: true },
});

const RelatedSubject: Model<IRelatedSubject> =
mongoose.models.RelatedSubject ??
mongoose.model<IRelatedSubject>("RelatedSubject", relatedSubjectSchema);

export default RelatedSubject;
6 changes: 6 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,9 @@ export interface TransformedPaper {
subject: string;
slots: string[];
}


export interface IRelatedSubject {
subject: string;
related_subjects: string[];
}
48 changes: 31 additions & 17 deletions src/lib/mongoose.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import mongoose from "mongoose";

if (!process.env.MONGODB_URI) {
throw new Error("Please add your Mongo URI to .env.local");
declare global {
// eslint-disable-next-line no-var
var mongoose: { conn: mongoose.Mongoose | null; promise: Promise<mongoose.Mongoose> | null } | undefined; // This must be a `var` and not a `let / const`
}

const uri = process.env.MONGODB_URI;
let cached = global.mongoose;

let isConnected = false;
cached ??= global.mongoose = { conn: null, promise: null };

export const connectToDatabase = async () => {
if (isConnected) {
return;
}
export async function connectToDatabase() {
const MONGODB_URI = process.env.MONGODB_URI!;

if (mongoose.connection.readyState === mongoose.ConnectionStates.connected) {
isConnected = true;
return;
if (!MONGODB_URI) {
throw new Error(
"Please define the MONGODB_URI environment variable inside .env.local",
);
}

if (cached?.conn) {
return cached.conn;
}
if (cached && !cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
await mongoose.connect(uri);
isConnected = true;
} catch (error) {
throw new Error("Failed to connect to MongoDB");
cached!.conn = await cached!.promise;
} catch (e) {
if (cached) {
cached.promise = null;
}
throw e;
}
};

return cached?.conn;
}