mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 01:44:53 +10:00
Feature: Implement Embedding Links in Titles of all Section Items (#2662)
* feat: add options.showLinkInTitle to baseItemSchema Add itemOptionsSchema with showLinkInTitle boolean property to control whether the website URL is rendered as a hyperlink on the title instead of a separate link at the bottom. The field is optional for backwards compatibility with existing resumes. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add hideLabelButton prop to URLInput When hideLabelButton is true, the tag/label button is hidden from the URL input. This is used when showLinkInTitle is enabled since the label is not needed when the link is shown in the title. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: create LinkedTitle component for title-as-link rendering Add a reusable component that conditionally renders the title as a hyperlink when showLinkInTitle is true and a website URL is provided. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add showLinkInTitle option to experience section - Add Switch toggle in experience dialog for showLinkInTitle option - Update URLInput to hide label button when showLinkInTitle is enabled - Use LinkedTitle component in experience-item for conditional link rendering - Hide bottom website link when showLinkInTitle is enabled Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add showLinkInTitle option to all section items - Update education, projects, awards, certifications, publications, volunteer, references, and profiles dialogs with Switch toggle - Add LinkedTitle component usage in all corresponding item components - Conditionally hide bottom website link when showLinkInTitle is enabled - Add hideLabelButton prop to URLInput when showLinkInTitle is enabled Co-authored-by: Cursor <cursoragent@cursor.com> * chore: extract i18n strings for showLinkInTitle feature Add "Show link in title" translation string to all locale catalogs. Co-authored-by: Cursor <cursoragent@cursor.com> * update dependencies, fix an issue with glalie template and non-clickable links, fix better-auth type error * remove unused export --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type AwardsItemProps = SectionItem<"awards"> & {
|
||||
@@ -15,7 +16,12 @@ export function AwardsItem({ className, ...item }: AwardsItemProps) {
|
||||
<div className="section-item-header awards-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title awards-item-title">{item.title}</strong>
|
||||
<LinkedTitle
|
||||
title={item.title}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title awards-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata awards-item-date shrink-0 text-end">{item.date}</span>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +37,11 @@ export function AwardsItem({ className, ...item }: AwardsItemProps) {
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website awards-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website awards-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type CertificationsItemProps = SectionItem<"certifications"> & {
|
||||
@@ -15,7 +16,12 @@ export function CertificationsItem({ className, ...item }: CertificationsItemPro
|
||||
<div className="section-item-header certifications-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title certifications-item-title">{item.title}</strong>
|
||||
<LinkedTitle
|
||||
title={item.title}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title certifications-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata certifications-item-date shrink-0 text-end">{item.date}</span>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +42,11 @@ export function CertificationsItem({ className, ...item }: CertificationsItemPro
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website certifications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website certifications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type EducationItemProps = SectionItem<"education"> & {
|
||||
@@ -15,7 +16,12 @@ export function EducationItem({ className, ...item }: EducationItemProps) {
|
||||
<div className="section-item-header education-item-header mb-2">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title education-item-title">{item.school}</strong>
|
||||
<LinkedTitle
|
||||
title={item.school}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title education-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata education-item-degree-grade shrink-0 text-end">
|
||||
{[item.degree, item.grade].filter(Boolean).join(" • ")}
|
||||
</span>
|
||||
@@ -38,9 +44,11 @@ export function EducationItem({ className, ...item }: EducationItemProps) {
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website education-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website education-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type ExperienceItemProps = SectionItem<"experience"> & {
|
||||
@@ -15,7 +16,12 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
|
||||
<div className="section-item-header experience-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title experience-item-title">{item.company}</strong>
|
||||
<LinkedTitle
|
||||
title={item.company}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title experience-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata experience-item-location shrink-0 text-end">{item.location}</span>
|
||||
</div>
|
||||
|
||||
@@ -34,9 +40,11 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website experience-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website experience-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageIcon } from "../page-icon";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
@@ -13,15 +14,22 @@ export function ProfilesItem({ className, ...item }: ProfilesItemProps) {
|
||||
{/* Header */}
|
||||
<div className="section-item-header profiles-item-header flex items-center gap-x-1.5">
|
||||
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon" />
|
||||
<strong className="section-item-title profiles-item-network">{item.network}</strong>
|
||||
<LinkedTitle
|
||||
title={item.network}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title profiles-item-network"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label || item.username}
|
||||
className="section-item-website profiles-item-website"
|
||||
/>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label || item.username}
|
||||
className="section-item-website profiles-item-website"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type ProjectsItemProps = SectionItem<"projects"> & {
|
||||
@@ -15,7 +16,12 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
|
||||
<div className="section-item-header projects-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title projects-item-title">{item.name}</strong>
|
||||
<LinkedTitle
|
||||
title={item.name}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title projects-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata projects-item-period shrink-0 text-end">{item.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,9 +34,11 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website projects-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website projects-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type PublicationsItemProps = SectionItem<"publications"> & {
|
||||
@@ -15,7 +16,12 @@ export function PublicationsItem({ className, ...item }: PublicationsItemProps)
|
||||
<div className="section-item-header publications-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title publications-item-title">{item.title}</strong>
|
||||
<LinkedTitle
|
||||
title={item.title}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title publications-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata publications-item-date shrink-0 text-end">{item.date}</span>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +42,11 @@ export function PublicationsItem({ className, ...item }: PublicationsItemProps)
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website publications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website publications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type ReferencesItemProps = SectionItem<"references"> & {
|
||||
@@ -15,7 +16,12 @@ export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
|
||||
<div className="section-item-header references-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title references-item-name">{item.name}</strong>
|
||||
<LinkedTitle
|
||||
title={item.name}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title references-item-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
@@ -37,11 +43,13 @@ export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
|
||||
<span className="section-item-metadata references-item-phone inline-block">{item.phone}</span>
|
||||
|
||||
{/* Row 2 */}
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label}
|
||||
className="section-item-website references-item-website"
|
||||
/>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label}
|
||||
className="section-item-website references-item-website"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TiptapContent } from "@/components/input/rich-input";
|
||||
import type { SectionItem } from "@/schema/resume/data";
|
||||
import { stripHtml } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
import { LinkedTitle } from "../linked-title";
|
||||
import { PageLink } from "../page-link";
|
||||
|
||||
type VolunteerItemProps = SectionItem<"volunteer"> & {
|
||||
@@ -15,7 +16,12 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
|
||||
<div className="section-item-header volunteer-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-start justify-between gap-x-2">
|
||||
<strong className="section-item-title volunteer-item-title">{item.organization}</strong>
|
||||
<LinkedTitle
|
||||
title={item.organization}
|
||||
website={item.website}
|
||||
showLinkInTitle={item.options?.showLinkInTitle}
|
||||
className="section-item-title volunteer-item-title"
|
||||
/>
|
||||
<span className="section-item-metadata volunteer-item-period shrink-0 text-end">{item.period}</span>
|
||||
</div>
|
||||
|
||||
@@ -33,9 +39,11 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website volunteer-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
{!item.options?.showLinkInTitle && (
|
||||
<div className="section-item-website volunteer-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cn } from "@/utils/style";
|
||||
|
||||
type LinkedTitleProps = {
|
||||
title: string;
|
||||
website?: { url: string; label: string };
|
||||
showLinkInTitle?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function LinkedTitle({ title, website, showLinkInTitle, className }: LinkedTitleProps) {
|
||||
if (showLinkInTitle && website?.url) {
|
||||
return (
|
||||
<a href={website.url} target="_blank" rel="noopener" className={cn("inline-block", className)}>
|
||||
<strong>{title}</strong>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return <strong className={className}>{title}</strong>;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function ChikoritaTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
<div className="template-chikorita page-content">
|
||||
{/* Sidebar Background */}
|
||||
{!fullWidth && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:end-0 rtl:start-0" />
|
||||
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:end-0 rtl:start-0" />
|
||||
)}
|
||||
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
<div className="template-ditgar page-content">
|
||||
{/* Sidebar Background */}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
|
||||
@@ -27,7 +27,7 @@ export function GengarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
<div className="template-gengar page-content">
|
||||
{/* Sidebar Background */}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
|
||||
@@ -27,14 +27,14 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
<div className="template-glalie page-content">
|
||||
{/* Sidebar Background */}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<aside
|
||||
data-layout="sidebar"
|
||||
className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 px-(--page-margin-x) pt-(--page-margin-y)"
|
||||
className="group page-sidebar z-10 flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 px-(--page-margin-x) pt-(--page-margin-y)"
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<main data-layout="main" className="group page-main">
|
||||
<main data-layout="main" className="group page-main z-10">
|
||||
<div className="space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
|
||||
{main.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
|
||||
Reference in New Issue
Block a user