From 42519f3890717c5b598bdb0e4c7bfd812853d864 Mon Sep 17 00:00:00 2001 From: lajbel <71136486+lajbel@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:34:51 -0300 Subject: [PATCH 1/7] initial commit, start frontend --- .vscode/settings.json | 3 + package.json | 3 +- src/app/page.tsx | 5 + src/app/roast-my-setup/page.tsx | 16 ++ src/app/roast-my-setup/roaster/page.tsx | 5 + src/roast-my-setup/components/error-badge.tsx | 22 +++ .../components/feedback-email.tsx | 28 ++++ .../components/feedback-form.tsx | 137 ++++++++++++++++++ src/roast-my-setup/components/flags.tsx | 109 ++++++++++++++ src/roast-my-setup/components/layout.tsx | 17 +++ src/roast-my-setup/components/pdf.tsx | 94 ++++++++++++ src/roast-my-setup/components/score.tsx | 66 +++++++++ src/roast-my-setup/components/skeleton.tsx | 15 ++ src/roast-my-setup/hooks/form-context.tsx | 35 +++++ src/roast-my-setup/pages/index.tsx | 67 +++++++++ src/roast-my-setup/pages/privacy.tsx | 65 +++++++++ src/roast-my-setup/pages/roaster.tsx | 36 +++++ src/roast-my-setup/types.ts | 5 + src/roast-my-setup/utils.ts | 2 + 19 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 src/app/roast-my-setup/page.tsx create mode 100644 src/app/roast-my-setup/roaster/page.tsx create mode 100644 src/roast-my-setup/components/error-badge.tsx create mode 100644 src/roast-my-setup/components/feedback-email.tsx create mode 100644 src/roast-my-setup/components/feedback-form.tsx create mode 100644 src/roast-my-setup/components/flags.tsx create mode 100644 src/roast-my-setup/components/layout.tsx create mode 100644 src/roast-my-setup/components/pdf.tsx create mode 100644 src/roast-my-setup/components/score.tsx create mode 100644 src/roast-my-setup/components/skeleton.tsx create mode 100644 src/roast-my-setup/hooks/form-context.tsx create mode 100644 src/roast-my-setup/pages/index.tsx create mode 100644 src/roast-my-setup/pages/privacy.tsx create mode 100644 src/roast-my-setup/pages/roaster.tsx create mode 100644 src/roast-my-setup/types.ts create mode 100644 src/roast-my-setup/utils.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6a3b83c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "javascript.preferences.importModuleSpecifierEnding": "minimal" +} \ No newline at end of file diff --git a/package.json b/package.json index b3d8f04..d6828b1 100644 --- a/package.json +++ b/package.json @@ -85,5 +85,6 @@ "postcss": "^8", "tailwindcss": "^4", "typescript": "^5" - } + }, + "packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677" } diff --git a/src/app/page.tsx b/src/app/page.tsx index 9413aa3..a7ffa31 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -67,6 +67,11 @@ const tools: { description: "Test your speed and practice typing code.", href: "https://wpm.silver.dev", }, + { + title: "Roast My Setup", + description: "Recíbi feedback de tu setup.", + href: "/roast-my-setup", + }, ], }, { diff --git a/src/app/roast-my-setup/page.tsx b/src/app/roast-my-setup/page.tsx new file mode 100644 index 0000000..330936f --- /dev/null +++ b/src/app/roast-my-setup/page.tsx @@ -0,0 +1,16 @@ +import Home from "@/roast-my-setup/pages/index"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Roast My Setup", + description: "Submit your setup and get a roast for improvement.", + openGraph: { + title: "Roast My Setup • Open Silver", + description: "Show off your setup and get roasted for fun and improvement.", + type: "website", + }, +}; + +export default function RoastMySetupPage() { + return ; +} diff --git a/src/app/roast-my-setup/roaster/page.tsx b/src/app/roast-my-setup/roaster/page.tsx new file mode 100644 index 0000000..4615245 --- /dev/null +++ b/src/app/roast-my-setup/roaster/page.tsx @@ -0,0 +1,5 @@ +import Roaster from "@/roast-my-setup/pages/roaster"; + +export default function BehavioralCheckerPage() { + return ; +} diff --git a/src/roast-my-setup/components/error-badge.tsx b/src/roast-my-setup/components/error-badge.tsx new file mode 100644 index 0000000..184f771 --- /dev/null +++ b/src/roast-my-setup/components/error-badge.tsx @@ -0,0 +1,22 @@ +import { useEffect, useState } from "react"; + +export default function ErrorBadge({ error }: { error: Error | null }) { + const [d, set] = useState(false); + + function dismiss() { + set(true); + } + + useEffect(() => { + set(false); + }, [error]); + + return error && !d ? ( +
+

{error.message}

+ +
+ ) : null; +} diff --git a/src/roast-my-setup/components/feedback-email.tsx b/src/roast-my-setup/components/feedback-email.tsx new file mode 100644 index 0000000..24f3abd --- /dev/null +++ b/src/roast-my-setup/components/feedback-email.tsx @@ -0,0 +1,28 @@ +import { FormState } from "@/resume-checker/types"; + +export default function FeedbackEmail({ + yellow_flags, + red_flags, + grade, + description, +}: FormState & { description: string }) { + return ( +
+

Description:

+

{description}

+

Score: {grade}

+

Yellow flags:

+
    + {yellow_flags.map((flag) => ( +
  • {flag}
  • + ))} +
+

Red flags:

+
    + {red_flags.map((flag) => ( +
  • {flag}
  • + ))} +
+
+ ); +} diff --git a/src/roast-my-setup/components/feedback-form.tsx b/src/roast-my-setup/components/feedback-form.tsx new file mode 100644 index 0000000..24280f9 --- /dev/null +++ b/src/roast-my-setup/components/feedback-form.tsx @@ -0,0 +1,137 @@ +import { Button } from "@/components/ui/button"; +import { useFormState } from "@/resume-checker/hooks/form-context"; +import { FormState } from "@/resume-checker/types"; +import { useMutation } from "@tanstack/react-query"; +import { Dispatch, FormEvent, SetStateAction, useState } from "react"; + +export default function FeedbackForm({ + data, + isFeedbackFormOpen, + setFeedbackFormOpen, +}: { + data: FormState; + isFeedbackFormOpen: boolean; + setFeedbackFormOpen: Dispatch>; +}) { + const [formState] = useFormState(); + const [hasConsented, setHasConsented] = useState(false); + + const feedbackMutation = useMutation({ + mutationKey: ["feedback"], + mutationFn: async ({ formData }) => { + if (!hasConsented) + throw new Error( + "Necesitamos tu consentimiento para poder enviar el feedback." + ); + const res = await fetch("/api/feedback", { + method: "POST", + body: formData, + }).then((blob) => blob.json()); + + if (!res.success) { + throw new Error(res.message); + } + }, + onSuccess: () => { + setFeedbackFormOpen(false); + }, + }); + + function handleFeedbackSubmission(event: FormEvent) { + event.preventDefault(); + const form = event.currentTarget; + if (!form || !(form instanceof HTMLFormElement)) return; + + const file = formState.formData?.get("resume"); + const formData = new FormData(form); + + if (file) { + formData.set("resume", file); + } + + feedbackMutation.mutate({ formData }, { onSuccess: form.reset }); + } + + function close() { + setFeedbackFormOpen(false); + } + + return ( +
+
e.stopPropagation()} + className={`fixed transition-transform left-0 top-0 h-full w-full sm:h-max sm:rounded-lg sm:-translate-x-1/2 sm:-translate-y-1/2 sm:top-1/2 sm:left-1/2 bg-background p-10 shadow-lg flex justify-between flex-col sm:max-w-xl`} + > +
+ + {feedbackMutation.error ? ( +

+ {feedbackMutation.error.message} +

+ ) : null} +
+ + + + + +

+ Gracias por tu feedback +

+
+ +
+ + setHasConsented(e.target.checked)} + /> +
+ +
+
+
+ ); +} diff --git a/src/roast-my-setup/components/flags.tsx b/src/roast-my-setup/components/flags.tsx new file mode 100644 index 0000000..db638bd --- /dev/null +++ b/src/roast-my-setup/components/flags.tsx @@ -0,0 +1,109 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { FaFlag } from "react-icons/fa"; +import Markdown from "react-markdown"; +import { twMerge } from "tailwind-merge"; + +function Flag({ color }: { color: string }) { + return ( + + + + + + + + + ); +} + +export default function Flags({ + flags, + color, + label, +}: { + flags: Array; + color: string; + label: string; +}) { + const getColorStyles = (color: string) => { + const colorMap: Record< + string, + { border: string; text: string; icon: string } + > = { + red: { + border: "border-red-500", + text: "text-red-500", + icon: "text-red-500", + }, + yellow: { + border: "border-yellow-500", + text: "text-yellow-500", + icon: "text-yellow-500", + }, + green: { + border: "border-green-500", + text: "text-green-500", + icon: "text-green-500", + }, + }; + return colorMap[color.toLowerCase()] || colorMap.green; + }; + + const { border, text, icon } = getColorStyles(color); + + return ( + + + + + {label} ({flags.length}) + + + +
    + {flags.length > 0 ? ( + flags.map((flag) => ( +
  • + ( + + {children} + + ), + }} + > + {flag} + +
  • + )) + ) : ( +
  • No flags
  • + )} +
+
+
+ ); +} diff --git a/src/roast-my-setup/components/layout.tsx b/src/roast-my-setup/components/layout.tsx new file mode 100644 index 0000000..bf0f018 --- /dev/null +++ b/src/roast-my-setup/components/layout.tsx @@ -0,0 +1,17 @@ +import { Lato } from "next/font/google"; +import { type ReactNode } from "react"; +const lato = Lato({ subsets: ["latin"], weight: "400" }); + +export default function Layout({ children }: { children: ReactNode }) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/src/roast-my-setup/components/pdf.tsx b/src/roast-my-setup/components/pdf.tsx new file mode 100644 index 0000000..722626f --- /dev/null +++ b/src/roast-my-setup/components/pdf.tsx @@ -0,0 +1,94 @@ +import { useFormState } from "@/resume-checker/hooks/form-context"; +import Image from "next/image"; +import * as pdfjsLib from "pdfjs-dist"; +import { useEffect, useState } from "react"; + +pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`; + +function getUrlFromFormData(formData?: FormData) { + if (!formData) return; + const resume = formData.get("resume"); + if (resume instanceof Blob) return URL.createObjectURL(resume); +} + +function getUrlFromFormUrl(url?: string) { + if (!url) return; + if (url.startsWith("https")) return url; + return url.replace("public", ""); +} + +export default function PDF() { + const [formState] = useFormState(); + const [showFallback, setShowFallback] = useState(false); + console.log(formState); + const url = formState.formData + ? getUrlFromFormData(formState.formData) + : getUrlFromFormUrl(formState.url); + + return ( + setShowFallback(true)} + onLoad={(object) => { + // free memory + URL.revokeObjectURL((object.target as HTMLObjectElement).data); + }} + width="100%" + height="100%" + > + {showFallback ? : null} + + ); +} + +function ImageFallback({ url }: { url: string | undefined }) { + const [images, setImages] = useState>([]); + + useEffect(() => { + async function handleConversion() { + if (!url) return; + + try { + const pdf = await pdfjsLib.getDocument(url).promise; + + const imgs = []; + for (let pageNumber = 0; pageNumber < pdf.numPages; pageNumber++) { + const page = await pdf.getPage(pageNumber + 1); + const viewport = page.getViewport({ scale: 2 }); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = viewport.width; + canvas.height = viewport.height; + + await page.render({ canvasContext: ctx!, viewport }).promise; + + const imgUrl = canvas.toDataURL("image/jpeg"); + imgs.push(imgUrl); + } + + setImages(imgs); + } catch { + console.log("there was an error coverting pdf to jpeg"); + } + } + + handleConversion(); + }, [url]); + + return ( +
    + {images.map((img, idx) => ( + {`PDF + ))} +
+ ); +} diff --git a/src/roast-my-setup/components/score.tsx b/src/roast-my-setup/components/score.tsx new file mode 100644 index 0000000..27b7fc6 --- /dev/null +++ b/src/roast-my-setup/components/score.tsx @@ -0,0 +1,66 @@ +import { PreppingData } from "@/lib/utils"; +import { useEffect, useState } from "react"; + +const letterColors = { + S: "bg-gradient-to-tr from-pink-500 to-blue-500", + A: "bg-green-500/50", + B: "bg-green-400/50", + C: "bg-yellow-400/50", + " ": "bg-white/30", +} as const; + +const letterDescriptions = { + S: "El CV sigue todas las recomendaciones de Silver en formato y contenido y va a tener resultados optimos en procesos de entrevista", + A: "El CV tiene pocos desperfectos y los recruiters van a poder leer tu perfil de manera efectiva. De todas maneras mejorarlo te siempre ayuda", + B: "El CV tiene desperfectos que perjudican las chances de conseguir entrevistas y dar buenas impresiones en el equipo de contratación. Recomendamos que mejores los items demarcados", + C: "El CV va a ser descartado rapidamente en las screenings. Recomendamos que rehagas completamente desde 0 el curriculum usando nuestro template", + " ": "", +} as const; + +type Letter = keyof typeof letterColors; + +const letterKeys = Object.keys(letterColors) as Array; + +function loadingStyles(l: string) { + if (l !== " ") return ""; + + return "animate-pulse"; +} + +export default function Score({ letter }: { letter?: string }) { + const [idx, setIdx] = useState(letterKeys.length - 1); + + useEffect(() => { + const index = letterKeys.indexOf(letter as Letter); + if (index !== -1) { + PreppingData.setToolData("resume-checker", letter as string); + setIdx(index); + } + }, [letter]); + + return ( +
+
+
+ {letterKeys.map((l) => ( + + {l} + + ))} +
+
+ + {letter ?

{letterDescriptions[letter as Letter]}

: null} +
+ ); +} diff --git a/src/roast-my-setup/components/skeleton.tsx b/src/roast-my-setup/components/skeleton.tsx new file mode 100644 index 0000000..572ad07 --- /dev/null +++ b/src/roast-my-setup/components/skeleton.tsx @@ -0,0 +1,15 @@ +export default function Skeleton() { + return ( +
+
+
+
+
+ +
+
+
+
+
+ ); +} diff --git a/src/roast-my-setup/hooks/form-context.tsx b/src/roast-my-setup/hooks/form-context.tsx new file mode 100644 index 0000000..3711a94 --- /dev/null +++ b/src/roast-my-setup/hooks/form-context.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useContext, + useState, +} from "react"; + +type State = { url?: string; formData?: FormData }; +const FormContext = createContext<[State, Dispatch>]>([ + {}, + () => {}, +]); + +export function FormProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState({}); + return ( + + {children} + + ); +} + +export function useFormState() { + const ctx = useContext(FormContext); + + if (!ctx) { + throw new Error("useFormState must be used within a FormProvider"); + } + + return ctx; +} diff --git a/src/roast-my-setup/pages/index.tsx b/src/roast-my-setup/pages/index.tsx new file mode 100644 index 0000000..fa65762 --- /dev/null +++ b/src/roast-my-setup/pages/index.tsx @@ -0,0 +1,67 @@ +"use client"; + +import Description from "@/components/description"; +import Heading from "@/components/heading"; +import Section from "@/components/section"; +import Spacer from "@/components/spacer"; +import { Button } from "@/components/ui/button"; +import ErrorBadge from "@/resume-checker/components/error-badge"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +function usePasteEvent(pasteListener: (event: ClipboardEvent) => void) { + useEffect(() => { + document.addEventListener("paste", pasteListener); + + return () => { + document.removeEventListener("paste", pasteListener); + }; + }, [pasteListener]); +} + +export default function Home() { + const router = useRouter(); + const [error, setError] = useState(null); + + const requestCameraAndMicrophone = () => { + navigator.mediaDevices + .getUserMedia({ video: true, audio: true }) + .then((stream) => { + router.push("/roast-my-setup/roaster"); + }) + .catch((err) => { + setError(new Error("Access to microphone not granted.")); + }); + }; + + return ( +
+ + + Roast My{" "} + Setup + + + Get feedback from your setup. + Como te ven los demas? + +
+ +
+ + +
+ + Política de Privacidad + +
+
+ ); +} diff --git a/src/roast-my-setup/pages/privacy.tsx b/src/roast-my-setup/pages/privacy.tsx new file mode 100644 index 0000000..c5d4ded --- /dev/null +++ b/src/roast-my-setup/pages/privacy.tsx @@ -0,0 +1,65 @@ +import Heading from "@/components/heading"; +import Section from "@/components/section"; +import Spacer from "@/components/spacer"; + +export default function Privacy() { + return ( +
+ Política de Privacidad + +

+ Gracias por utilizar Roast my Setup. Su privacidad es muy importante + para nosotros, y estamos comprometidos a ser transparentes sobre cómo + manejamos sus datos. A continuación, encontrará los detalles sobre + nuestras prácticas de privacidad: +

+
    +
  • +

    + No almacenamos currículums ni información personal +

    +

    + La herramienta Roast my Setup no almacena los videos ni ninguna + información contenida en ellos en nuestros servidores ni en ningún + otro lugar. Una vez que su documento es procesado, todos los datos + se eliminan de forma inmediata. +

    +
  • +
  • +

    + Uso de tecnología de inteligencia artificial +

    +

    + Para el análisis y retroalimentación del contenido, la herramienta + utiliza Gemini AI. Este proceso se realiza de manera segura y no se + retiene ningún dato después del análisis. +

    +
  • +
  • +

    Seguridad de los datos

    +

    + Nos aseguramos de que sus datos sean procesados en un entorno + seguro. Dado que no almacenamos información, no existe riesgo de + acceso no autorizado ni de uso indebido de sus datos. +

    +
  • +
  • +

    Servicios de terceros

    +

    + La herramienta Roast My Setup utiliza [Redacted] para el análisis + del contenido y la generación de feedback. [Redacted] opera bajo sus + propias políticas de privacidad y seguridad, diseñadas para manejar + sus datos de manera responsable. +

    +
  • +
  • +

    Su consentimiento

    +

    + Al utilizar la herramienta Roast my Setup, usted acepta los términos + descritos en esta Política de Privacidad. +

    +
  • +
+
+ ); +} diff --git a/src/roast-my-setup/pages/roaster.tsx b/src/roast-my-setup/pages/roaster.tsx new file mode 100644 index 0000000..08d1ec1 --- /dev/null +++ b/src/roast-my-setup/pages/roaster.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { useEffect, useRef } from "react"; + +export default function Roaster() { + const videoRef = useRef(null); + + useEffect(() => { + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + }) + .catch((err) => { + console.error("Error al acceder a la cámara:", err); + }); + }, []); + + return ( +
+
+
+ Say "Roast my Setup" +
+ ); +} diff --git a/src/roast-my-setup/types.ts b/src/roast-my-setup/types.ts new file mode 100644 index 0000000..75db505 --- /dev/null +++ b/src/roast-my-setup/types.ts @@ -0,0 +1,5 @@ +export type FormState = { + grade: string; + red_flags: Array; + yellow_flags: Array; +}; diff --git a/src/roast-my-setup/utils.ts b/src/roast-my-setup/utils.ts new file mode 100644 index 0000000..1f2894b --- /dev/null +++ b/src/roast-my-setup/utils.ts @@ -0,0 +1,2 @@ +const TYPST_TEMPLATE_VERSION = "1.0.2"; +export const TYPST_TEMPLATE_URL = `https://typst.app/app?template=silver-dev-cv&version=${TYPST_TEMPLATE_VERSION}`; From 3f64952441683258404c3ea26b8c89712c362d97 Mon Sep 17 00:00:00 2001 From: lajbel <71136486+lajbel@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:37:16 -0300 Subject: [PATCH 2/7] remove own configurations --- .gitignore | 3 +++ .vscode/settings.json | 3 --- package.json | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 5ef6a52..ce83b01 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# vscode +.vscode/settings.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6a3b83c..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "javascript.preferences.importModuleSpecifierEnding": "minimal" -} \ No newline at end of file diff --git a/package.json b/package.json index d6828b1..b3d8f04 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,5 @@ "postcss": "^8", "tailwindcss": "^4", "typescript": "^5" - }, - "packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677" + } } From 14400224da38bdd509c82d3428d8a34ccfb921e9 Mon Sep 17 00:00:00 2001 From: lajbel <71136486+lajbel@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:39:12 -0300 Subject: [PATCH 3/7] clean unused --- .../components/feedback-email.tsx | 28 ---- .../components/feedback-form.tsx | 137 ------------------ src/roast-my-setup/components/flags.tsx | 109 -------------- src/roast-my-setup/components/layout.tsx | 17 --- src/roast-my-setup/components/pdf.tsx | 94 ------------ src/roast-my-setup/components/score.tsx | 66 --------- src/roast-my-setup/components/skeleton.tsx | 15 -- src/roast-my-setup/hooks/form-context.tsx | 35 ----- src/roast-my-setup/pages/index.tsx | 12 +- src/roast-my-setup/types.ts | 5 - src/roast-my-setup/utils.ts | 2 - 11 files changed, 1 insertion(+), 519 deletions(-) delete mode 100644 src/roast-my-setup/components/feedback-email.tsx delete mode 100644 src/roast-my-setup/components/feedback-form.tsx delete mode 100644 src/roast-my-setup/components/flags.tsx delete mode 100644 src/roast-my-setup/components/layout.tsx delete mode 100644 src/roast-my-setup/components/pdf.tsx delete mode 100644 src/roast-my-setup/components/score.tsx delete mode 100644 src/roast-my-setup/components/skeleton.tsx delete mode 100644 src/roast-my-setup/hooks/form-context.tsx delete mode 100644 src/roast-my-setup/types.ts delete mode 100644 src/roast-my-setup/utils.ts diff --git a/src/roast-my-setup/components/feedback-email.tsx b/src/roast-my-setup/components/feedback-email.tsx deleted file mode 100644 index 24f3abd..0000000 --- a/src/roast-my-setup/components/feedback-email.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FormState } from "@/resume-checker/types"; - -export default function FeedbackEmail({ - yellow_flags, - red_flags, - grade, - description, -}: FormState & { description: string }) { - return ( -
-

Description:

-

{description}

-

Score: {grade}

-

Yellow flags:

-
    - {yellow_flags.map((flag) => ( -
  • {flag}
  • - ))} -
-

Red flags:

-
    - {red_flags.map((flag) => ( -
  • {flag}
  • - ))} -
-
- ); -} diff --git a/src/roast-my-setup/components/feedback-form.tsx b/src/roast-my-setup/components/feedback-form.tsx deleted file mode 100644 index 24280f9..0000000 --- a/src/roast-my-setup/components/feedback-form.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { useFormState } from "@/resume-checker/hooks/form-context"; -import { FormState } from "@/resume-checker/types"; -import { useMutation } from "@tanstack/react-query"; -import { Dispatch, FormEvent, SetStateAction, useState } from "react"; - -export default function FeedbackForm({ - data, - isFeedbackFormOpen, - setFeedbackFormOpen, -}: { - data: FormState; - isFeedbackFormOpen: boolean; - setFeedbackFormOpen: Dispatch>; -}) { - const [formState] = useFormState(); - const [hasConsented, setHasConsented] = useState(false); - - const feedbackMutation = useMutation({ - mutationKey: ["feedback"], - mutationFn: async ({ formData }) => { - if (!hasConsented) - throw new Error( - "Necesitamos tu consentimiento para poder enviar el feedback." - ); - const res = await fetch("/api/feedback", { - method: "POST", - body: formData, - }).then((blob) => blob.json()); - - if (!res.success) { - throw new Error(res.message); - } - }, - onSuccess: () => { - setFeedbackFormOpen(false); - }, - }); - - function handleFeedbackSubmission(event: FormEvent) { - event.preventDefault(); - const form = event.currentTarget; - if (!form || !(form instanceof HTMLFormElement)) return; - - const file = formState.formData?.get("resume"); - const formData = new FormData(form); - - if (file) { - formData.set("resume", file); - } - - feedbackMutation.mutate({ formData }, { onSuccess: form.reset }); - } - - function close() { - setFeedbackFormOpen(false); - } - - return ( -
-
e.stopPropagation()} - className={`fixed transition-transform left-0 top-0 h-full w-full sm:h-max sm:rounded-lg sm:-translate-x-1/2 sm:-translate-y-1/2 sm:top-1/2 sm:left-1/2 bg-background p-10 shadow-lg flex justify-between flex-col sm:max-w-xl`} - > -
- - {feedbackMutation.error ? ( -

- {feedbackMutation.error.message} -

- ) : null} -
- - - - - -

- Gracias por tu feedback -

-
- -
- - setHasConsented(e.target.checked)} - /> -
- -
-
-
- ); -} diff --git a/src/roast-my-setup/components/flags.tsx b/src/roast-my-setup/components/flags.tsx deleted file mode 100644 index db638bd..0000000 --- a/src/roast-my-setup/components/flags.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { FaFlag } from "react-icons/fa"; -import Markdown from "react-markdown"; -import { twMerge } from "tailwind-merge"; - -function Flag({ color }: { color: string }) { - return ( - - - - - - - - - ); -} - -export default function Flags({ - flags, - color, - label, -}: { - flags: Array; - color: string; - label: string; -}) { - const getColorStyles = (color: string) => { - const colorMap: Record< - string, - { border: string; text: string; icon: string } - > = { - red: { - border: "border-red-500", - text: "text-red-500", - icon: "text-red-500", - }, - yellow: { - border: "border-yellow-500", - text: "text-yellow-500", - icon: "text-yellow-500", - }, - green: { - border: "border-green-500", - text: "text-green-500", - icon: "text-green-500", - }, - }; - return colorMap[color.toLowerCase()] || colorMap.green; - }; - - const { border, text, icon } = getColorStyles(color); - - return ( - - - - - {label} ({flags.length}) - - - -
    - {flags.length > 0 ? ( - flags.map((flag) => ( -
  • - ( - - {children} - - ), - }} - > - {flag} - -
  • - )) - ) : ( -
  • No flags
  • - )} -
-
-
- ); -} diff --git a/src/roast-my-setup/components/layout.tsx b/src/roast-my-setup/components/layout.tsx deleted file mode 100644 index bf0f018..0000000 --- a/src/roast-my-setup/components/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Lato } from "next/font/google"; -import { type ReactNode } from "react"; -const lato = Lato({ subsets: ["latin"], weight: "400" }); - -export default function Layout({ children }: { children: ReactNode }) { - return ( -
-
- {children} -
-
- ); -} diff --git a/src/roast-my-setup/components/pdf.tsx b/src/roast-my-setup/components/pdf.tsx deleted file mode 100644 index 722626f..0000000 --- a/src/roast-my-setup/components/pdf.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useFormState } from "@/resume-checker/hooks/form-context"; -import Image from "next/image"; -import * as pdfjsLib from "pdfjs-dist"; -import { useEffect, useState } from "react"; - -pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`; - -function getUrlFromFormData(formData?: FormData) { - if (!formData) return; - const resume = formData.get("resume"); - if (resume instanceof Blob) return URL.createObjectURL(resume); -} - -function getUrlFromFormUrl(url?: string) { - if (!url) return; - if (url.startsWith("https")) return url; - return url.replace("public", ""); -} - -export default function PDF() { - const [formState] = useFormState(); - const [showFallback, setShowFallback] = useState(false); - console.log(formState); - const url = formState.formData - ? getUrlFromFormData(formState.formData) - : getUrlFromFormUrl(formState.url); - - return ( - setShowFallback(true)} - onLoad={(object) => { - // free memory - URL.revokeObjectURL((object.target as HTMLObjectElement).data); - }} - width="100%" - height="100%" - > - {showFallback ? : null} - - ); -} - -function ImageFallback({ url }: { url: string | undefined }) { - const [images, setImages] = useState>([]); - - useEffect(() => { - async function handleConversion() { - if (!url) return; - - try { - const pdf = await pdfjsLib.getDocument(url).promise; - - const imgs = []; - for (let pageNumber = 0; pageNumber < pdf.numPages; pageNumber++) { - const page = await pdf.getPage(pageNumber + 1); - const viewport = page.getViewport({ scale: 2 }); - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = viewport.width; - canvas.height = viewport.height; - - await page.render({ canvasContext: ctx!, viewport }).promise; - - const imgUrl = canvas.toDataURL("image/jpeg"); - imgs.push(imgUrl); - } - - setImages(imgs); - } catch { - console.log("there was an error coverting pdf to jpeg"); - } - } - - handleConversion(); - }, [url]); - - return ( -
    - {images.map((img, idx) => ( - {`PDF - ))} -
- ); -} diff --git a/src/roast-my-setup/components/score.tsx b/src/roast-my-setup/components/score.tsx deleted file mode 100644 index 27b7fc6..0000000 --- a/src/roast-my-setup/components/score.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PreppingData } from "@/lib/utils"; -import { useEffect, useState } from "react"; - -const letterColors = { - S: "bg-gradient-to-tr from-pink-500 to-blue-500", - A: "bg-green-500/50", - B: "bg-green-400/50", - C: "bg-yellow-400/50", - " ": "bg-white/30", -} as const; - -const letterDescriptions = { - S: "El CV sigue todas las recomendaciones de Silver en formato y contenido y va a tener resultados optimos en procesos de entrevista", - A: "El CV tiene pocos desperfectos y los recruiters van a poder leer tu perfil de manera efectiva. De todas maneras mejorarlo te siempre ayuda", - B: "El CV tiene desperfectos que perjudican las chances de conseguir entrevistas y dar buenas impresiones en el equipo de contratación. Recomendamos que mejores los items demarcados", - C: "El CV va a ser descartado rapidamente en las screenings. Recomendamos que rehagas completamente desde 0 el curriculum usando nuestro template", - " ": "", -} as const; - -type Letter = keyof typeof letterColors; - -const letterKeys = Object.keys(letterColors) as Array; - -function loadingStyles(l: string) { - if (l !== " ") return ""; - - return "animate-pulse"; -} - -export default function Score({ letter }: { letter?: string }) { - const [idx, setIdx] = useState(letterKeys.length - 1); - - useEffect(() => { - const index = letterKeys.indexOf(letter as Letter); - if (index !== -1) { - PreppingData.setToolData("resume-checker", letter as string); - setIdx(index); - } - }, [letter]); - - return ( -
-
-
- {letterKeys.map((l) => ( - - {l} - - ))} -
-
- - {letter ?

{letterDescriptions[letter as Letter]}

: null} -
- ); -} diff --git a/src/roast-my-setup/components/skeleton.tsx b/src/roast-my-setup/components/skeleton.tsx deleted file mode 100644 index 572ad07..0000000 --- a/src/roast-my-setup/components/skeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function Skeleton() { - return ( -
-
-
-
-
- -
-
-
-
-
- ); -} diff --git a/src/roast-my-setup/hooks/form-context.tsx b/src/roast-my-setup/hooks/form-context.tsx deleted file mode 100644 index 3711a94..0000000 --- a/src/roast-my-setup/hooks/form-context.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { - createContext, - Dispatch, - ReactNode, - SetStateAction, - useContext, - useState, -} from "react"; - -type State = { url?: string; formData?: FormData }; -const FormContext = createContext<[State, Dispatch>]>([ - {}, - () => {}, -]); - -export function FormProvider({ children }: { children: ReactNode }) { - const [state, setState] = useState({}); - return ( - - {children} - - ); -} - -export function useFormState() { - const ctx = useContext(FormContext); - - if (!ctx) { - throw new Error("useFormState must be used within a FormProvider"); - } - - return ctx; -} diff --git a/src/roast-my-setup/pages/index.tsx b/src/roast-my-setup/pages/index.tsx index fa65762..3a077b0 100644 --- a/src/roast-my-setup/pages/index.tsx +++ b/src/roast-my-setup/pages/index.tsx @@ -8,17 +8,7 @@ import { Button } from "@/components/ui/button"; import ErrorBadge from "@/resume-checker/components/error-badge"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; - -function usePasteEvent(pasteListener: (event: ClipboardEvent) => void) { - useEffect(() => { - document.addEventListener("paste", pasteListener); - - return () => { - document.removeEventListener("paste", pasteListener); - }; - }, [pasteListener]); -} +import { useState } from "react"; export default function Home() { const router = useRouter(); diff --git a/src/roast-my-setup/types.ts b/src/roast-my-setup/types.ts deleted file mode 100644 index 75db505..0000000 --- a/src/roast-my-setup/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type FormState = { - grade: string; - red_flags: Array; - yellow_flags: Array; -}; diff --git a/src/roast-my-setup/utils.ts b/src/roast-my-setup/utils.ts deleted file mode 100644 index 1f2894b..0000000 --- a/src/roast-my-setup/utils.ts +++ /dev/null @@ -1,2 +0,0 @@ -const TYPST_TEMPLATE_VERSION = "1.0.2"; -export const TYPST_TEMPLATE_URL = `https://typst.app/app?template=silver-dev-cv&version=${TYPST_TEMPLATE_VERSION}`; From 8bc23df112e24953cff4be790b84cf16a1f9e4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pujia?= Date: Thu, 31 Jul 2025 16:43:05 -0300 Subject: [PATCH 4/7] fix: privacy policy link --- src/roast-my-setup/pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roast-my-setup/pages/index.tsx b/src/roast-my-setup/pages/index.tsx index 3a077b0..62b4293 100644 --- a/src/roast-my-setup/pages/index.tsx +++ b/src/roast-my-setup/pages/index.tsx @@ -48,7 +48,7 @@ export default function Home() {
- + Política de Privacidad
From 650d7327e685fd6054283e1c171bfd907948fdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pujia?= Date: Thu, 31 Jul 2025 16:45:06 -0300 Subject: [PATCH 5/7] fix: move privacy page to app router --- .../pages/privacy.tsx => app/roast-my-setup/privacy/page.tsx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/{roast-my-setup/pages/privacy.tsx => app/roast-my-setup/privacy/page.tsx} (95%) diff --git a/src/roast-my-setup/pages/privacy.tsx b/src/app/roast-my-setup/privacy/page.tsx similarity index 95% rename from src/roast-my-setup/pages/privacy.tsx rename to src/app/roast-my-setup/privacy/page.tsx index c5d4ded..b62234b 100644 --- a/src/roast-my-setup/pages/privacy.tsx +++ b/src/app/roast-my-setup/privacy/page.tsx @@ -2,7 +2,7 @@ import Heading from "@/components/heading"; import Section from "@/components/section"; import Spacer from "@/components/spacer"; -export default function Privacy() { +export default function Page() { return (
Política de Privacidad @@ -16,7 +16,7 @@ export default function Privacy() {
  • - No almacenamos currículums ni información personal + No almacenamos currículums {/* FIXME! */}ni información personal

    La herramienta Roast my Setup no almacena los videos ni ninguna From 4ca132dd3e6b8ed49110c53fed04ea88680bfb8f Mon Sep 17 00:00:00 2001 From: lajbel <71136486+lajbel@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:01:48 -0300 Subject: [PATCH 6/7] chore: move roaster files to app router --- src/app/roast-my-setup/page.tsx | 58 +++++++++++++++++++++++-- src/app/roast-my-setup/privacy/page.tsx | 2 +- src/app/roast-my-setup/roaster/page.tsx | 37 ++++++++++++++-- src/roast-my-setup/pages/index.tsx | 57 ------------------------ src/roast-my-setup/pages/roaster.tsx | 36 --------------- 5 files changed, 90 insertions(+), 100 deletions(-) delete mode 100644 src/roast-my-setup/pages/index.tsx delete mode 100644 src/roast-my-setup/pages/roaster.tsx diff --git a/src/app/roast-my-setup/page.tsx b/src/app/roast-my-setup/page.tsx index 330936f..8fb2fa3 100644 --- a/src/app/roast-my-setup/page.tsx +++ b/src/app/roast-my-setup/page.tsx @@ -1,5 +1,15 @@ -import Home from "@/roast-my-setup/pages/index"; +"use client"; + import { Metadata } from "next"; +import Description from "@/components/description"; +import Heading from "@/components/heading"; +import Section from "@/components/section"; +import Spacer from "@/components/spacer"; +import { Button } from "@/components/ui/button"; +import ErrorBadge from "@/resume-checker/components/error-badge"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; export const metadata: Metadata = { title: "Roast My Setup", @@ -11,6 +21,48 @@ export const metadata: Metadata = { }, }; -export default function RoastMySetupPage() { - return ; +export default function Home() { + const router = useRouter(); + const [error, setError] = useState(null); + + const requestCameraAndMicrophone = () => { + navigator.mediaDevices + .getUserMedia({ video: true, audio: true }) + .then((stream) => { + router.push("/roast-my-setup/roaster"); + }) + .catch((err) => { + setError(new Error("Access to microphone not granted.")); + }); + }; + + return ( +

    + + + Roast My{" "} + Setup + + + Get feedback from your setup. + Como te ven los demas? + +
    + +
    + + +
    + + Política de Privacidad + +
    +
    + ); } diff --git a/src/app/roast-my-setup/privacy/page.tsx b/src/app/roast-my-setup/privacy/page.tsx index b62234b..e3db649 100644 --- a/src/app/roast-my-setup/privacy/page.tsx +++ b/src/app/roast-my-setup/privacy/page.tsx @@ -16,7 +16,7 @@ export default function Page() {
    • - No almacenamos currículums {/* FIXME! */}ni información personal + No almacenamos grabaciónes, audio ni información personal

      La herramienta Roast my Setup no almacena los videos ni ninguna diff --git a/src/app/roast-my-setup/roaster/page.tsx b/src/app/roast-my-setup/roaster/page.tsx index 4615245..08d1ec1 100644 --- a/src/app/roast-my-setup/roaster/page.tsx +++ b/src/app/roast-my-setup/roaster/page.tsx @@ -1,5 +1,36 @@ -import Roaster from "@/roast-my-setup/pages/roaster"; +"use client"; -export default function BehavioralCheckerPage() { - return ; +import { Card } from "@/components/ui/card"; +import { useEffect, useRef } from "react"; + +export default function Roaster() { + const videoRef = useRef(null); + + useEffect(() => { + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + }) + .catch((err) => { + console.error("Error al acceder a la cámara:", err); + }); + }, []); + + return ( +

      +
      +
      + Say "Roast my Setup" +
      + ); } diff --git a/src/roast-my-setup/pages/index.tsx b/src/roast-my-setup/pages/index.tsx deleted file mode 100644 index 62b4293..0000000 --- a/src/roast-my-setup/pages/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import Description from "@/components/description"; -import Heading from "@/components/heading"; -import Section from "@/components/section"; -import Spacer from "@/components/spacer"; -import { Button } from "@/components/ui/button"; -import ErrorBadge from "@/resume-checker/components/error-badge"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; - -export default function Home() { - const router = useRouter(); - const [error, setError] = useState(null); - - const requestCameraAndMicrophone = () => { - navigator.mediaDevices - .getUserMedia({ video: true, audio: true }) - .then((stream) => { - router.push("/roast-my-setup/roaster"); - }) - .catch((err) => { - setError(new Error("Access to microphone not granted.")); - }); - }; - - return ( -
      - - - Roast My{" "} - Setup - - - Get feedback from your setup. - Como te ven los demas? - -
      - -
      - - -
      - - Política de Privacidad - -
      -
      - ); -} diff --git a/src/roast-my-setup/pages/roaster.tsx b/src/roast-my-setup/pages/roaster.tsx deleted file mode 100644 index 08d1ec1..0000000 --- a/src/roast-my-setup/pages/roaster.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { Card } from "@/components/ui/card"; -import { useEffect, useRef } from "react"; - -export default function Roaster() { - const videoRef = useRef(null); - - useEffect(() => { - navigator.mediaDevices - .getUserMedia({ video: true }) - .then((stream) => { - if (videoRef.current) { - videoRef.current.srcObject = stream; - } - }) - .catch((err) => { - console.error("Error al acceder a la cámara:", err); - }); - }, []); - - return ( -
      -
      -
      - Say "Roast my Setup" -
      - ); -} From 4fc47d67de2afe11ffd0160db592305a3dcb187a Mon Sep 17 00:00:00 2001 From: lajbel <71136486+lajbel@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:03:36 -0300 Subject: [PATCH 7/7] chore: move error-badge.tsx to src/components/ui --- src/app/roast-my-setup/page.tsx | 2 +- .../ui}/error-badge.tsx | 0 src/resume-checker/pages/index.tsx | 2 +- src/roast-my-setup/components/error-badge.tsx | 22 ------------------- 4 files changed, 2 insertions(+), 24 deletions(-) rename src/{resume-checker/components => components/ui}/error-badge.tsx (100%) delete mode 100644 src/roast-my-setup/components/error-badge.tsx diff --git a/src/app/roast-my-setup/page.tsx b/src/app/roast-my-setup/page.tsx index 8fb2fa3..fbb6f9d 100644 --- a/src/app/roast-my-setup/page.tsx +++ b/src/app/roast-my-setup/page.tsx @@ -6,7 +6,7 @@ import Heading from "@/components/heading"; import Section from "@/components/section"; import Spacer from "@/components/spacer"; import { Button } from "@/components/ui/button"; -import ErrorBadge from "@/resume-checker/components/error-badge"; +import ErrorBadge from "@/components/ui/error-badge"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; diff --git a/src/resume-checker/components/error-badge.tsx b/src/components/ui/error-badge.tsx similarity index 100% rename from src/resume-checker/components/error-badge.tsx rename to src/components/ui/error-badge.tsx diff --git a/src/resume-checker/pages/index.tsx b/src/resume-checker/pages/index.tsx index cd2081a..5d8da7f 100644 --- a/src/resume-checker/pages/index.tsx +++ b/src/resume-checker/pages/index.tsx @@ -6,7 +6,7 @@ import Heading from "@/components/heading"; import Section from "@/components/section"; import Spacer, { spaceSizes } from "@/components/spacer"; import { Button } from "@/components/ui/button"; -import ErrorBadge from "@/resume-checker/components/error-badge"; +import ErrorBadge from "@/components/ui/error-badge"; import { useFormState } from "@/resume-checker/hooks/form-context"; import { useMutationState } from "@tanstack/react-query"; import Link from "next/link"; diff --git a/src/roast-my-setup/components/error-badge.tsx b/src/roast-my-setup/components/error-badge.tsx deleted file mode 100644 index 184f771..0000000 --- a/src/roast-my-setup/components/error-badge.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, useState } from "react"; - -export default function ErrorBadge({ error }: { error: Error | null }) { - const [d, set] = useState(false); - - function dismiss() { - set(true); - } - - useEffect(() => { - set(false); - }, [error]); - - return error && !d ? ( -
      -

      {error.message}

      - -
      - ) : null; -}