Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- "Reportar un bug" (rail y perfil) dejó de apuntar a GitHub y ahora abre el
-- mismo widget de Feedback que discrepancia/confuso/idea/elogio. Necesita su
-- propio valor de kind para no mezclarse con "other".
--
-- ALTER TYPE ... ADD VALUE es seguro (idempotente con IF NOT EXISTS) y el
-- valor nuevo no se usa en esta misma migración.
ALTER TYPE "FeedbackKind" ADD VALUE IF NOT EXISTS 'bug';
7 changes: 7 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,13 @@ enum FeedbackKind {
* contenido y la clase real, o "esta fue una mala unidad/lección".
*/
discrepancy
/**
* Error técnico GENERAL, no atado a un paso/ejercicio concreto (para eso
* está `BugReport`): "no puedo iniciar sesión", "esta página no carga".
* Es el destino del link "Reportar un bug" del rail y del perfil — antes
* apuntaba a GitHub, que la mayoría de los alumnos no sabe usar.
*/
bug
/**
* "no entendí esto"
*/
Expand Down
16 changes: 6 additions & 10 deletions src/app/app/(global)/perfil/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { AchievementsSection } from "@/features/profile/components/achievements-
import { ChangePasswordDialog } from "@/features/profile/components/change-password-dialog";
import { DeleteAccountDialog } from "@/features/profile/components/delete-account-dialog";
import { SignOutButton } from "@/features/profile/components/sign-out-button";
import { ReportBugButton } from "@/features/feedback/components/report-bug-button";

export const metadata = {
title: "Mi perfil",
Expand Down Expand Up @@ -198,16 +199,11 @@ export default async function PerfilPage() {
</section>

<p className="mt-8 text-[14px] text-muted-foreground">
¿Encontraste un bug?{" "}
<a
href="https://github.com/CesarManzoCode/cpp-ceti/issues"
target="_blank"
rel="noreferrer noopener"
className="font-semibold text-primary underline decoration-primary/35 decoration-2 underline-offset-4 hover:decoration-primary"
>
Repórtalo en GitHub
</a>
.
¿Encontraste un bug o algo que no corresponde con tu clase?{" "}
<ReportBugButton className="font-semibold text-primary underline decoration-primary/35 decoration-2 underline-offset-4 hover:decoration-primary">
Repórtalo aquí
</ReportBugButton>{" "}
— no hace falta GitHub.
</p>
</div>
);
Expand Down
17 changes: 5 additions & 12 deletions src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Link from "next/link";
import { Bug } from "lucide-react";

import { Logo } from "@/components/shared/logo";
import { UNOFFICIAL_NOTICE } from "@/lib/branding";
import { SidebarNav } from "@/components/layout/sidebar-nav";
import type { CourseSwitcherItem } from "@/features/courses/components/course-switcher";
import { FeedbackRailButton } from "@/features/feedback/components/feedback-rail-button";
import { ReportBugRailRow } from "@/features/feedback/components/report-bug-button";
import type { RoadmapUnit } from "@/features/roadmap/types";

/**
Expand Down Expand Up @@ -52,18 +52,11 @@ export function Sidebar({

<div className="shrink-0 space-y-1.5 border-t border-border px-6 py-3">
{/* Feedback general de la experiencia. Para contenido roto está el
botón de reporte dentro de la propia lección/ejercicio. */}
botón de reporte dentro de la propia lección/ejercicio. Ninguno
de los dos manda a GitHub: la mayoría de los alumnos no sabe
qué es un issue y no lo abriría. */}
<FeedbackRailButton />
<a
href="https://github.com/CesarManzoCode/cpp-ceti/issues"
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2 text-[13px] font-medium text-subtle-foreground transition-colors hover:text-foreground"
>
<Bug className="size-4" aria-hidden />
Reportar un bug
<span className="ml-auto tabular-nums">v0.1</span>
</a>
<ReportBugRailRow />
<p className="pt-1 text-[12px] leading-snug text-subtle-foreground">
{UNOFFICIAL_NOTICE}
</p>
Expand Down
21 changes: 20 additions & 1 deletion src/components/layout/user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from "react";
import { useRouter } from "next/navigation";
import {
Bug,
Dumbbell,
LogOut,
MessageSquarePlus,
Expand Down Expand Up @@ -56,6 +57,9 @@ export function UserMenu({
}: UserMenuProps) {
const router = useRouter();
const [feedbackOpen, setFeedbackOpen] = React.useState(false);
const [feedbackKind, setFeedbackKind] = React.useState<"confusing" | "bug">(
"confusing",
);
const initials = user.name
.split(" ")
.map((n) => n[0])
Expand Down Expand Up @@ -154,12 +158,23 @@ export function UserMenu({
// El menú se cierra al elegir; abrimos el diálogo aparte para que
// no se desmonte con él.
event.preventDefault();
setFeedbackKind("confusing");
setFeedbackOpen(true);
}}
>
<MessageSquarePlus className="size-4" />
Enviar comentario
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
setFeedbackKind("bug");
setFeedbackOpen(true);
}}
>
<Bug className="size-4" />
Reportar un bug
</DropdownMenuItem>
{isAdmin ? (
<DropdownMenuItem onClick={() => router.push("/app/admin")}>
<ShieldCheck className="size-4" />
Expand All @@ -172,7 +187,11 @@ export function UserMenu({
Cerrar sesión
</DropdownMenuItem>
</DropdownMenuContent>
<FeedbackDialog open={feedbackOpen} onOpenChange={setFeedbackOpen} />
<FeedbackDialog
open={feedbackOpen}
onOpenChange={setFeedbackOpen}
defaultKind={feedbackKind}
/>
</DropdownMenu>
);
}
5 changes: 4 additions & 1 deletion src/features/admin/components/triage-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const STATUSES: { value: ReportStatusInput; label: string }[] = [

const FEEDBACK_KIND_LABEL: Record<string, string> = {
discrepancy: "Discrepancia con clase",
bug: "Error técnico (general)",
confusing: "Confuso",
idea: "Idea",
praise: "Elogio",
Expand Down Expand Up @@ -79,7 +80,9 @@ export function TriageRow({ item }: { item: TriageItem }) {
<header className="flex flex-wrap items-center gap-2">
<Badge
variant={
item.kind === "bug" || item.feedbackKind === "discrepancy"
item.kind === "bug" ||
item.feedbackKind === "discrepancy" ||
item.feedbackKind === "bug"
? "destructive"
: "info"
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/bug-reports/components/report-bug-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function ReportBugDialog({ target, children }: ReportBugDialogProps) {
{children ?? (
<Button
type="button"
variant="ghost"
variant="outline"
size="icon-sm"
aria-label="Reportar problema en este contenido"
title="Reportar problema"
Expand Down
4 changes: 2 additions & 2 deletions src/features/feedback/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { FEEDBACK_MAX_LENGTH, resolveFeedbackContext } from "./context";
* (`/app/admin/reportes`) para que un admin lo normalice si vale la pena.
*/
const feedbackSchema = z.object({
kind: z.enum(["discrepancy", "confusing", "idea", "praise", "other"]),
kind: z.enum(["discrepancy", "bug", "confusing", "idea", "praise", "other"]),
message: z
.string()
.trim()
Expand All @@ -42,7 +42,7 @@ const feedbackSchema = z.object({
export const submitFeedback = withActionErrorHandling(
"submitFeedback",
async (input: {
kind: "discrepancy" | "confusing" | "idea" | "praise" | "other";
kind: "discrepancy" | "bug" | "confusing" | "idea" | "praise" | "other";
message: string;
path?: string;
}): Promise<{ ok: true }> => {
Expand Down
31 changes: 21 additions & 10 deletions src/features/feedback/components/feedback-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react";
import { usePathname } from "next/navigation";
import {
AlertTriangle,
Bug,
Lightbulb,
MessageSquarePlus,
Send,
Expand All @@ -27,14 +28,15 @@ import { submitFeedback } from "@/features/feedback/actions";
import { FEEDBACK_MAX_LENGTH } from "@/features/feedback/context";
import { cn } from "@/lib/utils";

type Kind = "discrepancy" | "confusing" | "idea" | "praise";
type Kind = "discrepancy" | "bug" | "confusing" | "idea" | "praise";

const KINDS: { value: Kind; label: string; icon: typeof HelpCircle }[] = [
{
value: "discrepancy",
label: "No corresponde con mi clase",
icon: AlertTriangle,
},
{ value: "bug", label: "Encontré un error técnico", icon: Bug },
{ value: "confusing", label: "Algo me confundió", icon: HelpCircle },
{ value: "idea", label: "Tengo una idea", icon: Lightbulb },
{ value: "praise", label: "Algo me gustó", icon: Sparkles },
Expand All @@ -45,11 +47,18 @@ const COPY: Record<
{ title: string; description: string; placeholder: string }
> = {
discrepancy: {
title: "Reportar una discrepancia",
title: "Reportar una discrepancia con tu clase",
description:
'¿Esta lección o unidad no corresponde con lo que viste en tu clase, o te pareció una mala sesión? Cuéntanos qué no cuadra — ya sabemos en qué pantalla estás, no hace falta que lo expliques.',
"¿Esta lección o unidad no corresponde con lo que tu profe está dando en clase, con el orden de tu plantel, o con el temario oficial? Es especialmente útil en semestres avanzados, donde casi no hay referencia práctica más allá del temario — y no todos los planteles lo siguen igual. Cuéntanos qué no cuadra; ya sabemos en qué pantalla estás.",
placeholder:
'Ej: "En mi clase vimos primero los arreglos y aquí aparecen hasta después de POO."',
'Ej: "En mi plantel ya vimos punteros antes que arreglos, y aquí van después."',
},
bug: {
title: "Reportar un error técnico",
description:
"¿Algo no carga, se traba, o no funciona como debería? No hace falta GitHub ni saber qué es un issue: cuéntanos qué pasó y lo revisamos. Ya sabemos en qué pantalla estás.",
placeholder:
'Ej: "No puedo iniciar sesión con Google, se queda cargando."',
},
confusing: {
title: "¿Cómo te está yendo?",
Expand All @@ -73,14 +82,16 @@ const COPY: Record<

/**
* Feedback general sobre la experiencia — incluye reportar que el contenido
* NO corresponde con la clase real, o que una lección/unidad fue mala. El
* contexto (dónde estaba el alumno) viaja solo: mandamos la ruta y el
* NO corresponde con la clase real (o que una lección/unidad fue mala) y
* reportar un error técnico general (login, una página que no carga, etc.).
* El contexto (dónde estaba el alumno) viaja solo: mandamos la ruta y el
* servidor la interpreta. No le pedimos que explique en qué pantalla estaba.
*
* Para contenido roto (typo, test mal configurado) existe `ReportBugDialog`,
* que sí apunta a un paso/ejercicio concreto. Ninguno de los dos crea un
* issue de GitHub: caen en la cola de triage interna (`/app/admin/reportes`)
* y un admin decide si vale la pena abrir uno.
* Para contenido roto atado a un paso/ejercicio concreto (typo, test mal
* configurado) existe `ReportBugDialog`. Ninguno de los dos crea un issue de
* GitHub — la mayoría de los alumnos no sabe qué es eso y no lo usaría: caen
* en la cola de triage interna (`/app/admin/reportes`) y un admin decide si
* vale la pena normalizarlo como issue real.
*/
export function FeedbackDialog({
children,
Expand Down
43 changes: 43 additions & 0 deletions src/features/feedback/components/report-bug-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import * as React from "react";
import { Bug } from "lucide-react";

import { FeedbackDialog } from "@/features/feedback/components/feedback-dialog";
import { cn } from "@/lib/utils";

/**
* "Reportar un bug" GENERAL (login, una página que no carga, etc.), sin
* pasar por GitHub — la mayoría de los alumnos no sabe qué es un issue y no
* lo abriría. Reemplaza el link que antes salía a
* `github.com/.../issues` desde el rail y desde el perfil.
*
* Para contenido roto atado a un paso/ejercicio concreto está
* `ReportBugDialog`, que vive dentro de la propia lección.
*/
export function ReportBugButton({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<FeedbackDialog defaultKind="bug">
<button type="button" className={cn(className)}>
{children}
</button>
</FeedbackDialog>
);
}

/** Fila lista para usar en el rail: ícono + texto, mismo look que antes. */
export function ReportBugRailRow() {
return (
<ReportBugButton className="flex w-full items-center gap-2 text-[13px] font-medium text-subtle-foreground transition-colors hover:text-foreground">
<Bug className="size-4 shrink-0" aria-hidden />
Reportar un bug
<span className="ml-auto tabular-nums">v0.1</span>
</ReportBugButton>
);
}
16 changes: 11 additions & 5 deletions src/features/feedback/components/report-discrepancy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ import { FeedbackDialog } from "@/features/feedback/components/feedback-dialog";
*
* Vive junto al `ReportBugDialog` en la cabecera de la lección y en la
* página de unidad: el alumno lo ve exactamente donde nota el problema, sin
* tener que buscarlo en un menú aparte.
* tener que buscarlo en un menú aparte. Estilo llamativo (no `ghost`) a
* propósito — un ícono apagado entre otros íconos apagados nunca se nota, y
* esto es justo lo que más nos importa saber en semestres avanzados, donde
* el temario oficial casi no tiene referencia práctica y cada plantel lo
* sigue distinto.
*/
export function ReportDiscrepancyButton() {
return (
<FeedbackDialog defaultKind="discrepancy">
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label="Reportar que esto no corresponde con tu clase"
title="¿No corresponde con tu clase?"
size="sm"
aria-label="Reportar que esto no corresponde con tu clase o tu plantel"
title="¿No corresponde con tu clase o tu plantel?"
className="gap-1.5 rounded-full border border-warning/40 bg-warning-soft px-2.5 text-warning hover:border-warning/60 hover:bg-warning-soft hover:text-warning hover:brightness-95"
>
<AlertTriangle className="size-4" />
<AlertTriangle className="size-4" aria-hidden />
<span className="hidden sm:inline">¿No es tu clase?</span>
</Button>
</FeedbackDialog>
);
Expand Down
Loading