diff --git a/frontend/src/components/Prcards.tsx b/frontend/src/components/Prcards.tsx new file mode 100644 index 0000000..62d499e --- /dev/null +++ b/frontend/src/components/Prcards.tsx @@ -0,0 +1,150 @@ +import React from 'react' +import { + GlowingStarsBackgroundCard, + GlowingStarsDescription, + GlowingStarsTitle, + } from "./ui/glowing-stars"; + import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalTrigger, + } from "./ui/animated-modal"; + import { PlaceholdersAndVanishInput } from "./ui/placeholders-and-vanish-input"; + import { motion } from "framer-motion"; + +const Prcards = () => { + const placeholders = [ + "Set the bounty value for each pull request", + "Encourage open source contributors", + "Fuel innovation and excellence", + "Seamlessly send ETH to contributors", + "Recognize developers across globe", + ]; + + const handleChange = (e: React.ChangeEvent) => { + console.log(e.target.value); + }; + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log("submitted"); + }; + + const images = [ + "https://images.unsplash.com/photo-1489875347897-49f64b51c1f8?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGNvbnRyaWJ1dGlvbnMlMjBvbiUyMGdpdGh1YiUyMGNvZGV8ZW58MHx8MHx8fDA%3D", + "https://images.unsplash.com/photo-1649274496773-c40eacd66e2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZXRoZXJldW0lMjBjb2lufGVufDB8fDB8fHww", + "https://images.unsplash.com/photo-1666625519702-7270420bb4f9?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjB8fGV0aGVyZXVtJTIwd2FsbGV0fGVufDB8fDB8fHww", + "https://images.unsplash.com/photo-1618401479427-c8ef9465fbe1?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8Z2l0aHViJTIwYm91bnR5fGVufDB8fDB8fHww", + "https://images.unsplash.com/photo-1518107616985-bd48230d3b20?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fGdpdGh1YnxlbnwwfHwwfHx8MA%3D%3D", + ]; + return ( +
+ + contributor-username +
+ + contributor bio + +
+

Add bounty

+
+ +
+
+ +
+
+
+ + + + Add bounty + +
+ 🏅 +
+
+ + +

+ Set bounties that reflect {" "} + + true + {" "} + worth. 🏅 +

+
+ {images.map((image, idx) => ( + + bali images + + ))} +
+
+ + +
+
+ + + + +
+
+
+
+ ); +} + +const Icon = () => { + return ( + + + + ); + +} + +export default Prcards \ No newline at end of file diff --git a/frontend/src/components/Repositories.tsx b/frontend/src/components/Repositories.tsx index 97d6330..2ace01e 100644 --- a/frontend/src/components/Repositories.tsx +++ b/frontend/src/components/Repositories.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { PinContainer } from "./ui/3d-pin"; import { BentoGrid, BentoGridItem } from "./ui/bento-grid"; import axios from "axios"; +import Prcards from "./Prcards"; const Repositories = () => { const [repos, setRepos] = useState([]); @@ -38,6 +39,8 @@ const Repositories = () => { } +

pr cards shift to each repo pr after clicking on repo card

+ ) } diff --git a/frontend/src/components/ui/animated-modal.tsx b/frontend/src/components/ui/animated-modal.tsx new file mode 100644 index 0000000..b202a07 --- /dev/null +++ b/frontend/src/components/ui/animated-modal.tsx @@ -0,0 +1,243 @@ +"use client"; +import { cn } from "./lib/utils"; +import { AnimatePresence, motion } from "framer-motion"; +import React, { + ReactNode, + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; + +interface ModalContextType { + open: boolean; + setOpen: (open: boolean) => void; +} + +const ModalContext = createContext(undefined); + +export const ModalProvider = ({ children }: { children: ReactNode }) => { + const [open, setOpen] = useState(false); + + return ( + + {children} + + ); +}; + +export const useModal = () => { + const context = useContext(ModalContext); + if (!context) { + throw new Error("useModal must be used within a ModalProvider"); + } + return context; +}; + +export function Modal({ children }: { children: ReactNode }) { + return {children}; +} + +export const ModalTrigger = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => { + const { setOpen } = useModal(); + return ( + + ); +}; + +export const ModalBody = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => { + const { open } = useModal(); + + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + }, [open]); + + const modalRef = useRef(null); + const { setOpen } = useModal(); + useOutsideClick(modalRef, () => setOpen(false)); + + return ( + + {open && ( + + + + + + {children} + + + )} + + ); +}; + +export const ModalContent = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); +}; + +export const ModalFooter = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); +}; + +const Overlay = ({ className }: { className?: string }) => { + return ( + + ); +}; + +const CloseIcon = () => { + const { setOpen } = useModal(); + return ( + + ); +}; + +// Hook to detect clicks outside of a component. +// Add it in a separate file, I've added here for simplicity +export const useOutsideClick = ( + ref: React.RefObject, + callback: Function +) => { + useEffect(() => { + const listener = (event: any) => { + // DO NOTHING if the element being clicked is the target element or their children + if (!ref.current || ref.current.contains(event.target)) { + return; + } + callback(event); + }; + + document.addEventListener("mousedown", listener); + document.addEventListener("touchstart", listener); + + return () => { + document.removeEventListener("mousedown", listener); + document.removeEventListener("touchstart", listener); + }; + }, [ref, callback]); +}; diff --git a/frontend/src/components/ui/glowing-stars.tsx b/frontend/src/components/ui/glowing-stars.tsx new file mode 100644 index 0000000..ef623d3 --- /dev/null +++ b/frontend/src/components/ui/glowing-stars.tsx @@ -0,0 +1,158 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { cn } from "./lib/utils"; + +export const GlowingStarsBackgroundCard = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) => { + const [mouseEnter, setMouseEnter] = useState(false); + + return ( +
{ + setMouseEnter(true); + }} + onMouseLeave={() => { + setMouseEnter(false); + }} + className={cn( + "bg-[linear-gradient(110deg,#333_0.6%,#222)] p-4 max-w-md max-h-[20rem] h-full w-full rounded-xl border border-[#eaeaea] dark:border-neutral-600", + className + )} + > +
+ +
+
{children}
+
+ ); +}; + +export const GlowingStarsDescription = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) => { + return ( +

+ {children} +

+ ); +}; + +export const GlowingStarsTitle = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) => { + return ( +

+ {children} +

+ ); +}; + +export const Illustration = ({ mouseEnter }: { mouseEnter: boolean }) => { + const stars = 108; + const columns = 18; + + const [glowingStars, setGlowingStars] = useState([]); + + const highlightedStars = useRef([]); + + useEffect(() => { + const interval = setInterval(() => { + highlightedStars.current = Array.from({ length: 5 }, () => + Math.floor(Math.random() * stars) + ); + setGlowingStars([...highlightedStars.current]); + }, 3000); + + return () => clearInterval(interval); + }, []); + + return ( +
+ {[...Array(stars)].map((_, starIdx) => { + const isGlowing = glowingStars.includes(starIdx); + const delay = (starIdx % 10) * 0.1; + const staticDelay = starIdx * 0.01; + return ( +
+ + {mouseEnter && } + + {isGlowing && } + +
+ ); + })} +
+ ); +}; + +const Star = ({ isGlowing, delay }: { isGlowing: boolean; delay: number }) => { + return ( + + ); +}; + +const Glow = ({ delay }: { delay: number }) => { + return ( + + ); +}; diff --git a/frontend/src/components/ui/placeholders-and-vanish-input.tsx b/frontend/src/components/ui/placeholders-and-vanish-input.tsx new file mode 100644 index 0000000..d387fe4 --- /dev/null +++ b/frontend/src/components/ui/placeholders-and-vanish-input.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { cn } from "./lib/utils"; + +export function PlaceholdersAndVanishInput({ + placeholders, + onChange, + onSubmit, +}: { + placeholders: string[]; + onChange: (e: React.ChangeEvent) => void; + onSubmit: (e: React.FormEvent) => void; +}) { + const [currentPlaceholder, setCurrentPlaceholder] = useState(0); + + const intervalRef = useRef(null); + const startAnimation = () => { + intervalRef.current = setInterval(() => { + setCurrentPlaceholder((prev) => (prev + 1) % placeholders.length); + }, 3000); + }; + const handleVisibilityChange = () => { + if (document.visibilityState !== "visible" && intervalRef.current) { + clearInterval(intervalRef.current); // Clear the interval when the tab is not visible + intervalRef.current = null; + } else if (document.visibilityState === "visible") { + startAnimation(); // Restart the interval when the tab becomes visible + } + }; + + useEffect(() => { + startAnimation(); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [placeholders]); + + const canvasRef = useRef(null); + const newDataRef = useRef([]); + const inputRef = useRef(null); + const [value, setValue] = useState(""); + const [animating, setAnimating] = useState(false); + + const draw = useCallback(() => { + if (!inputRef.current) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + canvas.width = 800; + canvas.height = 800; + ctx.clearRect(0, 0, 800, 800); + const computedStyles = getComputedStyle(inputRef.current); + + const fontSize = parseFloat(computedStyles.getPropertyValue("font-size")); + ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`; + ctx.fillStyle = "#FFF"; + ctx.fillText(value, 16, 40); + + const imageData = ctx.getImageData(0, 0, 800, 800); + const pixelData = imageData.data; + const newData: any[] = []; + + for (let t = 0; t < 800; t++) { + let i = 4 * t * 800; + for (let n = 0; n < 800; n++) { + let e = i + 4 * n; + if ( + pixelData[e] !== 0 && + pixelData[e + 1] !== 0 && + pixelData[e + 2] !== 0 + ) { + newData.push({ + x: n, + y: t, + color: [ + pixelData[e], + pixelData[e + 1], + pixelData[e + 2], + pixelData[e + 3], + ], + }); + } + } + } + + newDataRef.current = newData.map(({ x, y, color }) => ({ + x, + y, + r: 1, + color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`, + })); + }, [value]); + + useEffect(() => { + draw(); + }, [value, draw]); + + const animate = (start: number) => { + const animateFrame = (pos: number = 0) => { + requestAnimationFrame(() => { + const newArr = []; + for (let i = 0; i < newDataRef.current.length; i++) { + const current = newDataRef.current[i]; + if (current.x < pos) { + newArr.push(current); + } else { + if (current.r <= 0) { + current.r = 0; + continue; + } + current.x += Math.random() > 0.5 ? 1 : -1; + current.y += Math.random() > 0.5 ? 1 : -1; + current.r -= 0.05 * Math.random(); + newArr.push(current); + } + } + newDataRef.current = newArr; + const ctx = canvasRef.current?.getContext("2d"); + if (ctx) { + ctx.clearRect(pos, 0, 800, 800); + newDataRef.current.forEach((t) => { + const { x: n, y: i, r: s, color: color } = t; + if (n > pos) { + ctx.beginPath(); + ctx.rect(n, i, s, s); + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.stroke(); + } + }); + } + if (newDataRef.current.length > 0) { + animateFrame(pos - 8); + } else { + setValue(""); + setAnimating(false); + } + }); + }; + animateFrame(start); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !animating) { + vanishAndSubmit(); + } + }; + + const vanishAndSubmit = () => { + setAnimating(true); + draw(); + + const value = inputRef.current?.value || ""; + if (value && inputRef.current) { + const maxX = newDataRef.current.reduce( + (prev, current) => (current.x > prev ? current.x : prev), + 0 + ); + animate(maxX); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + vanishAndSubmit(); + onSubmit && onSubmit(e); + }; + return ( +
+ + { + if (!animating) { + setValue(e.target.value); + onChange && onChange(e); + } + }} + onKeyDown={handleKeyDown} + ref={inputRef} + value={value} + type="text" + className={cn( + "w-full relative text-sm sm:text-base z-50 border-none dark:text-white bg-transparent text-black h-full rounded-full focus:outline-none focus:ring-0 pl-4 sm:pl-10 pr-20", + animating && "text-transparent dark:text-transparent" + )} + /> + + + +
+ + {!value && ( + + {placeholders[currentPlaceholder]} + + )} + +
+ + ); +}