Skip to content

Commit c062727

Browse files
Merge pull request #247 from abhitrueprogrammer/related
Feat: related subject tags
2 parents 7da80d6 + 023a0dd commit c062727

File tree

5 files changed

+148
-34
lines changed

5 files changed

+148
-34
lines changed

src/app/api/related-subject/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextResponse, type NextRequest } from "next/server";
2+
import { connectToDatabase } from "@/lib/mongoose";
3+
import { IRelatedSubject } from "@/interface";
4+
import RelatedSubject from "@/db/relatedSubjects";
5+
6+
export const dynamic = "force-dynamic";
7+
8+
export async function GET(req: NextRequest) {
9+
try {
10+
await connectToDatabase();
11+
const url = req.nextUrl.searchParams;
12+
const subject = url.get("subject");
13+
const escapeRegExp = (text: string) => {
14+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15+
};
16+
const escapedSubject = escapeRegExp(subject ?? "");
17+
18+
if (!subject) {
19+
return NextResponse.json(
20+
{ message: "Subject query parameter is required" },
21+
{ status: 400 },
22+
);
23+
}
24+
const subjects: IRelatedSubject[] = await RelatedSubject.find({
25+
subject: { $regex: new RegExp(`${escapedSubject}`, "i") },
26+
});
27+
console.log("realted", subjects);
28+
29+
return NextResponse.json(
30+
{
31+
related_subjects: subjects[0]?.related_subjects
32+
},
33+
{ status: 200 },
34+
);
35+
} catch (error) {
36+
return NextResponse.json(
37+
{ message: "Failed to fetch related subject", error },
38+
{ status: 500 },
39+
);
40+
}
41+
}

src/components/CatalogueContent.tsx

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
44
import { useCallback, useEffect, useState } from "react";
55
import axios, { type AxiosError } from "axios";
66
import { Button } from "@/components/ui/button";
7-
import { type IPaper, type Filters } from "@/interface";
7+
import { type IPaper, type Filters, IRelatedSubject, StoredSubjects } from "@/interface";
88
import Card from "./Card";
99
import { useRouter } from "next/navigation";
1010
import Loader from "./ui/loader";
@@ -13,8 +13,8 @@ import Error from "./Error";
1313
import { Filter } from "lucide-react";
1414
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
1515
import { Pin } from "lucide-react";
16-
import { StoredSubjects } from "@/interface";
1716
import { getSecureUrl, generateFileName, downloadFile } from "@/util/download";
17+
import Link from "next/link";
1818

1919
const CatalogueContent = () => {
2020
const router = useRouter();
@@ -39,6 +39,29 @@ const CatalogueContent = () => {
3939
const [filtersPulled, setFiltersPulled] = useState<boolean>(false);
4040
const [appliedFilters, setAppliedFilters] = useState<boolean>(false);
4141
const [pinned, setPinned] = useState<boolean>(false);
42+
const [relatedSubjects, setRelatedSubjects] = useState<string[]>([]);
43+
// Fetch related subjects when subject changes
44+
useEffect(() => {
45+
if (!subject) return;
46+
const fetchRelatedSubjects = async () => {
47+
try {
48+
const res = await axios.get<{related_subjects: string []}>("/api/related-subject", {
49+
params: { subject },
50+
});
51+
console.log(res.data)
52+
const data = res.data.related_subjects;
53+
console.log("data" , data[0], data[1]);
54+
if (data && data.length > 0) {
55+
setRelatedSubjects(data);
56+
} else {
57+
setRelatedSubjects([]);
58+
}
59+
} catch (e) {
60+
setRelatedSubjects([]);
61+
}
62+
};
63+
void fetchRelatedSubjects();
64+
}, [subject]);
4265

4366
// Set initial state from searchParams on client-side mount
4467
useEffect(() => {
@@ -317,22 +340,38 @@ const CatalogueContent = () => {
317340
</SheetContent>
318341
</Sheet>
319342

320-
<div className="flex items-center gap-2 p-7">
321-
<div>
322-
<p className="text-s font-semibold text-gray-700 dark:text-white/80">
323-
{subject?.split("[")[1]?.replace("]", "")}
324-
</p>
325-
<h2 className="text-2xl font-extrabold text-gray-700 dark:text-white md:text-3xl">
326-
{subject?.split(" [")[0]}
327-
</h2>
328-
</div>
329-
<div className="mt-7">
330-
<button onClick={handlePinToggle}>
331-
<Pin
332-
className={`h-7 w-7 ${pinned ? "fill-[#A78BFA]" : ""} stroke-gray-700 dark:stroke-white`}
333-
/>
334-
</button>
343+
<div className="p-7">
344+
<div className="flex items-center gap-2">
345+
<div>
346+
<p className="text-s font-semibold text-gray-700 dark:text-white/80">
347+
{subject?.split("[")[1]?.replace("]", "")}
348+
</p>
349+
<h2 className="text-2xl font-extrabold text-gray-700 dark:text-white md:text-3xl">
350+
{subject?.split(" [")[0]}
351+
</h2>
352+
</div>
353+
<div className="mt-7">
354+
<button onClick={handlePinToggle}>
355+
<Pin
356+
className={`h-7 w-7 ${pinned ? "fill-[#A78BFA]" : ""} stroke-gray-700 dark:stroke-white`}
357+
/>
358+
</button>
359+
</div>
335360
</div>
361+
{relatedSubjects.length > 0 && (
362+
<div className="mt-3 flex flex-wrap items-center gap-2">
363+
<span className="text-sm font-medium text-gray-500 dark:text-gray-300 mr-2">Related subjects:</span>
364+
{relatedSubjects.map((sub) => (
365+
<Link
366+
key={sub}
367+
href={`/catalogue?subject=${encodeURIComponent(sub)}`}
368+
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"
369+
>
370+
{sub}
371+
</Link>
372+
))}
373+
</div>
374+
)}
336375
</div>
337376

338377
{loading ? (

src/db/relatedSubjects.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import mongoose, { Schema, type Model } from "mongoose";
2+
import { type IRelatedSubject } from "@/interface";
3+
4+
5+
const relatedSubjectSchema = new Schema<IRelatedSubject>({
6+
subject: { type: String, required: true },
7+
related_subjects: { type: [String], required: true },
8+
});
9+
10+
const RelatedSubject: Model<IRelatedSubject> =
11+
mongoose.models.RelatedSubject ??
12+
mongoose.model<IRelatedSubject>("RelatedSubject", relatedSubjectSchema);
13+
14+
export default RelatedSubject;

src/interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,9 @@ export interface TransformedPaper {
209209
subject: string;
210210
slots: string[];
211211
}
212+
213+
214+
export interface IRelatedSubject {
215+
subject: string;
216+
related_subjects: string[];
217+
}

src/lib/mongoose.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
11
import mongoose from "mongoose";
2-
3-
if (!process.env.MONGODB_URI) {
4-
throw new Error("Please add your Mongo URI to .env.local");
2+
declare global {
3+
// eslint-disable-next-line no-var
4+
var mongoose: { conn: mongoose.Mongoose | null; promise: Promise<mongoose.Mongoose> | null } | undefined; // This must be a `var` and not a `let / const`
55
}
66

7-
const uri = process.env.MONGODB_URI;
7+
let cached = global.mongoose;
88

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

11-
export const connectToDatabase = async () => {
12-
if (isConnected) {
13-
return;
14-
}
11+
export async function connectToDatabase() {
12+
const MONGODB_URI = process.env.MONGODB_URI!;
1513

16-
if (mongoose.connection.readyState === mongoose.ConnectionStates.connected) {
17-
isConnected = true;
18-
return;
14+
if (!MONGODB_URI) {
15+
throw new Error(
16+
"Please define the MONGODB_URI environment variable inside .env.local",
17+
);
1918
}
2019

20+
if (cached?.conn) {
21+
return cached.conn;
22+
}
23+
if (cached && !cached.promise) {
24+
const opts = {
25+
bufferCommands: false,
26+
};
27+
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
28+
return mongoose;
29+
});
30+
}
2131
try {
22-
await mongoose.connect(uri);
23-
isConnected = true;
24-
} catch (error) {
25-
throw new Error("Failed to connect to MongoDB");
32+
cached!.conn = await cached!.promise;
33+
} catch (e) {
34+
if (cached) {
35+
cached.promise = null;
36+
}
37+
throw e;
2638
}
27-
};
39+
40+
return cached?.conn;
41+
}

0 commit comments

Comments
 (0)