mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-14 00:32:35 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
121
apps/client/src/components/ai-actions.tsx
Normal file
121
apps/client/src/components/ai-actions.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
CaretDown,
|
||||
ChatTeardropText,
|
||||
CircleNotch,
|
||||
Exam,
|
||||
MagicWand,
|
||||
PenNib,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
import { changeTone } from "../services/openai/change-tone";
|
||||
import { fixGrammar } from "../services/openai/fix-grammar";
|
||||
import { improveWriting } from "../services/openai/improve-writing";
|
||||
import { useOpenAiStore } from "../stores/openai";
|
||||
|
||||
type Action = "improve" | "fix" | "tone";
|
||||
type Mood = "casual" | "professional" | "confident" | "friendly";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const AiActions = ({ value, onChange, className }: Props) => {
|
||||
const [loading, setLoading] = useState<Action | false>(false);
|
||||
const aiEnabled = useOpenAiStore((state) => !!state.apiKey);
|
||||
|
||||
if (!aiEnabled) return null;
|
||||
|
||||
const onClick = async (action: Action, mood?: Mood) => {
|
||||
setLoading(action);
|
||||
let result = value;
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
if (action === "improve") result = await improveWriting(value);
|
||||
if (action === "fix") result = await fixGrammar(value);
|
||||
if (action === "tone" && mood) result = await changeTone(value, mood);
|
||||
|
||||
onChange("Result" + result);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative mt-4 rounded bg-secondary-accent/50 p-3 outline outline-secondary-accent",
|
||||
"flex flex-wrap items-center justify-center gap-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="absolute -left-5 z-10">
|
||||
<Badge
|
||||
outline
|
||||
variant="primary"
|
||||
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
|
||||
>
|
||||
<MagicWand size={10} className="mr-1" />
|
||||
AI
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
|
||||
{loading === "improve" ? <CircleNotch className="animate-spin" /> : <PenNib />}
|
||||
<span className="ml-2 text-xs">Improve Writing</span>
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
|
||||
{loading === "fix" ? <CircleNotch className="animate-spin" /> : <Exam />}
|
||||
<span className="ml-2 text-xs">Fix Spelling & Grammar</span>
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline" disabled={!!loading}>
|
||||
{loading === "tone" ? <CircleNotch className="animate-spin" /> : <ChatTeardropText />}
|
||||
<span className="mx-2 text-xs">Change Tone</span>
|
||||
<CaretDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "casual")}>
|
||||
<span role="img" aria-label="Casual">
|
||||
🙂
|
||||
</span>
|
||||
<span className="ml-2">Casual</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "professional")}>
|
||||
<span role="img" aria-label="Professional">
|
||||
💼
|
||||
</span>
|
||||
<span className="ml-2">Professional</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "confident")}>
|
||||
<span role="img" aria-label="Confident">
|
||||
😎
|
||||
</span>
|
||||
<span className="ml-2">Confident</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "friendly")}>
|
||||
<span role="img" aria-label="Friendly">
|
||||
😊
|
||||
</span>
|
||||
<span className="ml-2">Friendly</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
apps/client/src/components/copyright.tsx
Normal file
34
apps/client/src/components/copyright.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Copyright = ({ className }: Props) => (
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-sm prose-zinc flex max-w-none flex-col gap-y-1 text-xs opacity-40 dark:prose-invert",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
Licensed under{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
|
||||
>
|
||||
MIT
|
||||
</a>
|
||||
</span>
|
||||
<span>By the community, for the community.</span>
|
||||
<span>
|
||||
A passion project by{" "}
|
||||
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
|
||||
Amruth Pillai
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span className="mt-2 font-bold">Reactive Resume v{appVersion}</span>
|
||||
</div>
|
||||
);
|
||||
32
apps/client/src/components/icon.tsx
Normal file
32
apps/client/src/components/icon.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Icon = ({ size = 32, className }: Props) => {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
||||
switch (isDarkMode) {
|
||||
case false:
|
||||
src = "/icon/dark.svg";
|
||||
break;
|
||||
case true:
|
||||
src = "/icon/light.svg";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Reactive Resume"
|
||||
className={cn("rounded-sm", className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
32
apps/client/src/components/logo.tsx
Normal file
32
apps/client/src/components/logo.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Logo = ({ size = 32, className }: Props) => {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
||||
switch (isDarkMode) {
|
||||
case false:
|
||||
src = "/logo/light.svg";
|
||||
break;
|
||||
case true:
|
||||
src = "/logo/dark.svg";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Reactive Resume"
|
||||
className={cn("rounded-sm", className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
33
apps/client/src/components/theme-switch.tsx
Normal file
33
apps/client/src/components/theme-switch.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { CloudSun, Moon, Sun } from "@phosphor-icons/react";
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export const ThemeSwitch = ({ size = 20 }: Props) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const variants: Variants = useMemo(() => {
|
||||
return {
|
||||
light: { x: 0 },
|
||||
system: { x: size * -1 },
|
||||
dark: { x: size * -2 },
|
||||
};
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<Button size="icon" variant="ghost" onClick={toggleTheme}>
|
||||
<div className="cursor-pointer overflow-hidden" style={{ width: size, height: size }}>
|
||||
<motion.div animate={theme} variants={variants} className="flex">
|
||||
<Sun size={size} className="shrink-0" />
|
||||
<CloudSun size={size} className="shrink-0" />
|
||||
<Moon size={size} className="shrink-0" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
40
apps/client/src/components/user-avatar.tsx
Normal file
40
apps/client/src/components/user-avatar.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { getInitials } from "@reactive-resume/utils";
|
||||
|
||||
import { useUser } from "../services/user";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const UserAvatar = ({ size = 36, className }: Props) => {
|
||||
const { user } = useUser();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
let picture: React.ReactNode = null;
|
||||
|
||||
if (!user.picture) {
|
||||
const initials = getInitials(user.name);
|
||||
|
||||
picture = (
|
||||
<div
|
||||
style={{ width: size, height: size }}
|
||||
className="flex items-center justify-center rounded-full bg-secondary text-center text-[10px] font-semibold text-secondary-foreground"
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
picture = (
|
||||
<img
|
||||
alt={user.name}
|
||||
src={user.picture}
|
||||
className="rounded-full"
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={className}>{picture}</div>;
|
||||
};
|
||||
38
apps/client/src/components/user-options.tsx
Normal file
38
apps/client/src/components/user-options.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
KeyboardShortcut,
|
||||
} from "@reactive-resume/ui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useLogout } from "../services/auth";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const UserOptions = ({ children }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useLogout();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
|
||||
Settings
|
||||
<KeyboardShortcut>⇧S</KeyboardShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => logout()}>
|
||||
Logout
|
||||
<KeyboardShortcut>⇧Q</KeyboardShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user