Skip to content

Commit

Permalink
Merge pull request #1519 from AmruthPillai/feat/v4/implement-resume-l…
Browse files Browse the repository at this point in the history
…ocking

feat(resume): ✨ implement resume locking feature
  • Loading branch information
AmruthPillai authored Nov 6, 2023
2 parents 9a0402d + 015e284 commit 2d35057
Show file tree
Hide file tree
Showing 23 changed files with 289 additions and 84 deletions.
2 changes: 2 additions & 0 deletions apps/client/src/constants/sample-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,5 +489,7 @@ export const sampleResume: ResumeData = {
lineHeight: 1.5,
underlineLinks: true,
},
notes:
"<p>I sent this resume to Deloitte back in July 2022. I am yet to hear back from them.</p>",
},
};
14 changes: 11 additions & 3 deletions apps/client/src/pages/builder/_components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HouseSimple, SidebarSimple } from "@phosphor-icons/react";
import { HouseSimple, Lock, SidebarSimple } from "@phosphor-icons/react";
import { useBreakpoint } from "@reactive-resume/hooks";
import { Button } from "@reactive-resume/ui";
import { Button, Tooltip } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { Link } from "react-router-dom";

Expand All @@ -11,8 +11,10 @@ export const BuilderHeader = () => {
const { isDesktop } = useBreakpoint();
const defaultPanelSize = isDesktop ? 25 : 0;

const toggle = useBuilderStore((state) => state.toggle);
const title = useResumeStore((state) => state.resume.title);
const locked = useResumeStore((state) => state.resume.locked);

const toggle = useBuilderStore((state) => state.toggle);
const isDragging = useBuilderStore(
(state) => state.panel.left.isDragging || state.panel.right.isDragging,
);
Expand Down Expand Up @@ -48,6 +50,12 @@ export const BuilderHeader = () => {
<span className="mr-2 text-xs opacity-40">{"/"}</span>

<h1 className="font-medium">{title}</h1>

{locked && (
<Tooltip content="This resume is locked, please unlock to make further changes.">
<Lock size={14} className="ml-2 opacity-75" />
</Tooltip>
)}
</div>

<Button size="icon" variant="ghost" onClick={() => onToggle("right")}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
ArrowCounterClockwise,
Broom,
Columns,
DotsThreeVertical,
Eye,
EyeSlash,
List,
PencilSimple,
Plus,
TrashSimple,
Expand Down Expand Up @@ -55,7 +55,7 @@ export const SectionOptions = ({ id }: Props) => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<DotsThreeVertical weight="bold" />
<List weight="bold" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
Expand Down
15 changes: 15 additions & 0 deletions apps/client/src/pages/builder/sidebars/right/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ThemeSwitch } from "@/client/components/theme-switch";
import { ExportSection } from "./sections/export";
import { InformationSection } from "./sections/information";
import { LayoutSection } from "./sections/layout";
import { NotesSection } from "./sections/notes";
import { PageSection } from "./sections/page";
import { SharingSection } from "./sections/sharing";
import { StatisticsSection } from "./sections/statistics";
Expand Down Expand Up @@ -43,6 +44,8 @@ export const RightSidebar = () => {
<Separator />
<ExportSection />
<Separator />
<NotesSection />
<Separator />
<InformationSection />
<Separator />
<Copyright className="text-center" />
Expand All @@ -63,6 +66,18 @@ export const RightSidebar = () => {
<SectionIcon id="theme" name="Theme" onClick={() => scrollIntoView("#theme")} />
<SectionIcon id="page" name="Page" onClick={() => scrollIntoView("#page")} />
<SectionIcon id="sharing" name="Sharing" onClick={() => scrollIntoView("#sharing")} />
<SectionIcon
id="statistics"
name="Statistics"
onClick={() => scrollIntoView("#statistics")}
/>
<SectionIcon id="export" name="Export" onClick={() => scrollIntoView("#export")} />
<SectionIcon id="notes" name="Notes" onClick={() => scrollIntoView("#notes")} />
<SectionIcon
id="information"
name="Information"
onClick={() => scrollIntoView("#information")}
/>
</div>

<ThemeSwitch size={14} />
Expand Down
37 changes: 37 additions & 0 deletions apps/client/src/pages/builder/sidebars/right/sections/notes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RichInput } from "@reactive-resume/ui";

import { useResumeStore } from "@/client/stores/resume";

import { getSectionIcon } from "../shared/section-icon";

export const NotesSection = () => {
const setValue = useResumeStore((state) => state.setValue);
const notes = useResumeStore((state) => state.resume.data.metadata.notes);

return (
<section id="notes" className="grid gap-y-6">
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("notes")}
<h2 className="line-clamp-1 text-3xl font-bold">Notes</h2>
</div>
</header>

<main className="grid gap-y-4">
<p className="leading-relaxed">
This section is reserved for your personal notes specific to this resume. The content here
remains private and is not shared with anyone else.
</p>

<div className="space-y-1.5">
<RichInput content={notes} onChange={(content) => setValue("metadata.notes", content)} />

<p className="text-xs leading-relaxed opacity-75">
For example, information regarding which companies you sent this resume to or the links
to the job descriptions can be noted down here.
</p>
</div>
</main>
</section>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IconProps,
Info,
Layout,
Note,
Palette,
ReadCvLogo,
ShareFat,
Expand All @@ -13,6 +14,7 @@ import {
import { Button, ButtonProps, Tooltip } from "@reactive-resume/ui";

export type MetadataKey =
| "notes"
| "template"
| "layout"
| "typography"
Expand All @@ -26,6 +28,8 @@ export type MetadataKey =
export const getSectionIcon = (id: MetadataKey, props: IconProps = {}) => {
switch (id) {
// Left Sidebar
case "notes":
return <Note size={18} {...props} />;
case "template":
return <DiamondsFour size={18} {...props} />;
case "layout":
Expand Down
58 changes: 58 additions & 0 deletions apps/client/src/pages/dashboard/resumes/_dialogs/lock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ResumeDto } from "@reactive-resume/dto";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@reactive-resume/ui";

import { useLockResume } from "@/client/services/resume/lock";
import { useDialog } from "@/client/stores/dialog";

export const LockDialog = () => {
const { isOpen, mode, payload, close } = useDialog<ResumeDto>("lock");

const isLockMode = mode === "create";
const isUnlockMode = mode === "update";

const { lockResume, loading } = useLockResume();

const onSubmit = async () => {
if (!payload.item) return;

await lockResume({ id: payload.item.id, set: isLockMode });

close();
};

return (
<AlertDialog open={isOpen} onOpenChange={close}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isLockMode && "Are you sure you want to lock this resume?"}
{isUnlockMode && "Are you sure you want to unlock this resume?"}
</AlertDialogTitle>
<AlertDialogDescription>
{isLockMode &&
"Locking a resume will prevent any further changes to it. This is useful when you have already shared your resume with someone and you don't want to accidentally make any changes to it."}
{isUnlockMode && "Unlocking a resume will allow you to make changes to it again."}
</AlertDialogDescription>
</AlertDialogHeader>

<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>

<AlertDialogAction variant="info" disabled={loading} onClick={onSubmit}>
{isLockMode && "Lock"}
{isUnlockMode && "Unlock"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
CircleNotch,
CopySimple,
FolderOpen,
Lock,
LockOpen,
PencilSimple,
TrashSimple,
} from "@phosphor-icons/react";
Expand Down Expand Up @@ -30,6 +32,7 @@ type Props = {
export const ResumeCard = ({ resume }: Props) => {
const navigate = useNavigate();
const { open } = useDialog<ResumeDto>("resume");
const { open: lockOpen } = useDialog<ResumeDto>("lock");

const { url, loading } = useResumePreview(resume.id);

Expand All @@ -47,14 +50,18 @@ export const ResumeCard = ({ resume }: Props) => {
open("duplicate", { id: "resume", item: resume });
};

const onLockChange = () => {
lockOpen(resume.locked ? "update" : "create", { id: "lock", item: resume });
};

const onDelete = () => {
open("delete", { id: "resume", item: resume });
};

return (
<ContextMenu>
<ContextMenuTrigger>
<BaseCard onClick={onOpen}>
<BaseCard onClick={onOpen} className="space-y-0">
<AnimatePresence presenceAffectsLayout>
{loading && (
<motion.div
Expand Down Expand Up @@ -85,6 +92,19 @@ export const ResumeCard = ({ resume }: Props) => {
)}
</AnimatePresence>

<AnimatePresence>
{resume.locked && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center bg-background/75 backdrop-blur-sm"
>
<Lock size={42} />
</motion.div>
)}
</AnimatePresence>

<div
className={cn(
"absolute inset-x-0 bottom-0 z-10 flex flex-col justify-end space-y-0.5 p-4 pt-12",
Expand All @@ -110,6 +130,17 @@ export const ResumeCard = ({ resume }: Props) => {
<CopySimple size={14} className="mr-2" />
Duplicate
</ContextMenuItem>
{resume.locked ? (
<ContextMenuItem onClick={onLockChange}>
<LockOpen size={14} className="mr-2" />
Unlock
</ContextMenuItem>
) : (
<ContextMenuItem onClick={onLockChange}>
<Lock size={14} className="mr-2" />
Lock
</ContextMenuItem>
)}
<ContextMenuSeparator />
<ContextMenuItem onClick={onDelete} className="text-error">
<TrashSimple size={14} className="mr-2" />
Expand Down
2 changes: 2 additions & 0 deletions apps/client/src/providers/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ReferencesDialog } from "../pages/builder/sidebars/left/dialogs/referen
import { SkillsDialog } from "../pages/builder/sidebars/left/dialogs/skills";
import { VolunteerDialog } from "../pages/builder/sidebars/left/dialogs/volunteer";
import { ImportDialog } from "../pages/dashboard/resumes/_dialogs/import";
import { LockDialog } from "../pages/dashboard/resumes/_dialogs/lock";
import { ResumeDialog } from "../pages/dashboard/resumes/_dialogs/resume";
import { TwoFactorDialog } from "../pages/dashboard/settings/_dialogs/two-factor";
import { useResumeStore } from "../stores/resume";
Expand All @@ -29,6 +30,7 @@ export const DialogProvider = ({ children }: Props) => {

<div id="dialog-root">
<ResumeDialog />
<LockDialog />
<ImportDialog />
<TwoFactorDialog />

Expand Down
38 changes: 38 additions & 0 deletions apps/client/src/services/resume/lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ResumeDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";

import { axios } from "@/client/libs/axios";
import { queryClient } from "@/client/libs/query-client";

type LockResumeArgs = {
id: string;
set: boolean;
};

export const lockResume = async ({ id, set }: LockResumeArgs) => {
const response = await axios.patch(`/resume/${id}/lock`, { set });

queryClient.setQueryData<ResumeDto>(["resume", { id: response.data.id }], response.data);

queryClient.setQueryData<ResumeDto[]>(["resumes"], (cache) => {
if (!cache) return [response.data];
return cache.map((resume) => {
if (resume.id === response.data.id) return response.data;
return resume;
});
});

return response.data;
};

export const useLockResume = () => {
const {
error,
isPending: loading,
mutateAsync: lockResumeFn,
} = useMutation({
mutationFn: lockResume,
});

return { lockResume: lockResumeFn, loading, error };
};
Loading

0 comments on commit 2d35057

Please sign in to comment.