├── public
└── favicon.ico
├── postcss.config.js
├── .gitignore
├── .eslintrc.js
├── app
├── utils.js
├── components
│ ├── Sections
│ │ ├── Photo
│ │ │ ├── PhotoPreview.jsx
│ │ │ ├── Border.jsx
│ │ │ └── UploadPhoto.jsx
│ │ ├── ColorPicker.jsx
│ │ ├── UserDetails.jsx
│ │ ├── Skills.jsx
│ │ ├── Customization.jsx
│ │ ├── Socials.jsx
│ │ ├── Education.jsx
│ │ └── Projects.jsx
│ ├── ui
│ │ ├── label.jsx
│ │ ├── separator.jsx
│ │ ├── textarea.jsx
│ │ ├── input.jsx
│ │ ├── toaster.jsx
│ │ ├── slider.jsx
│ │ ├── tooltip.jsx
│ │ ├── popover.jsx
│ │ ├── accordion.jsx
│ │ ├── button.jsx
│ │ ├── calendar.jsx
│ │ ├── use-toast.ts
│ │ └── toast.jsx
│ ├── CustomInput
│ │ └── CustomInput.jsx
│ ├── TemplatePicker
│ │ └── TemplatePicker.jsx
│ ├── TemplateRender
│ │ └── TemplateRender.jsx
│ ├── DatePicker
│ │ └── DatePicker.jsx
│ └── Templates
│ │ ├── ModernTemplate.jsx
│ │ ├── CalmTemplate.jsx
│ │ └── InitialTemplate.jsx
├── entry.client.jsx
├── tailwind.css
├── hooks
│ └── useZoom.js
├── lib
│ └── helpers.js
├── root.jsx
├── constants
│ └── general.js
├── entry.server.jsx
└── routes
│ └── _index.jsx
├── components.json
├── remix.config.js
├── tsconfig.json
├── README.md
├── LICENSE
├── package.json
├── tailwind.config.js
└── api
└── metafile.server.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RawandDev/simple-resume-builder/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | .cache
4 | .env
5 | .vercel
6 | .output
7 |
8 | /build/
9 | /public/build
10 | /api/index.js
11 | /api/index.js.map
12 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.Config} */
2 | module.exports = {
3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4 | };
5 |
--------------------------------------------------------------------------------
/app/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": false,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/tailwind.css",
9 | "baseColor": "slate",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "components": "~/components",
14 | "utils": "~/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/app/components/Sections/Photo/PhotoPreview.jsx:
--------------------------------------------------------------------------------
1 | function PhotoPreview({ selectedImage, border }) {
2 | return (
3 | selectedImage && (
4 |
12 | )
13 | );
14 | }
15 |
16 | export default PhotoPreview;
17 |
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@remix-run/dev').AppConfig} */
2 | module.exports = {
3 | ignoredRouteFiles: ["**/.*"],
4 | // appDirectory: "app",
5 | // assetsBuildDirectory: "public/build",
6 | // publicPath: "/build/",
7 | serverModuleFormat: "cjs",
8 | future: {
9 | v2_dev: true,
10 | v2_errorBoundary: true,
11 | v2_headers: true,
12 | v2_meta: true,
13 | v2_normalizeFormMethod: true,
14 | v2_routeConvention: true,
15 | },
16 | tailwind: true,
17 | postcss: true,
18 | };
19 |
--------------------------------------------------------------------------------
/app/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva } from "class-variance-authority";
4 |
5 | import { cn } from "~/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef(({ className, ...props }, ref) => (
12 |
13 | ))
14 | Label.displayName = LabelPrimitive.Root.displayName
15 |
16 | export { Label }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 |
19 | // Remix takes care of building everything in `remix build`.
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/entry.client.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle hydrating your app on the client for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.client
5 | */
6 |
7 | import { RemixBrowser } from "@remix-run/react";
8 | import { startTransition, StrictMode } from "react";
9 | import { hydrateRoot } from "react-dom/client";
10 |
11 | startTransition(() => {
12 | hydrateRoot(
13 | document,
14 |
15 |
16 |
17 | );
18 | });
19 |
--------------------------------------------------------------------------------
/app/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @media print {
6 | #container {
7 | size: a4;
8 | }
9 | }
10 |
11 | @media screen {
12 | #container {
13 | size: unset;
14 | max-width: 210mm;
15 | }
16 | }
17 |
18 | ::-webkit-scrollbar {
19 | width: 12px;
20 | }
21 |
22 | ::-webkit-scrollbar-track {
23 | background-color: rgb(220, 220, 220, 0.5);
24 | }
25 |
26 | ::-webkit-scrollbar-thumb {
27 | background-color: rgba(214, 214, 214);
28 | border-radius: 5px;
29 | }
30 |
31 | ::-webkit-scrollbar-thumb:hover {
32 | background-color: rgb(210, 210, 210);
33 | border-radius: 5px;
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/ui/separator.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "~/utils"
5 |
6 | const Separator = React.forwardRef((
7 | { className, orientation = "horizontal", decorative = true, ...props },
8 | ref
9 | ) => (
10 |
20 | ))
21 | Separator.displayName = SeparatorPrimitive.Root.displayName
22 |
23 | export { Separator }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to the resume builder
2 |
3 | Simple yet elegant CV/Resume builder. Built with Remix + Shadcn/ui + Tailwindcss.
4 |
5 | Check the website here: https://simple-resume-builder.vercel.app
6 |
7 |
8 | ## Demo
9 |
10 | Here is a quick demo about the application.
11 |
12 |
13 | https://github.com/RawandDev/simple-resume-builder/assets/82733587/61f41ae6-eef9-4dec-9719-47a6534bef18
14 |
15 |
16 |
17 |
18 |
19 | ## Development
20 |
21 | To run your Remix app locally, make sure your project's local dependencies are installed:
22 |
23 | ```sh
24 | yarn
25 | ```
26 |
27 | Afterwards, start the Remix development server like so:
28 |
29 | ```sh
30 | yarn dev
31 | ```
32 |
33 | Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
34 |
--------------------------------------------------------------------------------
/app/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 | ()
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/app/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/utils"
4 |
5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
6 | return (
7 | ( )
15 | );
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/app/hooks/useZoom.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { SCALE_VALUES } from "../constants/general";
3 |
4 | const useZoom = (initialZoom) => {
5 | const [zoom, setZoom] = useState(initialZoom);
6 |
7 | const handleZoomOut = useCallback(() => {
8 | const currentIndex = SCALE_VALUES.indexOf(zoom);
9 | if (currentIndex === 0) {
10 | return;
11 | } else {
12 | const newZoom = SCALE_VALUES[currentIndex - 1];
13 | setZoom(newZoom);
14 | }
15 | }, [zoom]);
16 |
17 | const handleZoomIn = useCallback(() => {
18 | const currentIndex = SCALE_VALUES.indexOf(zoom);
19 | if (currentIndex === SCALE_VALUES.length - 1) {
20 | return;
21 | } else {
22 | const newZoom = SCALE_VALUES[currentIndex + 1];
23 | setZoom(newZoom);
24 | }
25 | }, [zoom]);
26 |
27 | return { zoom, handleZoomIn, handleZoomOut };
28 | };
29 |
30 | export default useZoom;
31 |
--------------------------------------------------------------------------------
/app/lib/helpers.js:
--------------------------------------------------------------------------------
1 | export const getRGBColor = (hex, type) => {
2 | let color = hex.replace(/#/g, "");
3 | var r = parseInt(color.substr(0, 2), 16);
4 | var g = parseInt(color.substr(2, 2), 16);
5 | var b = parseInt(color.substr(4, 2), 16);
6 |
7 | return `--color-${type}: ${r}, ${g}, ${b};`;
8 | };
9 |
10 | export const getAccessibleColor = (hex) => {
11 | let color = hex.replace(/#/g, "");
12 | var r = parseInt(color.substr(0, 2), 16);
13 | var g = parseInt(color.substr(2, 2), 16);
14 | var b = parseInt(color.substr(4, 2), 16);
15 | var yiq = (r * 299 + g * 587 + b * 114) / 1000;
16 | return yiq >= 128 ? "#000000" : "#FFFFFF";
17 | };
18 |
19 | export function withOpacity(variableName) {
20 | return ({ opacityValue }) => {
21 | if (opacityValue !== undefined) {
22 | return `rgba(var(${variableName}), ${opacityValue})`;
23 | }
24 |
25 | return `rgb(var(${variableName}))`;
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/app/components/ui/toaster.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "~/components/ui/toast"
9 | import { useToast } from "~/components/ui/use-toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 | (
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 | (
19 |
20 | {title && {title} }
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 | )
28 | );
29 | })}
30 |
31 | )
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/root.jsx:
--------------------------------------------------------------------------------
1 | import { cssBundleHref } from "@remix-run/css-bundle";
2 | import styles from "./tailwind.css";
3 | import { Toaster } from "~/components/ui/toaster";
4 |
5 | import {
6 | Links,
7 | LiveReload,
8 | Meta,
9 | Outlet,
10 | Scripts,
11 | ScrollRestoration,
12 | } from "@remix-run/react";
13 |
14 | export const links = () => [
15 | { rel: "stylesheet", href: styles },
16 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
17 | ];
18 |
19 | export default function App() {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/CustomInput/CustomInput.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from "~/components/ui/input";
2 | import { Label } from "~/components/ui/label";
3 | import { Textarea } from "~/components/ui/textarea";
4 |
5 | function CustomInput({
6 | type,
7 | placeholder,
8 | onChangeHandler,
9 | label: { text, htmlFor } = {},
10 | name,
11 | id,
12 | value,
13 | }) {
14 | return (
15 |
16 | {text && {text} }
17 | {type === "textarea" ? (
18 |
25 | ) : (
26 |
34 | )}
35 |
36 | );
37 | }
38 |
39 | export default CustomInput;
40 |
--------------------------------------------------------------------------------
/app/components/Sections/ColorPicker.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "~/components/ui/button";
2 | import { COLOR_VARIANTS } from "../../constants/general";
3 | import { CheckIcon } from "lucide-react";
4 |
5 | function ColorPicker({ selectedColor, setSelectedColor }) {
6 | return (
7 |
8 | Template Color
9 |
10 | {COLOR_VARIANTS.map((color) => {
11 | return (
12 | {
16 | setSelectedColor(color.hexCode);
17 | }}
18 | className={`rounded-full w-8 h-8 ${color.twColor} hover:${color.twColor} hover:bg-opacity-70`}
19 | size="icon"
20 | >
21 | {selectedColor === color.hexCode && }
22 |
23 | );
24 | })}
25 |
26 |
27 | );
28 | }
29 |
30 | export default ColorPicker;
31 |
--------------------------------------------------------------------------------
/app/components/Sections/Photo/Border.jsx:
--------------------------------------------------------------------------------
1 | import { HelpCircleIcon } from "lucide-react";
2 | import { Slider } from "~/components/ui/slider";
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipProvider,
7 | TooltipTrigger,
8 | } from "~/components/ui/tooltip";
9 |
10 | function Border({ border, onChangeHandler }) {
11 | return (
12 |
13 |
14 | Border
15 |
16 |
17 |
18 |
19 |
20 |
21 | Customizes border once you have uploaded a photo.
22 |
23 |
24 |
25 |
26 |
33 |
34 | );
35 | }
36 |
37 | export default Border;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Rawand
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/components/ui/slider.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "~/utils"
5 |
6 | const Slider = React.forwardRef(({ className, ...props }, ref) => (
7 |
11 |
13 |
14 |
15 |
17 |
18 | ))
19 | Slider.displayName = SliderPrimitive.Root.displayName
20 |
21 | export { Slider }
22 |
--------------------------------------------------------------------------------
/app/components/ui/tooltip.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "~/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
13 |
21 | ))
22 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
23 |
24 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
25 |
--------------------------------------------------------------------------------
/app/components/TemplatePicker/TemplatePicker.jsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon, LayoutTemplateIcon } from "lucide-react";
2 | import { Button } from "~/components/ui/button";
3 | import { MAP_TEMPLATE_KEYS_TO_JSX } from "../TemplateRender/TemplateRender";
4 |
5 | function TemplatePicker({ selectedTemplate, setSelectedTemplate }) {
6 | return (
7 |
8 |
9 | Available Templates
10 |
11 |
12 |
13 | {Object.keys(MAP_TEMPLATE_KEYS_TO_JSX).map((templateKey) => {
14 | return (
15 | {
18 | setSelectedTemplate(templateKey);
19 | }}
20 | key={templateKey}
21 | >
22 | {MAP_TEMPLATE_KEYS_TO_JSX[templateKey].name}
23 | {selectedTemplate === templateKey && (
24 |
25 | )}
26 |
27 | );
28 | })}
29 |
30 |
31 | );
32 | }
33 |
34 | export default TemplatePicker;
35 |
--------------------------------------------------------------------------------
/app/components/TemplateRender/TemplateRender.jsx:
--------------------------------------------------------------------------------
1 | import CalmTemplate from "../Templates/CalmTemplate";
2 | import InitialTemplate from "../Templates/InitialTemplate";
3 | import ModernTemplate from "../Templates/ModernTemplate";
4 |
5 | export const MAP_TEMPLATE_KEYS_TO_JSX = {
6 | INITIAL_TEMPLATE: { component: InitialTemplate, name: "Classic" },
7 | MODERN_TEMPLATE: { component: ModernTemplate, name: "Modern" },
8 | CALM_TEMPLATE: { component: CalmTemplate, name: "Calm" },
9 | };
10 |
11 | function TemplateRenderer({
12 | selectedTemplate,
13 | userDetails,
14 | projects,
15 | skills,
16 | socials,
17 | educations,
18 | selectedImage,
19 | templateRef,
20 | border,
21 | }) {
22 | const TemplateComponent =
23 | MAP_TEMPLATE_KEYS_TO_JSX[selectedTemplate].component;
24 |
25 | return (
26 |
27 | {TemplateComponent && (
28 |
37 | )}
38 |
39 | );
40 | }
41 |
42 | export default TemplateRenderer;
43 |
--------------------------------------------------------------------------------
/app/components/ui/popover.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "~/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
11 |
12 |
21 |
22 | ))
23 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
24 |
25 | export { Popover, PopoverTrigger, PopoverContent }
26 |
--------------------------------------------------------------------------------
/app/components/Sections/Photo/UploadPhoto.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from "~/components/ui/input";
2 | import { Label } from "~/components/ui/label";
3 | import { Button } from "~/components/ui/button";
4 | import { useRef } from "react";
5 | import { TrashIcon } from "lucide-react";
6 |
7 | const UploadPhoto = ({ setSelectedImage }) => {
8 | const fileRef = useRef();
9 |
10 | return (
11 |
12 | Upload your photo
13 | {
17 | setSelectedImage(event.target.files[0]);
18 | }}
19 | accept="image/*"
20 | className="file:bg-blue-100 file:text-blue-800 hover:file:bg-blue-200 file:rounded-md cursor-pointer file:cursor-pointer"
21 | />
22 | {fileRef.current?.value && (
23 | {
28 | setSelectedImage(null);
29 | fileRef.current.value = "";
30 | }}
31 | >
32 |
33 |
34 | )}
35 |
36 | );
37 | };
38 |
39 | export default UploadPhoto;
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "scripts": {
5 | "build": "remix build",
6 | "dev": "remix dev",
7 | "typecheck": "tsc"
8 | },
9 | "dependencies": {
10 | "@radix-ui/react-accordion": "^1.1.2",
11 | "@radix-ui/react-label": "^2.0.2",
12 | "@radix-ui/react-popover": "^1.0.6",
13 | "@radix-ui/react-separator": "^1.0.3",
14 | "@radix-ui/react-slider": "^1.1.2",
15 | "@radix-ui/react-slot": "^1.0.2",
16 | "@radix-ui/react-toast": "^1.1.4",
17 | "@radix-ui/react-tooltip": "^1.0.6",
18 | "@remix-run/css-bundle": "^1.19.2",
19 | "@remix-run/node": "^1.19.2",
20 | "@remix-run/react": "^1.19.2",
21 | "class-variance-authority": "^0.7.0",
22 | "clsx": "^2.0.0",
23 | "date-fns": "^2.30.0",
24 | "isbot": "^3.6.8",
25 | "lucide-react": "^0.263.1",
26 | "react": "^18.2.0",
27 | "react-day-picker": "^8.8.0",
28 | "react-dom": "^18.2.0",
29 | "react-to-print": "^2.14.13",
30 | "source-map-support": "^0.5.21",
31 | "tailwind-merge": "^1.14.0",
32 | "tailwindcss-animate": "^1.0.6"
33 | },
34 | "devDependencies": {
35 | "@remix-run/dev": "^1.19.2",
36 | "@remix-run/eslint-config": "^1.19.2",
37 | "@remix-run/serve": "^1.19.2",
38 | "@types/react": "^18.0.35",
39 | "@types/react-dom": "^18.0.11",
40 | "@types/source-map-support": "^0.5.6",
41 | "autoprefixer": "^10.4.14",
42 | "eslint": "^8.38.0",
43 | "tailwindcss": "^3.3.3",
44 | "typescript": "^5.0.4"
45 | },
46 | "engines": {
47 | "node": ">=14.0.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/components/ui/accordion.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "~/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
10 |
11 | ))
12 | AccordionItem.displayName = "AccordionItem"
13 |
14 | const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
15 |
16 | svg]:rotate-180",
20 | className
21 | )}
22 | {...props}>
23 | {children}
24 |
25 |
26 |
27 | ))
28 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
29 |
30 | const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
31 |
38 | {children}
39 |
40 | ))
41 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
42 |
43 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
44 |
--------------------------------------------------------------------------------
/app/components/DatePicker/DatePicker.jsx:
--------------------------------------------------------------------------------
1 | import { Calendar } from "~/components/ui/calendar";
2 | import {
3 | Popover,
4 | PopoverContent,
5 | PopoverTrigger,
6 | } from "~/components/ui/popover";
7 | import { format } from "date-fns";
8 | import { CalendarIcon } from "lucide-react";
9 | import { cn } from "../../utils";
10 | import { Button } from "~/components/ui/button";
11 | import { useRef } from "react";
12 |
13 | function DatePicker({ date, label, setProjects, index }) {
14 | const inputRef = useRef(null);
15 |
16 | return (
17 |
18 |
19 |
26 |
33 |
34 | {date ? format(date, "MMM do, yyyy") : {label} }
35 |
36 |
37 |
38 | {
42 | setProjects((prevProjects) => {
43 | const updatedProjects = [...prevProjects];
44 | updatedProjects[index] = {
45 | ...updatedProjects[index],
46 | [inputRef.current?.name]: e,
47 | };
48 | return updatedProjects;
49 | });
50 | }}
51 | initialFocus
52 | />
53 |
54 |
55 | );
56 | }
57 |
58 | export default DatePicker;
59 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import { withOpacity } from "./app/lib/helpers";
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{js,jsx}",
8 | "./components/**/*.{js,jsx}",
9 | "./app/**/*.{js,jsx}",
10 | "./src/**/*.{js,jsx}",
11 | ],
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | scale: {
22 | 50: "0.5",
23 | 60: "0.6",
24 | 70: "0.7",
25 | 80: "0.8",
26 | 90: "0.9",
27 | 100: "1",
28 | 110: "1.10",
29 | 120: "1.20",
30 | },
31 | keyframes: {
32 | "accordion-down": {
33 | from: { height: 0 },
34 | to: { height: "var(--radix-accordion-content-height)" },
35 | },
36 | "accordion-up": {
37 | from: { height: "var(--radix-accordion-content-height)" },
38 | to: { height: 0 },
39 | },
40 | },
41 | animation: {
42 | "accordion-down": "accordion-down 0.2s ease-out",
43 | "accordion-up": "accordion-up 0.2s ease-out",
44 | },
45 | textColor: {
46 | custom: {
47 | primary: withOpacity("--color-primary"),
48 | a11y: withOpacity("--color-a11y"),
49 | },
50 | },
51 | backgroundColor: {
52 | custom: {
53 | primary: withOpacity("--color-primary"),
54 | a11y: withOpacity("--color-a11y"),
55 | },
56 | },
57 | ringColor: {
58 | custom: {
59 | primary: withOpacity("--color-primary"),
60 | },
61 | },
62 | borderColor: {
63 | custom: {
64 | primary: withOpacity("--color-primary"),
65 | a11y: withOpacity("--color-a11y"),
66 | },
67 | },
68 | },
69 | },
70 | plugins: [require("tailwindcss-animate")],
71 | };
72 |
--------------------------------------------------------------------------------
/app/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority";
4 |
5 | import { cn } from "~/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-800",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
13 | destructive:
14 | "bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/90",
15 | outline:
16 | "border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
17 | secondary:
18 | "bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
19 | ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
20 | link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
37 | const Comp = asChild ? Slot : "button"
38 | return (
39 | ( )
43 | );
44 | })
45 | Button.displayName = "Button"
46 |
47 | export { Button, buttonVariants }
48 |
--------------------------------------------------------------------------------
/app/components/Sections/UserDetails.jsx:
--------------------------------------------------------------------------------
1 | import UploadPhoto from "./Photo/UploadPhoto";
2 | import CustomInput from "../CustomInput/CustomInput";
3 |
4 | function UserDetails({ userDetails, setUserDetails, setSelectedImage }) {
5 | function handleChangeInput(e) {
6 | setUserDetails((prev) => ({
7 | ...prev,
8 | [e.target.name]: e.target.value,
9 | }));
10 | }
11 |
12 | return (
13 |
14 |
15 |
26 |
37 |
38 |
49 |
60 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default UserDetails;
77 |
--------------------------------------------------------------------------------
/app/components/Sections/Skills.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "~/components/ui/button";
2 | import {
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "~/components/ui/accordion";
7 |
8 | import { PlusCircle, TrashIcon } from "lucide-react";
9 | import CustomInput from "../CustomInput/CustomInput";
10 | import { DUMMY_DATA } from "../../constants/general";
11 |
12 | function Skills({ skills, setSkills }) {
13 | const handleAddSkill = () => {
14 | setSkills((prevSkills) => [...prevSkills, ""]);
15 | };
16 |
17 | const handleChangeInput = (e, index) => {
18 | const { value } = e.target;
19 |
20 | setSkills((prevSkills) => {
21 | const updatedSkills = [...prevSkills];
22 | updatedSkills[index] = value;
23 | return updatedSkills;
24 | });
25 | };
26 |
27 | const handleDeleteSkill = (index) => {
28 | setSkills((prevSkills) => {
29 | const updatedSkills = prevSkills.filter((_, i) => i !== index);
30 | return updatedSkills;
31 | });
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 | Skills
39 |
40 |
41 | {skills.map((skill, index) => {
42 | return (
43 |
44 | handleChangeInput(e, index)}
47 | id={`skill-${index}`}
48 | name="skill"
49 | placeholder={DUMMY_DATA.skill[index]}
50 | value={skill}
51 | />
52 | handleDeleteSkill(index)}
57 | >
58 |
59 |
60 |
61 | );
62 | })}
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | export default Skills;
73 |
--------------------------------------------------------------------------------
/app/components/Sections/Customization.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { getAccessibleColor, getRGBColor } from "../../lib/helpers";
3 | import { COLOR_VARIANTS } from "../../constants/general";
4 |
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | Popover,
8 | PopoverContent,
9 | PopoverTrigger,
10 | } from "~/components/ui/popover";
11 | import { SettingsIcon } from "lucide-react";
12 | import ColorPicker from "./ColorPicker";
13 | import TemplatePicker from "../TemplatePicker/TemplatePicker";
14 | import Border from "./Photo/Border";
15 |
16 | function Customization({
17 | selectedTemplate,
18 | setSelectedTemplate,
19 | border,
20 | setBorder,
21 | }) {
22 | const [selectedColor, setSelectedColor] = useState(COLOR_VARIANTS[0].hexCode);
23 |
24 | const primaryColor = getRGBColor(selectedColor, "primary");
25 | const a11yColor = getRGBColor(getAccessibleColor(selectedColor), "a11y");
26 |
27 | useEffect(() => {
28 | const styleTag = document.createElement("style");
29 | styleTag.textContent = `:root {
30 | ${primaryColor} ${a11yColor}
31 | }`;
32 |
33 | document.head.appendChild(styleTag);
34 |
35 | return () => document.head.removeChild(styleTag);
36 | }, [a11yColor, primaryColor]);
37 |
38 | const handleChangeBorder = (value) => {
39 | setBorder(...value);
40 | };
41 |
42 | return (
43 |
44 |
45 |
46 | Customization
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Customization
54 |
55 | Customize your template the way you prefer!
56 |
57 |
58 |
59 |
63 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | export default Customization;
76 |
--------------------------------------------------------------------------------
/app/constants/general.js:
--------------------------------------------------------------------------------
1 | export const INITIAL = "INITIAL";
2 |
3 | export const MAP_STATE_TO_TYPE = {
4 | INITIAL: {
5 | userDetails: {
6 | firstName: "John",
7 | lastName: "Doe",
8 | email: "example@example.com",
9 | jobTitle: "Frontend web developer",
10 | bio: "A passionate frontend developer. I like to build cutting edge application with latest technoglogies. I have started to programming in June 2020 and loving it.",
11 | },
12 | projects: [
13 | {
14 | projectTitle: "",
15 | projectDescription: "",
16 | stDate: null,
17 | enDate: null,
18 | },
19 | ],
20 | socials: [
21 | {
22 | socialName: "",
23 | socialLink: "",
24 | },
25 | ],
26 | skills: [""],
27 | educations: [
28 | {
29 | educationTitle: "",
30 | stDate: null,
31 | enDate: null,
32 | },
33 | ],
34 | },
35 | };
36 |
37 | export const INITIAL_TEMPLATE = "INITIAL_TEMPLATE";
38 | export const MODERN_TEMPLATE = "MODERN_TEMPLATE";
39 |
40 | export const DUMMY_START_DATE = new Date("January 02, 2020");
41 |
42 | export const COLOR_VARIANTS = [
43 | { hexCode: "#272829", twColor: "bg-[#272829]" },
44 | { hexCode: "#9b2226", twColor: "bg-[#9b2226]" },
45 | { hexCode: "#2b2d42", twColor: "bg-[#2b2d42]" },
46 | { hexCode: "#b4eeb4", twColor: "bg-[#b4eeb4]" },
47 | { hexCode: "#720443", twColor: "bg-[#720443]" },
48 | { hexCode: "#102C57", twColor: "bg-[#102C57]" },
49 | { hexCode: "#1ABC9C", twColor: "bg-[#1ABC9C]" },
50 | { hexCode: "#27AE60", twColor: "bg-[#27AE60]" },
51 | { hexCode: "#7EAA92", twColor: "bg-[#7EAA92]" },
52 | { hexCode: "#0D1282", twColor: "bg-[#0D1282]" },
53 | { hexCode: "#E9B384", twColor: "bg-[#E9B384]" },
54 | { hexCode: "#e98a15", twColor: "bg-[#e98a15]" },
55 | { hexCode: "#ff66b3", twColor: "bg-[#ff66b3]" },
56 | ];
57 |
58 | export const DUMMY_DATA = {
59 | educationTitle: "Bachelor of Software Engineering",
60 | projectTitle: "Project Title",
61 | projectDescription:
62 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n \t-sed do eiusmod tempor incididunt. \n \t-ut labore et dolore magna aliqua.",
63 | skill: ["JavaScript", "Reactjs"],
64 | socialName: "GitHub",
65 | socialLink: "https://github.com/RawandDev",
66 | };
67 |
68 | export const MAP_SCALE_VALUES_TO_TAILWIND_STYLE = {
69 | 50: "scale-50",
70 | 60: "scale-60",
71 | 70: "scale-70",
72 | 80: "scale-80",
73 | 90: "scale-90",
74 | 100: "scale-100",
75 | 110: "scale-110",
76 | 120: "scale-120",
77 | };
78 |
79 | export const SCALE_VALUES = Object.keys(MAP_SCALE_VALUES_TO_TAILWIND_STYLE).map(
80 | Number
81 | );
82 |
83 | export const INITIAL_SCALE = 70;
84 |
--------------------------------------------------------------------------------
/app/components/ui/calendar.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight } from "lucide-react"
3 | import { DayPicker } from "react-day-picker"
4 |
5 | import { cn } from "~/utils"
6 | import { buttonVariants } from "~/components/ui/button"
7 |
8 | function Calendar({
9 | className,
10 | classNames,
11 | showOutsideDays = true,
12 | ...props
13 | }) {
14 | return (
15 | ( ,
52 | IconRight: ({ ...props }) => ,
53 | }}
54 | {...props} />)
55 | );
56 | }
57 | Calendar.displayName = "Calendar"
58 |
59 | export { Calendar }
60 |
--------------------------------------------------------------------------------
/app/components/Sections/Socials.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "~/components/ui/button";
2 | import {
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "~/components/ui/accordion";
7 |
8 | import { PlusCircle, TrashIcon } from "lucide-react";
9 | import CustomInput from "../CustomInput/CustomInput";
10 | import { DUMMY_DATA } from "../../constants/general";
11 |
12 | function Socials({ socials, setSocials }) {
13 | const handleAddSocial = () => {
14 | setSocials((prevSocials) => [
15 | ...prevSocials,
16 | {
17 | socialName: "",
18 | socialLink: "",
19 | },
20 | ]);
21 | };
22 |
23 | const handleChangeInput = (e, index) => {
24 | const { name, value } = e.target;
25 |
26 | setSocials((prevSocials) => {
27 | const updatedSocials = [...prevSocials];
28 | updatedSocials[index] = {
29 | ...updatedSocials[index],
30 | [name]: value,
31 | };
32 | return updatedSocials;
33 | });
34 | };
35 |
36 | const handleDeleteProject = (index) => {
37 | setSocials((prevSocials) => {
38 | const updatedSocials = prevSocials.filter((_, i) => i !== index);
39 | return updatedSocials;
40 | });
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 | Social Links
48 |
49 |
50 | {socials.map((social, index) => {
51 | return (
52 |
53 |
54 | handleChangeInput(e, index)}
57 | id={index}
58 | name="socialName"
59 | placeholder={DUMMY_DATA.socialName}
60 | value={social.socialName}
61 | />
62 |
63 |
64 | handleChangeInput(e, index)}
67 | id={index}
68 | name="socialLink"
69 | placeholder={DUMMY_DATA.socialLink}
70 | value={social.socialLink}
71 | />
72 | handleDeleteProject(index)}
77 | >
78 |
79 |
80 |
81 |
82 | );
83 | })}
84 |
85 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
93 | export default Socials;
94 |
--------------------------------------------------------------------------------
/app/components/Sections/Education.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "~/components/ui/button";
2 | import {
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "~/components/ui/accordion";
7 | import { Fragment } from "react";
8 | import DatePicker from "../DatePicker/DatePicker";
9 | import { PlusCircle, TrashIcon } from "lucide-react";
10 | import CustomInput from "../CustomInput/CustomInput";
11 | import { DUMMY_DATA } from "../../constants/general";
12 |
13 | function Education({ educations, setEducations }) {
14 | const addEducation = () => {
15 | setEducations((prevEducations) => [
16 | ...prevEducations,
17 | {
18 | educationTitle: "",
19 | stDate: "",
20 | enDate: "",
21 | },
22 | ]);
23 | };
24 |
25 | const handleChangeInput = (e, index) => {
26 | const { name, value } = e.target;
27 |
28 | setEducations((prevEducations) => {
29 | const updatedEducations = [...prevEducations];
30 | updatedEducations[index] = {
31 | ...updatedEducations[index],
32 | [name]: value,
33 | };
34 | return updatedEducations;
35 | });
36 | };
37 |
38 | const handleDeleteEducation = (index) => {
39 | setEducations((prevEducations) => {
40 | const updatedEducations = prevEducations.filter((_, i) => i !== index);
41 | return updatedEducations;
42 | });
43 | };
44 | return (
45 |
46 |
47 |
48 | Education
49 |
50 |
51 | {educations.map((education, index) => {
52 | return (
53 |
54 | handleChangeInput(e, index)}
57 | label={{
58 | htmlFor: `education-${index}`,
59 | text: "Degree",
60 | }}
61 | id={`education-${index}`}
62 | name="educationTitle"
63 | placeholder={DUMMY_DATA.educationTitle}
64 | value={education.educationTitle}
65 | />
66 |
67 |
73 | -
74 |
80 |
81 | handleDeleteEducation(index)}
86 | >
87 |
88 |
89 |
90 | );
91 | })}
92 |
93 |
94 | {" "}
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | export default Education;
102 |
--------------------------------------------------------------------------------
/app/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | const TOAST_LIMIT = 1
5 | const TOAST_REMOVE_DELAY = 1000000
6 |
7 | const actionTypes = {
8 | ADD_TOAST: "ADD_TOAST",
9 | UPDATE_TOAST: "UPDATE_TOAST",
10 | DISMISS_TOAST: "DISMISS_TOAST",
11 | REMOVE_TOAST: "REMOVE_TOAST"
12 | }
13 |
14 | let count = 0
15 |
16 | function genId() {
17 | count = (count + 1) % Number.MAX_VALUE
18 | return count.toString();
19 | }
20 |
21 | const toastTimeouts = new Map()
22 |
23 | const addToRemoveQueue = (toastId) => {
24 | if (toastTimeouts.has(toastId)) {
25 | return
26 | }
27 |
28 | const timeout = setTimeout(() => {
29 | toastTimeouts.delete(toastId)
30 | dispatch({
31 | type: "REMOVE_TOAST",
32 | toastId: toastId,
33 | })
34 | }, TOAST_REMOVE_DELAY)
35 |
36 | toastTimeouts.set(toastId, timeout)
37 | }
38 |
39 | export const reducer = (state, action) => {
40 | switch (action.type) {
41 | case "ADD_TOAST":
42 | return {
43 | ...state,
44 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
45 | };
46 |
47 | case "UPDATE_TOAST":
48 | return {
49 | ...state,
50 | toasts: state.toasts.map((t) =>
51 | t.id === action.toast.id ? { ...t, ...action.toast } : t),
52 | };
53 |
54 | case "DISMISS_TOAST": {
55 | const { toastId } = action
56 |
57 | // ! Side effects ! - This could be extracted into a dismissToast() action,
58 | // but I'll keep it here for simplicity
59 | if (toastId) {
60 | addToRemoveQueue(toastId)
61 | } else {
62 | state.toasts.forEach((toast) => {
63 | addToRemoveQueue(toast.id)
64 | })
65 | }
66 |
67 | return {
68 | ...state,
69 | toasts: state.toasts.map((t) =>
70 | t.id === toastId || toastId === undefined
71 | ? {
72 | ...t,
73 | open: false,
74 | }
75 | : t),
76 | };
77 | }
78 | case "REMOVE_TOAST":
79 | if (action.toastId === undefined) {
80 | return {
81 | ...state,
82 | toasts: [],
83 | }
84 | }
85 | return {
86 | ...state,
87 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
88 | };
89 | }
90 | }
91 |
92 | const listeners = []
93 |
94 | let memoryState = { toasts: [] }
95 |
96 | function dispatch(action) {
97 | memoryState = reducer(memoryState, action)
98 | listeners.forEach((listener) => {
99 | listener(memoryState)
100 | })
101 | }
102 |
103 | function toast({
104 | ...props
105 | }) {
106 | const id = genId()
107 |
108 | const update = (props) =>
109 | dispatch({
110 | type: "UPDATE_TOAST",
111 | toast: { ...props, id },
112 | })
113 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
114 |
115 | dispatch({
116 | type: "ADD_TOAST",
117 | toast: {
118 | ...props,
119 | id,
120 | open: true,
121 | onOpenChange: (open) => {
122 | if (!open) dismiss()
123 | },
124 | },
125 | })
126 |
127 | return {
128 | id: id,
129 | dismiss,
130 | update,
131 | }
132 | }
133 |
134 | function useToast() {
135 | const [state, setState] = React.useState(memoryState)
136 |
137 | React.useEffect(() => {
138 | listeners.push(setState)
139 | return () => {
140 | const index = listeners.indexOf(setState)
141 | if (index > -1) {
142 | listeners.splice(index, 1)
143 | }
144 | };
145 | }, [state])
146 |
147 | return {
148 | ...state,
149 | toast,
150 | dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
151 | };
152 | }
153 |
154 | export { useToast, toast }
155 |
--------------------------------------------------------------------------------
/app/entry.server.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle generating the HTTP Response for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.server
5 | */
6 |
7 | import { PassThrough } from "node:stream";
8 |
9 | import { Response } from "@remix-run/node";
10 | import { RemixServer } from "@remix-run/react";
11 | import isbot from "isbot";
12 | import { renderToPipeableStream } from "react-dom/server";
13 |
14 | const ABORT_DELAY = 5_000;
15 |
16 | export default function handleRequest(
17 | request,
18 | responseStatusCode,
19 | responseHeaders,
20 | remixContext,
21 | loadContext
22 | ) {
23 | return isbot(request.headers.get("user-agent"))
24 | ? handleBotRequest(
25 | request,
26 | responseStatusCode,
27 | responseHeaders,
28 | remixContext
29 | )
30 | : handleBrowserRequest(
31 | request,
32 | responseStatusCode,
33 | responseHeaders,
34 | remixContext
35 | );
36 | }
37 |
38 | function handleBotRequest(
39 | request,
40 | responseStatusCode,
41 | responseHeaders,
42 | remixContext
43 | ) {
44 | return new Promise((resolve, reject) => {
45 | let shellRendered = false;
46 | const { pipe, abort } = renderToPipeableStream(
47 | ,
52 |
53 | {
54 | onAllReady() {
55 | shellRendered = true;
56 | const body = new PassThrough();
57 |
58 | responseHeaders.set("Content-Type", "text/html");
59 |
60 | resolve(
61 | new Response(body, {
62 | headers: responseHeaders,
63 | status: responseStatusCode,
64 | })
65 | );
66 |
67 | pipe(body);
68 | },
69 | onShellError(error) {
70 | reject(error);
71 | },
72 | onError(error) {
73 | responseStatusCode = 500;
74 | // Log streaming rendering errors from inside the shell. Don't log
75 | // errors encountered during initial shell rendering since they'll
76 | // reject and get logged in handleDocumentRequest.
77 | if (shellRendered) {
78 | console.error(error);
79 | }
80 | },
81 | }
82 | );
83 |
84 | setTimeout(abort, ABORT_DELAY);
85 | });
86 | }
87 |
88 | function handleBrowserRequest(
89 | request,
90 | responseStatusCode,
91 | responseHeaders,
92 | remixContext
93 | ) {
94 | return new Promise((resolve, reject) => {
95 | let shellRendered = false;
96 | const { pipe, abort } = renderToPipeableStream(
97 | ,
102 |
103 | {
104 | onShellReady() {
105 | shellRendered = true;
106 | const body = new PassThrough();
107 |
108 | responseHeaders.set("Content-Type", "text/html");
109 |
110 | resolve(
111 | new Response(body, {
112 | headers: responseHeaders,
113 | status: responseStatusCode,
114 | })
115 | );
116 |
117 | pipe(body);
118 | },
119 | onShellError(error) {
120 | reject(error);
121 | },
122 | onError(error) {
123 | responseStatusCode = 500;
124 | // Log streaming rendering errors from inside the shell. Don't log
125 | // errors encountered during initial shell rendering since they'll
126 | // reject and get logged in handleDocumentRequest.
127 | if (shellRendered) {
128 | console.error(error);
129 | }
130 | },
131 | }
132 | );
133 |
134 | setTimeout(abort, ABORT_DELAY);
135 | });
136 | }
137 |
--------------------------------------------------------------------------------
/app/components/Sections/Projects.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 | import DatePicker from "../../components/DatePicker/DatePicker";
3 | import { PlusCircle, TrashIcon } from "lucide-react";
4 |
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | AccordionContent,
8 | AccordionItem,
9 | AccordionTrigger,
10 | } from "~/components/ui/accordion";
11 | import CustomInput from "../CustomInput/CustomInput";
12 | import {
13 | DUMMY_DATA,
14 | INITIAL,
15 | MAP_STATE_TO_TYPE,
16 | } from "../../constants/general";
17 |
18 | function Projects({ projects, setProjects }) {
19 | const addProject = () => {
20 | setProjects((prevProjects) => [
21 | ...prevProjects,
22 | {
23 | projectTitle: MAP_STATE_TO_TYPE[INITIAL].projects[0].projectTitle,
24 | stDate: "",
25 | enDate: "",
26 | projectDescription:
27 | MAP_STATE_TO_TYPE[INITIAL].projects[0].projectDescription,
28 | },
29 | ]);
30 | };
31 |
32 | const handleChangeInput = (e, index) => {
33 | const { name, value } = e.target;
34 |
35 | setProjects((prevProjects) => {
36 | const updatedProjects = [...prevProjects];
37 | updatedProjects[index] = {
38 | ...updatedProjects[index],
39 | [name]: value,
40 | };
41 | return updatedProjects;
42 | });
43 | };
44 |
45 | const handleDeleteProject = (index) => {
46 | setProjects((prevProjects) => {
47 | const updatedProjects = prevProjects.filter((_, i) => i !== index);
48 | return updatedProjects;
49 | });
50 | };
51 |
52 | return (
53 |
54 |
55 |
56 | Projects
57 |
58 |
59 | {projects.map((project, index) => {
60 | return (
61 |
62 | handleChangeInput(e, index)}
65 | label={{
66 | htmlFor: index,
67 | text: "Project Title",
68 | }}
69 | id={index}
70 | name="projectTitle"
71 | placeholder={DUMMY_DATA.projectTitle}
72 | value={project.projectTitle}
73 | />
74 |
75 |
81 | -
82 |
88 |
89 | handleChangeInput(e, index)}
92 | label={{
93 | htmlFor: `description-${index}`,
94 | text: "Brief Explanation",
95 | }}
96 | id={`description-${index}`}
97 | name="projectDescription"
98 | placeholder={DUMMY_DATA.projectDescription}
99 | value={project.projectDescription}
100 | />
101 | handleDeleteProject(index)}
106 | >
107 |
108 |
109 |
110 | );
111 | })}
112 |
113 |
114 | {" "}
115 |
116 |
117 |
118 | );
119 | }
120 |
121 | export default Projects;
122 |
--------------------------------------------------------------------------------
/app/components/ui/toast.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva } from "class-variance-authority";
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "~/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
11 |
18 | ))
19 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
20 |
21 | const toastVariants = cva(
22 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border border-slate-200 p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-slate-800",
23 | {
24 | variants: {
25 | variant: {
26 | default: "border bg-white dark:bg-slate-950",
27 | destructive:
28 | "destructive group border-red-500 bg-red-500 text-slate-50 dark:border-red-900 dark:bg-red-900 dark:text-red-50",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | },
34 | }
35 | )
36 |
37 | const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
38 | return (
39 | ( )
43 | );
44 | })
45 | Toast.displayName = ToastPrimitives.Root.displayName
46 |
47 | const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
48 |
55 | ))
56 | ToastAction.displayName = ToastPrimitives.Action.displayName
57 |
58 | const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
59 |
67 |
68 |
69 | ))
70 | ToastClose.displayName = ToastPrimitives.Close.displayName
71 |
72 | const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
73 |
74 | ))
75 | ToastTitle.displayName = ToastPrimitives.Title.displayName
76 |
77 | const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
78 |
79 | ))
80 | ToastDescription.displayName = ToastPrimitives.Description.displayName
81 |
82 | export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
83 |
--------------------------------------------------------------------------------
/app/routes/_index.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import { Button } from "~/components/ui/button";
3 | import { Separator } from "~/components/ui/separator";
4 | import { DownloadIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
5 | import Projects from "../components/Sections/Projects";
6 | import Socials from "../components/Sections/Socials";
7 | import Skills from "../components/Sections/Skills";
8 | import UserDetails from "../components/Sections/UserDetails";
9 | import {
10 | INITIAL,
11 | INITIAL_SCALE,
12 | INITIAL_TEMPLATE,
13 | MAP_SCALE_VALUES_TO_TAILWIND_STYLE,
14 | MAP_STATE_TO_TYPE,
15 | } from "../constants/general";
16 | import { useReactToPrint } from "react-to-print";
17 | import { Accordion } from "~/components/ui/accordion";
18 | import TemplateRenderer from "../components/TemplateRender/TemplateRender";
19 | import Education from "../components/Sections/Education";
20 | import useZoom from "../hooks/useZoom";
21 | import Customization from "../components/Sections/Customization";
22 |
23 | export const meta = () => {
24 | return [
25 | { title: "Simple Resume Builder" },
26 | { name: "description", content: "Elegant way of creating your resume." },
27 | ];
28 | };
29 |
30 | export default function Index() {
31 | const [userDetails, setUserDetails] = useState(
32 | MAP_STATE_TO_TYPE[INITIAL].userDetails
33 | );
34 | const [projects, setProjects] = useState(MAP_STATE_TO_TYPE[INITIAL].projects);
35 | const [socials, setSocials] = useState(MAP_STATE_TO_TYPE[INITIAL].socials);
36 | const [skills, setSkills] = useState(MAP_STATE_TO_TYPE[INITIAL].skills);
37 | const [educations, setEducations] = useState(
38 | MAP_STATE_TO_TYPE[INITIAL].educations
39 | );
40 | const [selectedTemplate, setSelectedTemplate] = useState(INITIAL_TEMPLATE);
41 | const [selectedImage, setSelectedImage] = useState(null);
42 | const { zoom, handleZoomIn, handleZoomOut } = useZoom(INITIAL_SCALE);
43 | const [border, setBorder] = useState(2);
44 | const templateRef = useRef();
45 |
46 | const handlePrint = useReactToPrint({
47 | content: () => templateRef.current,
48 | documentTitle: `${userDetails.firstName} ${userDetails.lastName} - CV`,
49 | });
50 |
51 | return (
52 |
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
76 |
77 | {
80 | handlePrint();
81 | }}
82 | >
83 | Download CV
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/app/components/Templates/ModernTemplate.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@remix-run/react";
2 | import { format } from "date-fns";
3 | import { DUMMY_DATA, DUMMY_START_DATE } from "../../constants/general";
4 | import PhotoPreview from "../Sections/Photo/PhotoPreview";
5 |
6 | function ModernTemplate({
7 | userDetails,
8 | projects,
9 | skills,
10 | socials,
11 | educations,
12 | selectedImage,
13 | }) {
14 | return (
15 | <>
16 |
44 |
45 |
46 |
47 | Work Experience
48 |
49 | {projects?.map((project, index) => (
50 |
51 |
52 |
53 | {project.projectTitle || DUMMY_DATA.projectTitle}
54 |
{" "}
55 |
60 |
61 | {project.stDate && format(project.stDate, "MMM do, yyyy")}
62 | {" "}
63 | -{" "}
64 |
65 | {project.enDate
66 | ? format(project.enDate, "MMM do, yyyy")
67 | : "Present"}
68 |
69 |
70 |
71 |
72 | {project.projectDescription || DUMMY_DATA.projectDescription}
73 |
74 |
75 | ))}
76 |
77 |
78 |
79 |
80 | Education
81 |
82 | {educations?.map((education, index) => (
83 |
84 |
85 |
86 | {education.educationTitle || DUMMY_DATA.educationTitle}
87 |
{" "}
88 |
89 |
90 | {education.stDate
91 | ? format(education.stDate, "MMM do, yyyy")
92 | : format(DUMMY_START_DATE, "MMM do, yyyy")}
93 | {" "}
94 | -{" "}
95 |
96 | {education.enDate
97 | ? format(education.enDate, "MMM do, yyyy")
98 | : "Present"}
99 |
100 |
101 |
102 |
103 | ))}
104 |
105 |
106 | Skills
107 |
108 | {skills.map((skill, index) => (
109 |
{skill || DUMMY_DATA.skill[index]}
110 | ))}
111 |
112 |
113 |
114 |
115 | >
116 | );
117 | }
118 |
119 | export default ModernTemplate;
120 |
--------------------------------------------------------------------------------
/app/components/Templates/CalmTemplate.jsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import { DUMMY_DATA, DUMMY_START_DATE } from "../../constants/general";
3 | import { Link } from "@remix-run/react";
4 | import PhotoPreview from "../Sections/Photo/PhotoPreview";
5 | import { Separator } from "~/components/ui/separator";
6 |
7 | function CalmTemplate({
8 | userDetails,
9 | projects,
10 | skills,
11 | socials,
12 | educations,
13 | selectedImage,
14 | border
15 | }) {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {userDetails.firstName} {userDetails.lastName}
25 |
26 |
27 | {userDetails.jobTitle}
28 |
29 |
30 |
31 |
32 |
{userDetails.email}
33 |
34 | {socials.map((social, index) => (
35 |
42 | {social.socialName || DUMMY_DATA.socialName}
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 |
50 |
51 | Profile
52 | {userDetails.bio}
53 |
54 |
55 |
56 |
57 | Education
58 |
59 | {educations?.map((education, index) => (
60 |
61 |
62 |
63 | {education.stDate
64 | ? format(education.stDate, "MMM do, yyyy")
65 | : format(DUMMY_START_DATE, "MMM do, yyyy")}
66 | {" "}
67 | -{" "}
68 |
69 | {education.enDate
70 | ? format(education.enDate, "MMM do, yyyy")
71 | : "Present"}
72 |
73 |
74 |
75 | {education.educationTitle || DUMMY_DATA.educationTitle}
76 |
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 | Work Experience
84 |
85 | {projects?.map((project, index) => (
86 |
87 |
88 |
91 |
92 | {project.stDate && format(project.stDate, "MMM do, yyyy")}
93 | {" "}
94 | -{" "}
95 |
96 | {project.enDate
97 | ? format(project.enDate, "MMM do, yyyy")
98 | : "Present"}
99 |
100 |
101 |
102 |
103 |
104 | {project.projectTitle || DUMMY_DATA.projectTitle}
105 |
{" "}
106 |
107 | {project.projectDescription || DUMMY_DATA.projectDescription}
108 |
109 |
110 |
111 | ))}
112 |
113 |
114 |
115 | Skills
116 |
117 | {skills.map((skill, index) => (
118 |
{skill || DUMMY_DATA.skill[index]}
119 | ))}
120 |
121 |
122 |
123 | );
124 | }
125 |
126 | export default CalmTemplate;
127 |
--------------------------------------------------------------------------------
/app/components/Templates/InitialTemplate.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@remix-run/react";
2 | import { format } from "date-fns";
3 | import { DUMMY_DATA, DUMMY_START_DATE } from "../../constants/general";
4 | import PhotoPreview from "../Sections/Photo/PhotoPreview";
5 |
6 | function InitialTemplate({
7 | userDetails,
8 | projects,
9 | skills,
10 | socials,
11 | educations,
12 | selectedImage,
13 | border,
14 | }) {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 | {userDetails.firstName} {userDetails.lastName}
23 |
24 |
25 | {userDetails.jobTitle}
26 |
27 |
28 |
29 |
30 |
31 | Summary
32 |
33 |
{userDetails.bio}
34 |
35 |
36 |
37 | Projects
38 |
39 |
40 |
41 | {projects?.map((project, index) => (
42 |
43 |
44 |
45 |
46 | {project.stDate
47 | ? format(project.stDate, "MMM do, yyyy")
48 | : format(DUMMY_START_DATE, "MMM do, yyyy")}
49 | {" "}
50 | -{" "}
51 |
52 | {project.enDate
53 | ? format(project.enDate, "MMM do, yyyy")
54 | : "Present"}
55 |
56 |
57 |
58 | {project.projectTitle || DUMMY_DATA.projectTitle}
59 |
60 |
61 |
62 | {project.projectDescription ||
63 | DUMMY_DATA.projectDescription}
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 |
72 |
73 | {userDetails.email}
74 |
75 |
76 |
77 | Socials
78 |
79 | {socials.map((social, index) => (
80 |
86 | {social.socialName || DUMMY_DATA.socialName}
87 |
88 | ))}
89 |
90 |
91 |
92 | Skills
93 |
94 | {skills.map((skill, index) => (
95 |
{skill || DUMMY_DATA.skill[index]}
96 | ))}
97 |
98 |
99 |
100 | Education
101 |
102 |
103 | {educations?.map((education, index) => (
104 |
105 |
106 |
107 | {education.educationTitle || DUMMY_DATA.educationTitle}
108 |
{" "}
109 |
110 |
111 | {education.stDate
112 | ? format(education.stDate, "MMM do, yyyy")
113 | : format(DUMMY_START_DATE, "MMM do, yyyy")}
114 | {" "}
115 | -{" "}
116 |
117 | {education.enDate
118 | ? format(education.enDate, "MMM do, yyyy")
119 | : "Present"}
120 |
121 |
122 |
123 |
124 | ))}
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
132 | export default InitialTemplate;
133 |
--------------------------------------------------------------------------------
/api/metafile.server.json:
--------------------------------------------------------------------------------
1 | {"inputs":{"app/entry.server.jsx":{"bytes":3371,"imports":[{"path":"node:stream","kind":"import-statement","external":true},{"path":"@remix-run/node","kind":"import-statement","external":true},{"path":"@remix-run/react","kind":"import-statement","external":true},{"path":"isbot","kind":"import-statement","external":true},{"path":"react-dom/server","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"css-bundle-update-plugin-ns:D:\\playground\\my-resume\\node_modules\\@remix-run\\css-bundle\\dist\\index.js":{"bytes":459,"imports":[],"format":"cjs"},"app/tailwind.css":{"bytes":28821,"imports":[]},"app/utils.js":{"bytes":136,"imports":[{"path":"clsx","kind":"import-statement","external":true},{"path":"tailwind-merge","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/toast.jsx":{"bytes":4358,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-toast","kind":"import-statement","external":true},{"path":"class-variance-authority","kind":"import-statement","external":true},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/use-toast.ts":{"bytes":3218,"imports":[{"path":"react","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/toaster.jsx":{"bytes":819,"imports":[{"path":"app/components/ui/toast.jsx","kind":"import-statement","original":"~/components/ui/toast"},{"path":"app/components/ui/use-toast.ts","kind":"import-statement","original":"~/components/ui/use-toast"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/root.jsx":{"bytes":786,"imports":[{"path":"css-bundle-update-plugin-ns:D:\\playground\\my-resume\\node_modules\\@remix-run\\css-bundle\\dist\\index.js","kind":"import-statement","original":"@remix-run/css-bundle"},{"path":"app/tailwind.css","kind":"import-statement","original":"./tailwind.css"},{"path":"app/components/ui/toaster.jsx","kind":"import-statement","original":"~/components/ui/toaster"},{"path":"@remix-run/react","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/button.jsx":{"bytes":1991,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-slot","kind":"import-statement","external":true},{"path":"class-variance-authority","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/separator.jsx":{"bytes":650,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-separator","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/calendar.jsx":{"bytes":2608,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"react-day-picker","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/popover.jsx":{"bytes":1190,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-popover","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/DatePicker/DatePicker.jsx":{"bytes":1714,"imports":[{"path":"app/components/ui/calendar.jsx","kind":"import-statement","original":"~/components/ui/calendar"},{"path":"app/components/ui/popover.jsx","kind":"import-statement","original":"~/components/ui/popover"},{"path":"date-fns","kind":"import-statement","external":true},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"../../utils"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"react","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/input.jsx":{"bytes":858,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/label.jsx":{"bytes":537,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-label","kind":"import-statement","external":true},{"path":"class-variance-authority","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/textarea.jsx":{"bytes":780,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/ui/accordion.jsx":{"bytes":1651,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@radix-ui/react-accordion","kind":"import-statement","external":true},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/utils.js","kind":"import-statement","original":"~/utils"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Sections/Projects.jsx":{"bytes":3938,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"app/components/DatePicker/DatePicker.jsx","kind":"import-statement","original":"../../components/DatePicker/DatePicker"},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/components/ui/input.jsx","kind":"import-statement","original":"~/components/ui/input"},{"path":"app/components/ui/label.jsx","kind":"import-statement","original":"~/components/ui/label"},{"path":"app/components/ui/textarea.jsx","kind":"import-statement","original":"~/components/ui/textarea"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/components/ui/accordion.jsx","kind":"import-statement","original":"~/components/ui/accordion"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Sections/Socials.jsx":{"bytes":2826,"imports":[{"path":"app/components/ui/input.jsx","kind":"import-statement","original":"~/components/ui/input"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/components/ui/accordion.jsx","kind":"import-statement","original":"~/components/ui/accordion"},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Sections/Skills.jsx":{"bytes":2113,"imports":[{"path":"app/components/ui/input.jsx","kind":"import-statement","original":"~/components/ui/input"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/components/ui/accordion.jsx","kind":"import-statement","original":"~/components/ui/accordion"},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Sections/UserDetails.jsx":{"bytes":2356,"imports":[{"path":"app/components/ui/input.jsx","kind":"import-statement","original":"~/components/ui/input"},{"path":"app/components/ui/label.jsx","kind":"import-statement","original":"~/components/ui/label"},{"path":"app/components/ui/textarea.jsx","kind":"import-statement","original":"~/components/ui/textarea"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/constants/general.js":{"bytes":1657,"imports":[],"format":"esm"},"app/components/Templates/InitialTemplate.jsx":{"bytes":4126,"imports":[{"path":"@remix-run/react","kind":"import-statement","external":true},{"path":"date-fns","kind":"import-statement","external":true},{"path":"app/constants/general.js","kind":"import-statement","original":"../../constants/general"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Templates/ModernTemplate.jsx":{"bytes":3708,"imports":[{"path":"@remix-run/react","kind":"import-statement","external":true},{"path":"date-fns","kind":"import-statement","external":true},{"path":"app/constants/general.js","kind":"import-statement","original":"../../constants/general"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/TemplateRender/TemplateRender.jsx":{"bytes":748,"imports":[{"path":"app/components/Templates/InitialTemplate.jsx","kind":"import-statement","original":"../Templates/InitialTemplate"},{"path":"app/components/Templates/ModernTemplate.jsx","kind":"import-statement","original":"../Templates/ModernTemplate"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/TemplatePicker/TemplatePicker.jsx":{"bytes":896,"imports":[{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/constants/general.js","kind":"import-statement","original":"../../constants/general"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/components/Sections/Education.jsx":{"bytes":3402,"imports":[{"path":"app/components/ui/input.jsx","kind":"import-statement","original":"~/components/ui/input"},{"path":"app/components/ui/label.jsx","kind":"import-statement","original":"~/components/ui/label"},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/components/ui/accordion.jsx","kind":"import-statement","original":"~/components/ui/accordion"},{"path":"react","kind":"import-statement","external":true},{"path":"app/components/DatePicker/DatePicker.jsx","kind":"import-statement","original":"../DatePicker/DatePicker"},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"app/routes/_index.jsx":{"bytes":4813,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"app/components/ui/button.jsx","kind":"import-statement","original":"~/components/ui/button"},{"path":"app/components/ui/separator.jsx","kind":"import-statement","original":"~/components/ui/separator"},{"path":"app/components/ui/toast.jsx","kind":"import-statement","original":"~/components/ui/toast"},{"path":"app/components/ui/use-toast.ts","kind":"import-statement","original":"~/components/ui/use-toast"},{"path":"lucide-react","kind":"import-statement","external":true},{"path":"app/components/Sections/Projects.jsx","kind":"import-statement","original":"../components/Sections/Projects"},{"path":"app/components/Sections/Socials.jsx","kind":"import-statement","original":"../components/Sections/Socials"},{"path":"app/components/Sections/Skills.jsx","kind":"import-statement","original":"../components/Sections/Skills"},{"path":"app/components/Sections/UserDetails.jsx","kind":"import-statement","original":"../components/Sections/UserDetails"},{"path":"app/constants/general.js","kind":"import-statement","original":"../constants/general"},{"path":"react-to-print","kind":"import-statement","external":true},{"path":"app/components/ui/accordion.jsx","kind":"import-statement","original":"~/components/ui/accordion"},{"path":"app/components/TemplateRender/TemplateRender.jsx","kind":"import-statement","original":"../components/TemplateRender/TemplateRender"},{"path":"app/components/TemplatePicker/TemplatePicker.jsx","kind":"import-statement","original":"../components/TemplatePicker/TemplatePicker"},{"path":"app/components/Sections/Education.jsx","kind":"import-statement","original":"../components/Sections/Education"},{"path":"react/jsx-dev-runtime","kind":"import-statement","external":true}],"format":"esm"},"server-assets-manifest:@remix-run/dev/assets-manifest":{"bytes":1037,"imports":[],"format":"esm"},"server-entry-module:@remix-run/dev/server-build":{"bytes":946,"imports":[{"path":"app/entry.server.jsx","kind":"import-statement","original":"D:\\playground\\my-resume\\app\\entry.server.jsx"},{"path":"app/root.jsx","kind":"import-statement","original":"./root.jsx"},{"path":"app/routes/_index.jsx","kind":"import-statement","original":"./routes/_index.jsx"},{"path":"server-assets-manifest:@remix-run/dev/assets-manifest","kind":"import-statement","original":"@remix-run/dev/assets-manifest"}],"format":"esm"},"":{"bytes":44,"imports":[{"path":"server-entry-module:@remix-run/dev/server-build","kind":"import-statement","original":"@remix-run/dev/server-build"}],"format":"esm"}},"outputs":{"api/_assets/tailwind-UHUB32Z3.css":{"imports":[],"exports":[],"inputs":{"app/tailwind.css":{"bytesInOutput":28821}},"bytes":28821},"api/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":97961},"api/index.js":{"imports":[{"path":"node:stream","kind":"require-call","external":true},{"path":"@remix-run/node","kind":"require-call","external":true},{"path":"@remix-run/react","kind":"require-call","external":true},{"path":"isbot","kind":"require-call","external":true},{"path":"react-dom/server","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"api/_assets/tailwind-UHUB32Z3.css","kind":"file-loader"},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-toast","kind":"require-call","external":true},{"path":"class-variance-authority","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"clsx","kind":"require-call","external":true},{"path":"tailwind-merge","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"@remix-run/react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-slot","kind":"require-call","external":true},{"path":"class-variance-authority","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-separator","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react-day-picker","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-popover","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"date-fns","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-label","kind":"require-call","external":true},{"path":"class-variance-authority","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"@radix-ui/react-accordion","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react-to-print","kind":"require-call","external":true},{"path":"@remix-run/react","kind":"require-call","external":true},{"path":"date-fns","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"@remix-run/react","kind":"require-call","external":true},{"path":"date-fns","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react","kind":"require-call","external":true},{"path":"lucide-react","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true},{"path":"react/jsx-dev-runtime","kind":"require-call","external":true}],"exports":[],"entryPoint":"","inputs":{"css-bundle-update-plugin-ns:D:\\playground\\my-resume\\node_modules\\@remix-run\\css-bundle\\dist\\index.js":{"bytesInOutput":314},"":{"bytesInOutput":294},"app/entry.server.jsx":{"bytesInOutput":3172},"app/root.jsx":{"bytesInOutput":2843},"app/tailwind.css":{"bytesInOutput":63},"app/components/ui/toast.jsx":{"bytesInOutput":5367},"app/utils.js":{"bytesInOutput":189},"app/components/ui/use-toast.ts":{"bytesInOutput":2423},"app/components/ui/toaster.jsx":{"bytesInOutput":1839},"app/routes/_index.jsx":{"bytesInOutput":8529},"app/components/ui/button.jsx":{"bytesInOutput":2115},"app/components/ui/separator.jsx":{"bytesInOutput":786},"app/components/Sections/Projects.jsx":{"bytesInOutput":7361},"app/components/ui/calendar.jsx":{"bytesInOutput":3103},"app/components/ui/popover.jsx":{"bytesInOutput":1415},"app/components/DatePicker/DatePicker.jsx":{"bytesInOutput":3305},"app/components/ui/input.jsx":{"bytesInOutput":990},"app/components/ui/label.jsx":{"bytesInOutput":747},"app/components/ui/textarea.jsx":{"bytesInOutput":921},"app/components/ui/accordion.jsx":{"bytesInOutput":2556},"app/components/Sections/Socials.jsx":{"bytesInOutput":5168},"app/components/Sections/Skills.jsx":{"bytesInOutput":3831},"app/components/Sections/UserDetails.jsx":{"bytesInOutput":6257},"app/constants/general.js":{"bytesInOutput":1502},"app/components/Templates/InitialTemplate.jsx":{"bytesInOutput":11503},"app/components/Templates/ModernTemplate.jsx":{"bytesInOutput":10083},"app/components/TemplateRender/TemplateRender.jsx":{"bytesInOutput":975},"app/components/TemplatePicker/TemplatePicker.jsx":{"bytesInOutput":2152},"app/components/Sections/Education.jsx":{"bytesInOutput":6228},"server-assets-manifest:@remix-run/dev/assets-manifest":{"bytesInOutput":1024},"server-entry-module:@remix-run/dev/server-build":{"bytesInOutput":593}},"bytes":101075}}}
--------------------------------------------------------------------------------