mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-14 23:07:01 +10:00
Merge pull request #2483 from y8a5y/main
[Feature] Permalink to share/download resume json data
This commit is contained in:
@@ -82,4 +82,14 @@ git commit -m "fix(homepage): fix typo on homepage in the faq section"
|
||||
|
||||
It helps to be as descriptive as possible in commit messages so that users can be aware of the changes made by you.
|
||||
|
||||
Then, you can run the basic checks, as to make sure the CI has the most chance of being successful:
|
||||
|
||||
```
|
||||
pnpm run lint
|
||||
|
||||
pnpm run test
|
||||
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Finally, create a pull request to merge the changes on your forked repository to the original repository hosted on AmruthPillai/Reactive-Resume. I can take a look at the changes you've made when I have the time and have it merged onto the app.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label, Switch, Tooltip } from "@reactive-resume/ui";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { axios } from "@/client/libs/axios"
|
||||
import { useUser } from "@/client/services/user";
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
@@ -20,6 +21,7 @@ export const SharingSection = () => {
|
||||
|
||||
// Constants
|
||||
const url = `${window.location.origin}/${username}/${slug}`;
|
||||
const rawJsonUrl = `${window.location.origin}${axios.defaults.baseURL}/resume/public/${username}/${slug}/stealthily`;
|
||||
|
||||
const onCopy = async () => {
|
||||
await navigator.clipboard.writeText(url);
|
||||
@@ -31,6 +33,16 @@ export const SharingSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onRawJsonCopy = async () => {
|
||||
await navigator.clipboard.writeText(rawJsonUrl);
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: t`A link has been copied to your clipboard.`,
|
||||
description: t`Anyone with this link can view and download the json data of the resume. This link will not count towards the resume view count.`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="sharing" className="grid gap-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
@@ -65,21 +77,50 @@ export const SharingSection = () => {
|
||||
{isPublic && (
|
||||
<motion.div
|
||||
layout
|
||||
className="space-y-1.5"
|
||||
className="space-y-3"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<Label htmlFor="resume-url">{t`URL`}</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="resume-url">{t`URL`}</Label>
|
||||
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input readOnly id="resume-url" value={url} className="flex-1" />
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input
|
||||
readOnly
|
||||
id="resume-url"
|
||||
value={url}
|
||||
className="flex-1 caret-transparent"
|
||||
/>
|
||||
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="resume-raw-json-url" className="space-y-1">
|
||||
<p>{t`Raw JSON URL`}</p>
|
||||
<p className="text-xs opacity-60">{t`For advanced users! Does not count towards resume statistics.`}</p>
|
||||
</Label>
|
||||
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input
|
||||
readOnly
|
||||
id="resume-raw-json-url"
|
||||
value={rawJsonUrl}
|
||||
className="flex-1 caret-transparent"
|
||||
/>
|
||||
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onRawJsonCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ResumeDto,
|
||||
UpdateResumeDto,
|
||||
} from "@reactive-resume/dto";
|
||||
import { ResumeData, resumeDataSchema } from "@reactive-resume/schema";
|
||||
import { resumeDataSchema } from "@reactive-resume/schema";
|
||||
import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import set from "lodash.set";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
@@ -92,6 +92,19 @@ export class ResumeController {
|
||||
return this.resumeService.findOneStatistics(id);
|
||||
}
|
||||
|
||||
@Get("/public/:username/:slug/stealthily")
|
||||
@UseGuards(OptionalGuard)
|
||||
async findOneByUsernameSlugStealthily(
|
||||
@Param("username") username: string,
|
||||
@Param("slug") slug: string,
|
||||
) {
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug);
|
||||
|
||||
redactPrivateNotes(resume as ResumeDto);
|
||||
|
||||
return resume;
|
||||
}
|
||||
|
||||
@Get("/public/:username/:slug")
|
||||
@UseGuards(OptionalGuard)
|
||||
async findOneByUsernameSlug(
|
||||
@@ -99,10 +112,13 @@ export class ResumeController {
|
||||
@Param("slug") slug: string,
|
||||
@User("id") userId: string,
|
||||
) {
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug, userId);
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug);
|
||||
|
||||
// Hide private notes from public resume API responses
|
||||
set(resume.data as ResumeData, "metadata.notes", undefined);
|
||||
redactPrivateNotes(resume as ResumeDto);
|
||||
|
||||
if (!userId) {
|
||||
await this.resumeService.incrementViewCountForOne(resume.id);
|
||||
}
|
||||
|
||||
return resume;
|
||||
}
|
||||
@@ -155,3 +171,7 @@ export class ResumeController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function redactPrivateNotes(resume: ResumeDto) {
|
||||
set(resume.data, "metadata.notes", undefined);
|
||||
}
|
||||
|
||||
@@ -84,21 +84,19 @@ export class ResumeService {
|
||||
};
|
||||
}
|
||||
|
||||
async findOneByUsernameSlug(username: string, slug: string, userId?: string) {
|
||||
const resume = await this.prisma.resume.findFirstOrThrow({
|
||||
async findOneByUsernameSlug(username: string, slug: string) {
|
||||
return await this.prisma.resume.findFirstOrThrow({
|
||||
where: { user: { username }, slug, visibility: "public" },
|
||||
});
|
||||
}
|
||||
|
||||
async incrementViewCountForOne(resumeId: string) {
|
||||
// Update statistics: increment the number of views by 1
|
||||
if (!userId) {
|
||||
await this.prisma.statistics.upsert({
|
||||
where: { resumeId: resume.id },
|
||||
create: { views: 1, downloads: 0, resumeId: resume.id },
|
||||
update: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return resume;
|
||||
await this.prisma.statistics.upsert({
|
||||
where: { resumeId },
|
||||
create: { views: 1, downloads: 0, resumeId },
|
||||
update: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, updateResumeDto: UpdateResumeDto) {
|
||||
|
||||
Reference in New Issue
Block a user