diff --git a/next.config.mjs b/next.config.mjs
index 8c8bab6..cd67477 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,6 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
+ trailingSlash: true,
+ images: {
+ domains: ["127.0.0.1", "localhost"], // Allow images from 127.0.0.1 (localhost)
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 7b7e594..6b1bc99 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@radix-ui/react-select": "^2.1.1",
"@sanity/client": "^6.21.3",
"@sanity/image-url": "^1.0.2",
+ "axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cva": "^0.0.0",
@@ -5820,8 +5821,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.20",
@@ -5887,6 +5887,17 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+ "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -6604,7 +6615,6 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -7626,7 +7636,6 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -9053,7 +9062,6 @@
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
- "peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -11988,7 +11996,6 @@
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -11998,7 +12005,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -15942,6 +15948,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
diff --git a/package.json b/package.json
index fadbe63..ddd3dca 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@radix-ui/react-select": "^2.1.1",
"@sanity/client": "^6.21.3",
"@sanity/image-url": "^1.0.2",
+ "axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cva": "^0.0.0",
diff --git a/src/app/certificates/[slug]/page.js b/src/app/certificates/[slug]/page.js
new file mode 100644
index 0000000..ef1bde9
--- /dev/null
+++ b/src/app/certificates/[slug]/page.js
@@ -0,0 +1,76 @@
+import Image from "next/image";
+import Navbar from "@/app/components/navbar";
+import About from "@/app/components/about";
+import { Raleway, JetBrains_Mono } from "next/font/google";
+import axios from "axios";
+import { notFound } from "next/navigation";
+import BlurFade from "@/app/components/ui/blur-fade";
+
+const raleway = Raleway({ subsets: ["latin"] });
+const jetbrains_mono = JetBrains_Mono({ subsets: ["latin"] });
+
+export const dynamic = "force-dynamic"; // Ensure page is always server-rendered
+
+export default async function Home({ params }) {
+ const { slug } = params;
+
+ let certificate;
+
+ try {
+ // Fetch data from the server-side API
+ const response = await axios.get(
+ `http://localhost:5000/certificates/${slug}`
+ );
+
+ if (!response.data) {
+ notFound(); // Trigger a 404 if no certificate is found
+ }
+
+ certificate = response.data; // If certificate exists, assign it
+ } catch (error) {
+ console.error("Error fetching certificate data:", error.message);
+ notFound(); // Trigger 404 if there is an error
+ }
+
+ const imageURL = `http://127.0.0.1:5000${certificate.image}`;
+ const name = certificate.name;
+
+ return (
+
+
+
+
+ ACM MPSTME
+
+
+ PFE - Participation Certificate{" "}
+
+ {certificate.subject}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/components/about.jsx b/src/app/components/about.jsx
new file mode 100644
index 0000000..832ed43
--- /dev/null
+++ b/src/app/components/about.jsx
@@ -0,0 +1,86 @@
+"use client"
+import { useState } from "react";
+import { Raleway, JetBrains_Mono } from "next/font/google";
+
+const raleway = Raleway({ subsets: ['latin']});
+const jbm = JetBrains_Mono({subsets: ['latin']});
+
+const About = ({ certificate }) => {
+ const [copySuccess, setCopySuccess] = useState(false);
+
+ const handleCopy = () => {
+ const link = window.location.href;
+
+ // Copy the link to the clipboard
+ navigator.clipboard.writeText(link)
+ .then(() => {
+ setCopySuccess(true); // Show success message
+ setTimeout(() => setCopySuccess(false), 2000); // Clear the message after 2 seconds
+ })
+ .catch((err) => {
+ console.error("Failed to copy: ", err);
+ });
+ };
+
+ const handleDownload = async () => {
+ try {
+ // Fetch the certificate image from the server as a Blob
+ const response = await fetch(`http://localhost:5000${certificate.image}`);
+ const blob = await response.blob(); // Convert the response into a Blob
+
+ // Create a URL for the Blob object
+ const url = window.URL.createObjectURL(blob);
+
+ // Create an invisible anchor element
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `ACM_PFE - ${certificate.name}_${certificate.id}.png`; // File name for the download
+
+ // Append the link to the DOM, trigger click, and remove it from the DOM
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // Revoke the object URL to free memory
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error('Error downloading the file:', error);
+ }
+ };
+
+ return (
+
+
+
+
About this Certificate
+
ID: {certificate.id} - {certificate.name}
+
+
+
+
PFE - Participation Certificate 2024
+
+ This certificate has been awarded to {certificate.name} for their 3 day continous participation in PFE (Programming For Everyone) where the students learnt the fudamentals of programming languages of their choice. In the workshop, the students learnt and were able to build a CLI RPG.
+
+
+
+
Share this certificate
+
+
+ {window.location.href}
+
+
handleCopy()} className=" text-xs py-2 md:py-0 bg-[#101010] border border-[#9046D4]/50 px-4 rounded-md text-white">{ copySuccess ? 'Copied!' : 'Copy'}
+
+
+
+
+
handleDownload()} className="transition-all duration-300 cursor-pointer text-center hover:shadow-[0_5px_20px_-10px] shadow-[0_10px_30px_-10px] hover:shadow-[#FFA7FB]/70 shadow-[#FFA7FB]/70 bg-gradient-to-b from-[#be6ee4] to-[#FFA7FB] font-bold border border-[#9046D4] text-black p-2 rounded-md">Download Certificate
+ {/*
+
+ Download Certificate
+
+ */}
+
+ )
+}
+
+export default About;
\ No newline at end of file
diff --git a/src/app/components/navbar.jsx b/src/app/components/navbar.jsx
new file mode 100644
index 0000000..39ed069
--- /dev/null
+++ b/src/app/components/navbar.jsx
@@ -0,0 +1,12 @@
+import Image from "next/image";
+
+const Navbar = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Navbar;
\ No newline at end of file
diff --git a/src/app/components/ui/blur-fade.jsx b/src/app/components/ui/blur-fade.jsx
new file mode 100644
index 0000000..d90d9b3
--- /dev/null
+++ b/src/app/components/ui/blur-fade.jsx
@@ -0,0 +1,42 @@
+"use client";;
+import { useRef } from "react";
+import { AnimatePresence, motion, useInView } from "framer-motion";
+
+export default function BlurFade({
+ children,
+ className,
+ variant,
+ duration = 0.4,
+ delay = 0,
+ yOffset = 6,
+ inView = false,
+ inViewMargin = "-50px",
+ blur = "6px"
+}) {
+ const ref = useRef(null);
+ const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
+ const isInView = !inView || inViewResult;
+ const defaultVariants = {
+ hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
+ visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
+ };
+ const combinedVariants = variant || defaultVariants;
+ return (
+ (
+
+ {children}
+
+ )
+ );
+}
diff --git a/src/app/components/ui/flickering-grid.jsx b/src/app/components/ui/flickering-grid.jsx
new file mode 100644
index 0000000..084597b
--- /dev/null
+++ b/src/app/components/ui/flickering-grid.jsx
@@ -0,0 +1,159 @@
+"use client";;
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+
+const FlickeringGrid = ({
+ squareSize = 4,
+ gridGap = 6,
+ flickerChance = 0.3,
+ color = "rgb(0, 0, 0)",
+ width,
+ height,
+ className,
+ maxOpacity = 0.3,
+}) => {
+ const canvasRef = useRef(null);
+ const [isInView, setIsInView] = useState(false);
+
+ const memoizedColor = useMemo(() => {
+ const toRGBA = (color) => {
+ if (typeof window === "undefined") {
+ return `rgba(0, 0, 0,`;
+ }
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 1;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return "rgba(255, 0,";
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
+ return `rgba(${r}, ${g}, ${b},`;
+ };
+ return toRGBA(color);
+ }, [color]);
+
+ const setupCanvas = useCallback((canvas) => {
+ const canvasWidth = width || canvas.clientWidth;
+ const canvasHeight = height || canvas.clientHeight;
+ const dpr = window.devicePixelRatio || 1;
+ canvas.width = canvasWidth * dpr;
+ canvas.height = canvasHeight * dpr;
+ canvas.style.width = `${canvasWidth}px`;
+ canvas.style.height = `${canvasHeight}px`;
+ const cols = Math.floor(canvasWidth / (squareSize + gridGap));
+ const rows = Math.floor(canvasHeight / (squareSize + gridGap));
+
+ const squares = new Float32Array(cols * rows);
+ for (let i = 0; i < squares.length; i++) {
+ squares[i] = Math.random() * maxOpacity;
+ }
+
+ return {
+ width: canvasWidth,
+ height: canvasHeight,
+ cols,
+ rows,
+ squares,
+ dpr,
+ };
+ }, [squareSize, gridGap, width, height, maxOpacity]);
+
+ const updateSquares = useCallback((squares, deltaTime) => {
+ for (let i = 0; i < squares.length; i++) {
+ if (Math.random() < flickerChance * deltaTime) {
+ squares[i] = Math.random() * maxOpacity;
+ }
+ }
+ }, [flickerChance, maxOpacity]);
+
+ const drawGrid = useCallback((
+ ctx,
+ width,
+ height,
+ cols,
+ rows,
+ squares,
+ dpr,
+ ) => {
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = "transparent";
+ ctx.fillRect(0, 0, width, height);
+
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ const opacity = squares[i * rows + j];
+ ctx.fillStyle = `${memoizedColor}${opacity})`;
+ ctx.fillRect(
+ i * (squareSize + gridGap) * dpr,
+ j * (squareSize + gridGap) * dpr,
+ squareSize * dpr,
+ squareSize * dpr
+ );
+ }
+ }
+ }, [memoizedColor, squareSize, gridGap]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ let animationFrameId;
+ let { width, height, cols, rows, squares, dpr } = setupCanvas(canvas);
+
+ let lastTime = 0;
+ const animate = (time) => {
+ if (!isInView) return;
+
+ const deltaTime = (time - lastTime) / 1000;
+ lastTime = time;
+
+ updateSquares(squares, deltaTime);
+ drawGrid(ctx, width * dpr, height * dpr, cols, rows, squares, dpr);
+ animationFrameId = requestAnimationFrame(animate);
+ };
+
+ const handleResize = () => {
+ ({ width, height, cols, rows, squares, dpr } = setupCanvas(canvas));
+ };
+
+ const observer = new IntersectionObserver(([entry]) => {
+ setIsInView(entry.isIntersecting);
+ }, { threshold: 0 });
+
+ observer.observe(canvas);
+
+ window.addEventListener("resize", handleResize);
+
+ if (isInView) {
+ animationFrameId = requestAnimationFrame(animate);
+ }
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ cancelAnimationFrame(animationFrameId);
+ observer.disconnect();
+ };
+ }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
+
+ return (
+ ( )
+ );
+};
+
+export default FlickeringGrid;
diff --git a/src/app/components/ui/number-ticker.jsx b/src/app/components/ui/number-ticker.jsx
new file mode 100644
index 0000000..4c537c8
--- /dev/null
+++ b/src/app/components/ui/number-ticker.jsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { useInView, useMotionValue, useSpring } from "framer-motion";
+
+import { cn } from "@/lib/utils";
+
+export default function NumberTicker({
+ value,
+ direction = "up",
+ delay = 0,
+ className,
+ decimalPlaces = 0
+}) {
+ const ref = useRef(null);
+ const motionValue = useMotionValue(direction === "down" ? value : 0);
+ const springValue = useSpring(motionValue, {
+ damping: 60,
+ stiffness: 100,
+ });
+ const isInView = useInView(ref, { once: true, margin: "0px" });
+
+ useEffect(() => {
+ isInView &&
+ setTimeout(() => {
+ motionValue.set(direction === "down" ? 0 : value);
+ }, delay * 1000);
+ }, [motionValue, isInView, delay, value, direction]);
+
+ useEffect(() =>
+ springValue.on("change", (latest) => {
+ if (ref.current) {
+ ref.current.textContent = Intl.NumberFormat("en-US", {
+ minimumFractionDigits: decimalPlaces,
+ maximumFractionDigits: decimalPlaces,
+ }).format(Number(latest.toFixed(decimalPlaces)));
+ }
+ }), [springValue, decimalPlaces]);
+
+ return (
+ ( )
+ );
+}