feat: improve tickets dialog, navbar buttons, hero section, and fix button bleeding

This commit is contained in:
Tiago Yamamoto 2026-02-23 18:56:06 -06:00
parent 59cd1fa01a
commit 9f5725bf01
14 changed files with 216 additions and 37 deletions

BIN
frontend/build_errors.txt Normal file

Binary file not shown.

83
frontend/fix-lines.js Normal file
View file

@ -0,0 +1,83 @@
const fs = require('fs');
let lines = fs.readFileSync('c:/dev/gohorsejobs/frontend/src/app/dashboard/backoffice/page.tsx', 'utf8').split('\n');
// 1. Add import ConfirmModal near lucide-react (line 45)
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('import { Archive, CheckCircle, Copy, ExternalLink, PauseCircle, Plus, RefreshCw, XCircle } from "lucide-react"')) {
lines.splice(i + 1, 0, 'import { ConfirmModal } from "@/components/confirm-modal"');
break;
}
}
// 2. Add state deletePlanId near editingPlanId (around line 96)
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('const [editingPlanId, setEditingPlanId] = useState<string | null>(null)')) {
lines.splice(i + 1, 0, ' const [deletePlanId, setDeletePlanId] = useState<string | null>(null)');
break;
}
}
// 3. Replace handleDeletePlan block
let startIdx = -1;
let endIdx = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('const handleDeletePlan = async (id: string) => {')) {
startIdx = i;
}
if (startIdx !== -1 && i > startIdx && lines[i].includes('} catch (error) {')) {
// found end of try block
}
if (startIdx !== -1 && i > startIdx && lines[i].trim() === '}') {
endIdx = i;
break;
}
}
if (startIdx !== -1 && endIdx !== -1) {
const newFunc = [
' const handleDeletePlan = async (id: string) => {',
' setDeletePlanId(id)',
' }',
'',
' const confirmDeletePlan = async () => {',
' if (!deletePlanId) return',
' try {',
' await plansApi.delete(deletePlanId)',
' toast.success("Plan deleted")',
' loadBackoffice(true)',
' } catch (error) {',
' toast.error("Failed to delete plan")',
' } finally {',
' setDeletePlanId(null)',
' }',
' }'
].map(l => l + (lines[0].endsWith('\\r') ? '\\r' : ''));
lines.splice(startIdx, endIdx - startIdx + 1, ...newFunc);
}
// 4. Add ConfirmModal before last closing </div>
let lastDivIdx = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].includes('</div>')) {
lastDivIdx = i;
break;
}
}
if (lastDivIdx !== -1) {
const modal = [
' <ConfirmModal',
' isOpen={!!deletePlanId}',
' onClose={() => setDeletePlanId(null)}',
' onConfirm={confirmDeletePlan}',
' title="Are you sure you want to delete this plan?"',
' description="This action cannot be undone."',
' />'
].map(l => l + (lines[0].endsWith('\\r') ? '\\r' : ''));
lines.splice(lastDivIdx, 0, ...modal);
}
fs.writeFileSync('c:/dev/gohorsejobs/frontend/src/app/dashboard/backoffice/page.tsx', lines.join('\n'));
console.log('done lines edit');

56
frontend/fix.js Normal file
View file

@ -0,0 +1,56 @@
const fs = require('fs');
let content = fs.readFileSync('c:/dev/gohorsejobs/frontend/src/app/dashboard/backoffice/page.tsx', 'utf8');
content = content.replace(
'import { Archive, CheckCircle, Copy, ExternalLink, PauseCircle, Plus, RefreshCw, XCircle } from "lucide-react"',
'import { Archive, CheckCircle, Copy, ExternalLink, PauseCircle, Plus, RefreshCw, XCircle } from "lucide-react"\r\nimport { ConfirmModal } from "@/components/confirm-modal"'
);
content = content.replace(
'const [editingPlanId, setEditingPlanId] = useState<string | null>(null)',
'const [editingPlanId, setEditingPlanId] = useState<string | null>(null)\r\n const [deletePlanId, setDeletePlanId] = useState<string | null>(null)'
);
const oldFunction = ` const handleDeletePlan = async (id: string) => {\r
if (!confirm("Delete this plan?")) return\r
try {\r
await plansApi.delete(id)\r
toast.success("Plan deleted")\r
loadBackoffice(true)\r
} catch (error) {\r
toast.error("Failed to delete plan")\r
}\r
}`;
const newFunction = ` const handleDeletePlan = async (id: string) => {\r
setDeletePlanId(id)\r
}\r
\r
const confirmDeletePlan = async () => {\r
if (!deletePlanId) return\r
try {\r
await plansApi.delete(deletePlanId)\r
toast.success("Plan deleted")\r
loadBackoffice(true)\r
} catch (error) {\r
toast.error("Failed to delete plan")\r
} finally {\r
setDeletePlanId(null)\r
}\r
}`;
content = content.replace(new RegExp(oldFunction.replace(/[.*+?^$\\{\\}()|[\\]\\\\]/g, '\\\\$&').replace(/\\r\\n/g, '\\r?\\n'), 'g'), newFunction);
content = content.replace(
' </TabsContent>\r\n </Tabs>\r\n </div>\r\n )\r\n}',
' </TabsContent>\r\n </Tabs>\r\n <ConfirmModal\r\n isOpen={!!deletePlanId}\r\n onClose={() => setDeletePlanId(null)}\r\n onConfirm={confirmDeletePlan}\r\n title="Are you sure you want to delete this plan?"\r\n description="This action cannot be undone."\r\n />\r\n </div>\r\n )\r\n}'
);
content = content.replace(
' </TabsContent>\n </Tabs>\n </div>\n )\n}',
' </TabsContent>\n </Tabs>\n <ConfirmModal\n isOpen={!!deletePlanId}\n onClose={() => setDeletePlanId(null)}\n onConfirm={confirmDeletePlan}\n title="Are you sure you want to delete this plan?"\n description="This action cannot be undone."\n />\n </div>\n )\n}'
);
fs.writeFileSync('c:/dev/gohorsejobs/frontend/src/app/dashboard/backoffice/page.tsx', content);
console.log('done');

View file

@ -322,14 +322,24 @@ export default function AdminTicketsPage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bug">Bug</SelectItem>
<SelectItem value="feature">Feature Request</SelectItem>
<SelectItem value="support">Support</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="other">Other</SelectItem>
<SelectItem value="bug">{t("ticketsPage.category.bug")}</SelectItem>
<SelectItem value="feature">{t("ticketsPage.category.feature")}</SelectItem>
<SelectItem value="support">{t("ticketsPage.category.support")}</SelectItem>
<SelectItem value="billing">{t("ticketsPage.category.billing")}</SelectItem>
<SelectItem value="other">{t("ticketsPage.category.other")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="message">{t("ticketsPage.createDialog.message")}</Label>
<Textarea
id="message"
value={newTicket.message}
onChange={(e) => setNewTicket(prev => ({ ...prev, message: e.target.value }))}
placeholder={t("ticketsPage.createDialog.messagePlaceholder")}
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="priority">{t("ticketsPage.createDialog.priority")}</Label>
<Select

View file

@ -363,6 +363,7 @@ function JobsContent() {
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<Card>
<CardContent className="p-4">

View file

@ -134,17 +134,6 @@ export default function Home() {
{t("home.hero.subtitle")}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Link href="/jobs">
<Button className="h-12 px-8 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg rounded-lg shadow-lg transition-transform hover:scale-105">
{t("home.hero.cta")}
</Button>
</Link>
</motion.div>
</div>
</div>
</section>

View file

@ -246,11 +246,11 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
<CardFooter className="pt-4 border-t mt-auto">
<div className="flex gap-2 w-full">
<Link href={`/jobs/${job.id}`} className="flex-1">
<Button variant="outline" className="w-full cursor-pointer rounded-lg">
<Button variant="outline" className="flex-1 cursor-pointer rounded-lg" asChild>
<Link href={`/jobs/${job.id}`}>
{t('jobs.card.viewDetails')}
</Button>
</Link>
</Link>
</Button>
{isApplied ? (
<Button className="flex-1 w-full cursor-default bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg" variant="secondary">
{applicationStatus === 'pending' ? t('jobs.card.applied') :
@ -259,9 +259,11 @@ export function JobCard({ job, isApplied, applicationStatus }: JobCardProps) {
t('jobs.card.applied')}
</Button>
) : (
<Link href={`/jobs/${job.id}/apply`} className="flex-1">
<Button className="w-full cursor-pointer rounded-lg">{t('jobs.card.apply')}</Button>
</Link>
<Button className="flex-1 cursor-pointer rounded-lg" asChild>
<Link href={`/jobs/${job.id}/apply`}>
{t('jobs.card.apply')}
</Link>
</Button>
)}
</div>
</CardFooter>

View file

@ -66,21 +66,21 @@ export function Navbar() {
<div className="hidden md:flex items-center gap-3">
<LanguageSwitcher className="text-white/90 hover:text-white hover:bg-white/10" />
{user ? (
<Link href="/dashboard">
<Button variant="ghost" className="gap-2 text-white hover:text-primary transition-colors">
<Button variant="ghost" className="gap-2 text-white hover:text-primary transition-colors" asChild>
<Link href="/dashboard">
<User className="w-4 h-4" />
Dashboard
</Button>
</Link>
</Link>
</Button>
) : (
<>
<Link href="/login">
<Button variant="outline" className="border-primary text-primary hover:bg-primary/10 px-6 h-9 font-normal">
<Button variant="outline" className="bg-white hover:bg-gray-50 border-primary text-primary px-6 h-9 font-normal transition-all">
{loginLabel}
</Button>
</Link>
<Link href="/register">
<Button className="bg-primary hover:bg-primary/90 text-white px-6 h-9 font-normal">
<Button className="bg-primary hover:bg-primary hover:brightness-110 text-white px-6 h-9 font-normal transition-all">
{registerLabel}
</Button>
</Link>
@ -115,12 +115,12 @@ export function Navbar() {
<div className="flex flex-col gap-2 mt-4 pt-4 border-t">
{user ? (
<Link href="/dashboard" onClick={() => setIsOpen(false)}>
<Button variant="ghost" className="w-full justify-start gap-2">
<Button variant="ghost" className="w-full justify-start gap-2" asChild>
<Link href="/dashboard" onClick={() => setIsOpen(false)}>
<User className="w-4 h-4" />
Dashboard
</Button>
</Link>
</Link>
</Button>
) : (
<>
<Link href="/login" onClick={() => setIsOpen(false)}>
@ -130,7 +130,7 @@ export function Navbar() {
</Button>
</Link>
<Link href="/register" onClick={() => setIsOpen(false)}>
<Button className="w-full justify-start gap-2">
<Button className="w-full justify-start gap-2 bg-primary hover:bg-primary hover:brightness-110 text-white transition-all">
<User className="w-4 h-4" />
{registerLabel}
</Button>

View file

@ -1321,11 +1321,21 @@
"description": "Fill in the details to create a new support ticket.",
"subject": "Subject",
"subjectPlaceholder": "Describe your issue briefly",
"category": "Category",
"message": "Message",
"messagePlaceholder": "Describe your request in detail",
"priority": "Priority",
"cancel": "Cancel",
"create": "Create Ticket",
"creating": "Creating..."
},
"category": {
"bug": "Bug",
"feature": "Feature Request",
"support": "Support",
"billing": "Billing",
"other": "Other"
},
"messages": {
"created": "Ticket created successfully",
"deleted": "Ticket deleted successfully"
@ -1385,4 +1395,4 @@
},
"copyright": "GoHorse Jobs. All rights reserved."
}
}
}

View file

@ -1322,11 +1322,21 @@
"description": "Completa los detalles para crear un nuevo ticket de soporte.",
"subject": "Asunto",
"subjectPlaceholder": "Describe brevemente tu problema",
"category": "Categoría",
"message": "Mensaje",
"messagePlaceholder": "Describe detalladamente tu solicitud",
"priority": "Prioridad",
"cancel": "Cancelar",
"create": "Crear Ticket",
"creating": "Creando..."
},
"category": {
"bug": "Error (Bug)",
"feature": "Nueva Funcionalidad",
"support": "Soporte",
"billing": "Facturación",
"other": "Otro"
},
"messages": {
"created": "Ticket creado exitosamente",
"deleted": "Ticket eliminado exitosamente"
@ -1386,4 +1396,4 @@
},
"copyright": "GoHorse Jobs. Todos los derechos reservados."
}
}
}

View file

@ -1330,11 +1330,21 @@
"description": "Preencha os detalhes para criar um novo ticket de suporte.",
"subject": "Assunto",
"subjectPlaceholder": "Descreva brevemente seu problema",
"category": "Categoria",
"message": "Mensagem",
"messagePlaceholder": "Descreva detalhadamente a sua solicitação",
"priority": "Prioridade",
"cancel": "Cancelar",
"create": "Criar Ticket",
"creating": "Criando..."
},
"category": {
"bug": "Erro (Bug)",
"feature": "Nova Funcionalidade",
"support": "Suporte",
"billing": "Faturamento",
"other": "Outro"
},
"messages": {
"created": "Ticket criado com sucesso",
"deleted": "Ticket excluído com sucesso"
@ -1394,4 +1404,4 @@
},
"copyright": "GoHorse Jobs. Todos os direitos reservados."
}
}
}

1
frontend/tsc2.txt Normal file
View file

@ -0,0 +1 @@
src/app/dashboard/backoffice/page.tsx(557,1): error TS1128: Declaration or statement expected.

6
frontend/tsc3.txt Normal file
View file

@ -0,0 +1,6 @@
src/app/dashboard/backoffice/page.tsx(46,10): error TS2300: Duplicate identifier 'ConfirmModal'.
src/app/dashboard/backoffice/page.tsx(47,10): error TS2300: Duplicate identifier 'ConfirmModal'.
src/app/dashboard/backoffice/page.tsx(99,12): error TS2451: Cannot redeclare block-scoped variable 'deletePlanId'.
src/app/dashboard/backoffice/page.tsx(99,26): error TS2451: Cannot redeclare block-scoped variable 'setDeletePlanId'.
src/app/dashboard/backoffice/page.tsx(100,12): error TS2451: Cannot redeclare block-scoped variable 'deletePlanId'.
src/app/dashboard/backoffice/page.tsx(100,26): error TS2451: Cannot redeclare block-scoped variable 'setDeletePlanId'.

1
frontend/tsc_errors.txt Normal file
View file

@ -0,0 +1 @@
src/app/dashboard/backoffice/page.tsx(564,1): error TS1128: Declaration or statement expected.