diff --git a/.env.example b/.env.example index f606d153..9d0f15be 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ DATABASE_URL="postgres://postgres:postgres@db:5432/job-board-db" NEXTAUTH_SECRET="koXrQGB5TFDhiX47swxHW+4KALDX4kAvnQ5RHHvAOIzB" NEXTAUTH_URL="http://localhost:3000" - RAZORPAY_WEBHOOK_SECRET= RAZORPAY_ID= -RAZORPAY_SECRET= \ No newline at end of file +RAZORPAY_SECRET= +AWS_S3_REGION=your-aws-region +AWS_S3_ACCESS_KEY_ID=your-access-ID +AWS_S3_SECRET_ACCESS_KEY=your-access-key +AWS_S3_BUCKET_NAME=your-bucket diff --git a/next.config.js b/next.config.js index 5b4cce95..8e2fe13f 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,15 @@ const nextConfig = { fullUrl: true, }, }, + images: { + remotePatterns: [ + { + protocol: 'https', + //Add aws s3 bucket hostname + hostname: '', //example - youraws.s3.ap-south-2.amazonaws.com + }, + ], + }, }; module.exports = nextConfig; diff --git a/package.json b/package.json index 6e189553..0be342e2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.645.0", + "@aws-sdk/s3-request-presigner": "^3.645.0", "@hookform/resolvers": "^3.9.0", "@prisma/client": "5.18.0", "@radix-ui/react-accordion": "^1.2.0", @@ -39,6 +41,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dayjs": "^1.11.13", + "framer-motion": "^11.5.4", "lodash": "^4.17.21", "lucide-react": "^0.426.0", "next": "14.2.5", @@ -50,6 +53,7 @@ "react-dom": "^18", "react-hook-form": "^7.52.2", "react-icons": "^5.2.1", + "react-quill": "^2.0.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.1", diff --git a/prisma/migrations/20240902211016_/migration.sql b/prisma/migrations/20240902211016_/migration.sql deleted file mode 100644 index 3029dfde..00000000 --- a/prisma/migrations/20240902211016_/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -/* - Warnings: - - - Changed the type of `location` on the `Job` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. - -*/ --- CreateEnum -CREATE TYPE "JobLocations" AS ENUM ('BANGLORE', 'DELHI', 'MUMBAI', 'PUNE', 'CHENNAI', 'HYDERABAD', 'KOLKATA', 'AHMEDABAD', 'JAIPUR', 'SURAT'); - --- AlterTable -ALTER TABLE "Job" DROP COLUMN "location", -ADD COLUMN "location" "JobLocations" NOT NULL; - --- DropEnum -DROP TYPE "Location"; diff --git a/prisma/migrations/20240901095201_/migration.sql b/prisma/migrations/20240909111324_new_fields/migration.sql similarity index 77% rename from prisma/migrations/20240901095201_/migration.sql rename to prisma/migrations/20240909111324_new_fields/migration.sql index 3ac83c34..b49156f4 100644 --- a/prisma/migrations/20240901095201_/migration.sql +++ b/prisma/migrations/20240909111324_new_fields/migration.sql @@ -8,7 +8,7 @@ CREATE TYPE "WorkMode" AS ENUM ('remote', 'hybrid', 'office'); CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); -- CreateEnum -CREATE TYPE "Location" AS ENUM ('BANGLORE', 'DELHI', 'MUMBAI', 'PUNE', 'CHENNAI', 'HYDERABAD', 'KOLKATA', 'AHMEDABAD', 'JAIPUR', 'SURAT'); +CREATE TYPE "JobLocations" AS ENUM ('BANGLORE', 'DELHI', 'MUMBAI', 'CHENNAI', 'PUNE', 'HYDERABAD', 'KOLKATA', 'AHMEDABAD', 'JAIPUR', 'SURAT'); -- CreateTable CREATE TABLE "User" ( @@ -30,9 +30,15 @@ CREATE TABLE "Job" ( "title" TEXT NOT NULL, "description" TEXT, "company_name" TEXT NOT NULL, + "company_bio" TEXT NOT NULL, + "company_email" TEXT NOT NULL, + "category" TEXT NOT NULL, + "type" TEXT NOT NULL, "work_mode" "WorkMode" NOT NULL, "currency" "Currency" NOT NULL DEFAULT 'INR', - "location" "Location" NOT NULL, + "location" "JobLocations" NOT NULL, + "application" TEXT NOT NULL, + "companyLogo" TEXT NOT NULL, "has_salary_range" BOOLEAN NOT NULL DEFAULT false, "minSalary" INTEGER, "maxSalary" INTEGER, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 850468ce..e2f16026 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,9 +24,15 @@ model Job { title String description String? companyName String @map("company_name") + companyBio String @map("company_bio") + companyEmail String @map("company_email") + category String + type String workMode WorkMode @map("work_mode") currency Currency @default(INR) location JobLocations + application String + companyLogo String hasSalaryRange Boolean @default(false) @map("has_salary_range") minSalary Int? maxSalary Int? @@ -70,8 +76,8 @@ enum JobLocations { BANGLORE DELHI MUMBAI - PUNE CHENNAI + PUNE HYDERABAD KOLKATA AHMEDABAD diff --git a/prisma/seed.ts b/prisma/seed.ts index 81001261..347cfcfc 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -18,8 +18,15 @@ let jobs = [ title: 'Frontend Developer', description: 'Develop and maintain web applications.', companyName: 'Tech Corp', + companyBio: + 'Leading tech solutions provider specializing in innovative web development.', + companyEmail: 'contact@techcorp.com', + category: 'development', + type: 'full-time', workMode: WorkMode.remote, currency: Currency.USD, + application: 'apply@techcorp.com', + companyLogo: '', hasSalaryRange: true, minSalary: 60000, maxSalary: 80000, @@ -31,8 +38,15 @@ let jobs = [ title: 'Backend Developer', description: 'Build and maintain server-side logic.', companyName: 'Innovatech', + companyBio: + 'Innovatech specializes in backend systems and cloud-based solutions.', + companyEmail: 'careers@innovatech.com', + category: 'development', + type: 'full-time', workMode: WorkMode.office, - currency: Currency.INR, + currency: Currency.USD, + application: 'jobs@innovatech.com', + companyLogo: '', hasSalaryRange: false, minSalary: null, maxSalary: null, @@ -44,8 +58,15 @@ let jobs = [ title: 'Full Stack Developer', description: 'Develop both client-side and server-side software.', companyName: 'Global Solutions', + companyBio: + 'Global Solutions offers comprehensive IT services for businesses worldwide.', + companyEmail: 'recruitment@globalsolutions.com', + category: 'development', + type: 'full-time', workMode: WorkMode.hybrid, currency: Currency.USD, + application: 'careers@globalsolutions.com', + companyLogo: '', hasSalaryRange: true, minSalary: 90000, maxSalary: 120000, @@ -58,8 +79,15 @@ let jobs = [ description: 'Automate and streamline the company’s operations and processes.', companyName: 'DevOps Ltd.', + companyBio: + 'DevOps Ltd. specializes in automation and cloud infrastructure management.', + companyEmail: 'jobs@devopsltd.com', + category: 'development', + type: 'full-time', workMode: WorkMode.remote, - currency: Currency.INR, + currency: Currency.USD, + application: 'apply@devopsltd.com', + companyLogo: '', hasSalaryRange: true, minSalary: 50000, maxSalary: 70000, @@ -72,8 +100,15 @@ let jobs = [ description: 'Oversee product development and ensure the success of the product.', companyName: 'Productive Minds', + companyBio: + 'Productive Minds helps businesses achieve their goals through strategic product management.', + companyEmail: 'hr@productiveminds.com', + category: 'management', + type: 'full-time', workMode: WorkMode.hybrid, currency: Currency.USD, + application: 'careers@productiveminds.com', + companyLogo: '', hasSalaryRange: true, minSalary: 110000, maxSalary: 150000, @@ -86,8 +121,15 @@ let jobs = [ description: 'Analyze and interpret complex data to help the company make informed decisions.', companyName: 'Data Insights', + companyBio: + 'Data Insights provides data-driven solutions to empower businesses.', + companyEmail: 'recruitment@datainsights.com', + category: 'development', + type: 'full-time', workMode: WorkMode.office, - currency: Currency.INR, + currency: Currency.USD, + application: 'apply@datainsights.com', + companyLogo: '', hasSalaryRange: true, minSalary: 80000, maxSalary: 100000, @@ -100,8 +142,15 @@ let jobs = [ description: 'Design user-friendly interfaces for web and mobile applications.', companyName: 'Creative Designs', + companyBio: + 'Creative Designs excels in crafting intuitive and visually appealing user interfaces.', + companyEmail: 'careers@creativedesigns.com', + category: 'design', + type: 'full-time', workMode: WorkMode.remote, currency: Currency.USD, + application: 'jobs@creativedesigns.com', + companyLogo: '', hasSalaryRange: true, minSalary: 70000, maxSalary: 90000, @@ -113,8 +162,15 @@ let jobs = [ title: 'Mobile App Developer', description: 'Develop and maintain mobile applications.', companyName: 'App Innovators', + companyBio: + 'App Innovators is a leader in mobile application development and innovation.', + companyEmail: 'careers@appinnovators.com', + category: 'development', + type: 'full-time', workMode: WorkMode.hybrid, - currency: Currency.INR, + currency: Currency.USD, + application: 'apply@appinnovators.com', + companyLogo: '', hasSalaryRange: false, minSalary: null, maxSalary: null, @@ -126,8 +182,14 @@ let jobs = [ title: 'Cloud Engineer', description: 'Design and manage cloud-based systems and services.', companyName: 'Cloud Works', + companyBio: 'Cloud Works provides cutting-edge cloud computing solutions.', + companyEmail: 'hr@cloudworks.com', + category: 'development', + type: 'full-time', workMode: WorkMode.office, currency: Currency.USD, + application: 'careers@cloudworks.com', + companyLogo: '', hasSalaryRange: true, minSalary: 100000, maxSalary: 130000, @@ -139,8 +201,15 @@ let jobs = [ title: 'Security Analyst', description: 'Ensure the security and integrity of company systems.', companyName: 'SecureTech', + companyBio: + 'SecureTech specializes in cybersecurity solutions for modern businesses.', + companyEmail: 'security@securetech.com', + category: 'support', + type: 'full-time', workMode: WorkMode.remote, - currency: Currency.INR, + currency: Currency.USD, + application: 'jobs@securetech.com', + companyLogo: '', hasSalaryRange: true, minSalary: 75000, maxSalary: 95000, @@ -152,8 +221,15 @@ let jobs = [ title: 'QA Engineer', description: 'Ensure the quality of software products.', companyName: 'QA Solutions', + companyBio: + 'QA Solutions ensures top-notch quality assurance services for software.', + companyEmail: 'contact@qasolutions.com', + category: 'support', + type: 'full-time', workMode: WorkMode.remote, currency: Currency.USD, + application: 'apply@qasolutions.com', + companyLogo: '', hasSalaryRange: true, minSalary: 45000, maxSalary: 50000, @@ -163,249 +239,22 @@ let jobs = [ id: '12', userId: '2', title: 'Technical Writer', - description: 'Write technical documentation for software and hardware.', - companyName: 'Tech Docs', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 30000, - maxSalary: 35000, - isVerifiedJob: false, - }, - { - id: '13', - userId: '1', - title: 'IT Support Specialist', - description: 'Provide technical support for IT systems.', - companyName: 'Support Corp', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 20000, - maxSalary: 25000, - isVerifiedJob: true, - }, - { - id: '14', - userId: '2', - title: 'Network Administrator', - description: 'Manage and maintain network infrastructure.', - companyName: 'Net Admins', - workMode: WorkMode.remote, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 35000, - maxSalary: 40000, - isVerifiedJob: true, - }, - { - id: '15', - userId: '1', - title: 'System Analyst', - description: 'Analyze and improve IT systems.', - companyName: 'Sys Solutions', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 27000, - maxSalary: 32000, - isVerifiedJob: false, - }, - { - id: '16', - userId: '2', - title: 'Sales Engineer', - description: 'Support sales teams with technical expertise.', - companyName: 'Sales Tech', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 30000, - maxSalary: 35000, - isVerifiedJob: true, - }, - { - id: '17', - userId: '1', - title: 'Marketing Specialist', - description: 'Develop and execute marketing strategies.', - companyName: 'Market Pro', - workMode: WorkMode.remote, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 20000, - maxSalary: 25000, - isVerifiedJob: true, - }, - { - id: '18', - userId: '2', - title: 'Content Manager', - description: 'Manage and curate content for the company.', - companyName: 'Content Creators', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 25000, - maxSalary: 30000, - isVerifiedJob: false, - }, - { - id: '19', - userId: '1', - title: 'Graphic Designer', - description: 'Design visual content for digital and print media.', - companyName: 'Design Pros', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 22000, - maxSalary: 27000, - isVerifiedJob: true, - }, - { - id: '20', - userId: '2', - title: 'Business Analyst', - description: 'Analyze business processes and recommend improvements.', - companyName: 'Business Solutions', + description: 'Create and manage technical documentation.', + companyName: 'WriteTech', + companyBio: + 'WriteTech specializes in high-quality technical writing services.', + companyEmail: 'hr@writetech.com', + category: 'writing', + type: 'contract', workMode: WorkMode.remote, currency: Currency.USD, - hasSalaryRange: true, - minSalary: 38000, - maxSalary: 43000, - isVerifiedJob: true, - }, - { - id: '21', - userId: '1', - title: 'SEO Specialist', - description: 'Optimize websites for search engines.', - companyName: 'SEO Experts', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 15000, - maxSalary: 20000, - isVerifiedJob: false, - }, - { - id: '22', - userId: '2', - title: 'Data Analyst', - description: 'Analyze data to provide business insights.', - companyName: 'DataPro', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 23000, - maxSalary: 28000, - isVerifiedJob: true, - }, - { - id: '23', - userId: '1', - title: 'Operations Manager', - description: 'Oversee daily operations of the company.', - companyName: 'OpsCorp', - workMode: WorkMode.remote, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 29000, - maxSalary: 34000, - isVerifiedJob: true, - }, - { - id: '24', - userId: '2', - title: 'Customer Service Manager', - description: 'Manage customer service teams and operations.', - companyName: 'Customer Care Inc.', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 26000, - maxSalary: 31000, - isVerifiedJob: true, - }, - { - id: '25', - userId: '1', - title: 'Product Designer', - description: 'Design and develop new products.', - companyName: 'Product Innovators', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 32000, - maxSalary: 37000, - isVerifiedJob: true, - }, - { - id: '26', - userId: '2', - title: 'Social Media Manager', - description: 'Manage the company’s social media presence.', - companyName: 'Social Media Pros', - workMode: WorkMode.remote, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 18000, - maxSalary: 23000, - isVerifiedJob: true, - }, - { - id: '27', - userId: '1', - title: 'HR Specialist', - description: 'Manage human resources activities.', - companyName: 'HR Hub', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 24000, - maxSalary: 29000, - isVerifiedJob: true, - }, - { - id: '28', - userId: '2', - title: 'Supply Chain Manager', - description: 'Oversee supply chain operations.', - companyName: 'Supply Chain Solutions', - workMode: WorkMode.office, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 30000, - maxSalary: 35000, - isVerifiedJob: true, - }, - { - id: '29', - userId: '1', - title: 'E-commerce Manager', - description: 'Manage online sales and operations.', - companyName: 'E-commerce Pros', - workMode: WorkMode.remote, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 27000, - maxSalary: 32000, + application: 'careers@writetech.com', + companyLogo: '', + hasSalaryRange: false, + minSalary: null, + maxSalary: null, isVerifiedJob: true, }, - { - id: '30', - userId: '2', - title: 'Project Coordinator', - description: 'Assist in project management and coordination.', - companyName: 'Project Managers Inc.', - workMode: WorkMode.hybrid, - currency: Currency.USD, - hasSalaryRange: true, - minSalary: 12000, - maxSalary: 17000, - isVerifiedJob: false, - }, ]; async function seedUsers() { @@ -455,10 +304,16 @@ async function seedJobs() { title: j.title, description: j.description, companyName: j.companyName, + companyBio: j.companyBio, + companyEmail: j.companyEmail, + category: j.category, + type: j.type, workMode: j.workMode, currency: j.currency, + application: j.application, //@ts-ignore location: j.location, + companyLogo: j.companyLogo, hasSalaryRange: j.hasSalaryRange, minSalary: j.minSalary, maxSalary: j.maxSalary, diff --git a/src/actions/job.action.ts b/src/actions/job.action.ts index c466b1d6..85d0a57e 100644 --- a/src/actions/job.action.ts +++ b/src/actions/job.action.ts @@ -26,7 +26,13 @@ export const createJob = withServerActionAsyncCatcher< const result = JobPostSchema.parse(data); const { companyName, + companyBio, + companyEmail, + type, + category, + application, location, + companyLogo, title, workMode, description, @@ -41,10 +47,16 @@ export const createJob = withServerActionAsyncCatcher< title, description, companyName, + companyBio, + companyEmail, + type, + category, + application, hasSalaryRange, minSalary, maxSalary, location, + companyLogo, workMode, isVerifiedJob: false, // Default to false since there's no session to check for admin role }, @@ -86,6 +98,7 @@ export const getAllJobs = withServerActionAsyncCatcher< minSalary: true, maxSalary: true, postedAt: true, + companyLogo: true, }, }); const totalJobsPromise = prisma.job.count({ @@ -118,6 +131,9 @@ export const getJobById = withServerActionAsyncCatcher< title: true, description: true, companyName: true, + companyBio: true, + companyEmail: true, + companyLogo: true, location: true, workMode: true, minSalary: true, diff --git a/src/app/[...404]/page.tsx b/src/app/[...404]/page.tsx index dc33722b..42f7b062 100644 --- a/src/app/[...404]/page.tsx +++ b/src/app/[...404]/page.tsx @@ -1,8 +1,12 @@ +'use client'; import React from 'react'; import Link from 'next/link'; import { Poppins } from 'next/font/google'; +import { Home, AlertTriangle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { motion } from 'framer-motion'; -//applying font from google +// Applying font from Google const fontOptions = Poppins({ subsets: ['latin'], variable: '--font-poppins', @@ -11,29 +15,44 @@ const fontOptions = Poppins({ const Custom404Page = () => { return ( - <> -
+ -

- Oops! + + + +

+ 404 - Page Not Found

-

- We couldn’t find the page you’re looking for.
- 404 - PAGE NOT FOUND +

+ We are sorry, but we could not find the page you are looking for.

-

- The page you are looking for was moved, removed, renamed, or might - never have existed. +

+ The page may have been moved, removed, renamed, or might never have + existed.

- - Visit the homepage - -
- + + + + Return to Homepage + + + + + ); }; diff --git a/src/app/api/s3-upload/route.ts b/src/app/api/s3-upload/route.ts new file mode 100644 index 00000000..b3102bf3 --- /dev/null +++ b/src/app/api/s3-upload/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; + +const s3Client = new S3Client({ + region: process.env.AWS_S3_REGION!, + credentials: { + accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY!, + }, +}); + +export async function GET(req: Request): Promise { + const url = new URL(req.url); + const file = url.searchParams.get('file'); + const fileType = url.searchParams.get('fileType'); + const uniqueKey = url.searchParams.get('uniqueKey'); + + if (!file || !fileType || !uniqueKey) { + return NextResponse.json( + { error: 'File name and file type are required' }, + { status: 400 } + ); + } + + try { + const command = new PutObjectCommand({ + Bucket: process.env.AWS_S3_BUCKET_NAME!, + Key: uniqueKey, + ContentType: fileType, + }); + + const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 60 }); + + return NextResponse.json({ url: signedUrl }); + } catch (error) { + console.error('Error generating presigned URL:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx index 7b32f80e..92548bd3 100644 --- a/src/app/create/page.tsx +++ b/src/app/create/page.tsx @@ -3,7 +3,13 @@ import React from 'react'; const page = () => { return ( -
+
+
+

Post a job

+

+ 100xJobs is trusted by leading companies +

+
); diff --git a/src/app/globals.css b/src/app/globals.css index 6228a08a..b3c421ed 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -297,3 +297,51 @@ transform: rotate(360deg); } } + +/* Custom styles for Quill editor */ +.ql-toolbar.ql-snow { + border: none !important; + background-color: #1f2937 !important; + border-bottom: 1px solid #374151 !important; +} + +.ql-container.ql-snow { + border: none !important; + max-height: 56rem; /* Set a fixed height */ + overflow-y: hidden; /* Hide vertical overflow */ + overflow-x: auto; /* Allow horizontal scrolling */ +} + +.ql-editor { + min-height: 10px; + overflow-y: hidden; /* Prevent height increase */ + white-space: nowrap; /* Ensure long lines don't wrap */ +} + +.ql-editor.ql-blank::before { + color: #6b7280 !important; +} + +.ql-snow .ql-stroke { + stroke: #9ca3af !important; +} + +.ql-snow .ql-fill { + fill: #9ca3af !important; +} + +.ql-snow .ql-picker { + color: #9ca3af !important; +} + +/* Ensure the editor takes up full width of its container */ +.job-description-editor { + width: 100% !important; +} + +/* Ensure long content does not wrap within the editor */ +.job-description-editor .ql-editor { + white-space: nowrap !important; + overflow-x: hidden !important; /* Allow horizontal scrolling */ + overflow-y: hidden !important; /* Hide vertical overflow */ +} diff --git a/src/components/DescriptionEditor.tsx b/src/components/DescriptionEditor.tsx new file mode 100644 index 00000000..0bc0878f --- /dev/null +++ b/src/components/DescriptionEditor.tsx @@ -0,0 +1,63 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import dynamic from 'next/dynamic'; +const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }); +import 'react-quill/dist/quill.snow.css'; + +interface DescriptionEditorProps { + fieldName: string; + initialValue?: string; + onDescriptionChange: (fieldName: string, content: string) => void; + placeholder?: string; +} + +const DescriptionEditor: React.FC = ({ + fieldName, + initialValue = '', + onDescriptionChange, + placeholder = '', +}) => { + const [description, setDescription] = useState(initialValue || ''); + + useEffect(() => { + setDescription(initialValue || ''); + }, [initialValue]); + + const handleChange = (content: string) => { + setDescription(content); + onDescriptionChange(fieldName, content); // Pass the content back to the parent form + }; + + const modules = { + toolbar: [ + ['bold', 'italic', 'underline'], + [{ header: '1' }, { header: '2' }], + [{ list: 'ordered' }, { list: 'bullet' }], + ['link'], + ], + }; + + const formats = [ + 'bold', + 'italic', + 'underline', + 'header', + 'list', + 'bullet', + 'link', + ]; + + return ( + + ); +}; + +export default DescriptionEditor; diff --git a/src/components/job-form.tsx b/src/components/job-form.tsx index e550c21e..7896a035 100644 --- a/src/components/job-form.tsx +++ b/src/components/job-form.tsx @@ -1,5 +1,5 @@ 'use client'; -import { createJob } from '@/actions/job.action'; + import { Form, FormControl, @@ -16,7 +16,7 @@ import { SelectValue, } from '@/components/ui/select'; import { zodResolver } from '@hookform/resolvers/zod'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { JobPostSchema, @@ -24,34 +24,110 @@ import { } from '../lib/validators/jobs.validator'; import { Button } from './ui/button'; import { Input } from './ui/input'; -import { Textarea } from './ui/textarea'; -import { Label } from './ui/label'; -import { Switch } from './ui/switch'; import { useToast } from './ui/use-toast'; -import { WorkMode } from '@prisma/client'; import { Loader2 } from 'lucide-react'; import { useRazorpay } from '@/hooks/useRazorPay'; import { useRouter } from 'next/navigation'; - +import { Calendar, LucideRocket, MailOpenIcon } from 'lucide-react'; +import DescriptionEditor from './DescriptionEditor'; +import Image from 'next/image'; +import { FaFileUpload } from 'react-icons/fa'; +import { Switch } from './ui/switch'; +import { Label } from './ui/label'; const PostJobForm = () => { + const { toast } = useToast(); const router = useRouter(); const processPayment = useRazorpay(); + const [isLoading, setIsLoading] = useState(false); + + const companyLogoImg = useRef(null); const form = useForm({ resolver: zodResolver(JobPostSchema), defaultValues: { title: '', description: '', - companyName: '', + companyName: '', + companyBio: '', + companyEmail: '', location: undefined, - hasSalaryRange: false, + companyLogo: '', + workMode: 'remote', + type: 'full-time', + category: 'design', + hasSalaryRange: true, minSalary: 0, maxSalary: 0, + application: '', }, }); + + const handleClick = () => { + //@ts-ignore + document.getElementById('fileInput').click(); + }; + + const [file, setFile] = useState(null); + const [previewImg, setPreviewImg] = useState(null); + + const handleDescriptionChange = (fieldName: any, value: String) => { + form.setValue(fieldName, value); + }; + + const submitImage = async (file: File | null) => { + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); + + try { + const uniqueFileName = `${file.name}-${Date.now()}`; + const fileType = file.type; + + const res = await fetch( + `/api/s3-upload?file=${encodeURIComponent(file.name)}&fileType=${encodeURIComponent(fileType)}&uniqueKey=${encodeURIComponent(uniqueFileName)}` + ); + if (!res.ok) { + throw new Error('Failed to fetch presigned URL'); + } + + const { url: presignedUrl } = await res.json(); + const upload = await fetch(presignedUrl, { + method: 'PUT', + body: file, + headers: { 'Content-Type': fileType }, + }); + + if (!upload.ok) { + throw new Error('Upload failed'); + } + + const pubUrl = presignedUrl.split('?')[0]; + return pubUrl; + } catch (error) { + console.error('Image upload failed:', error); + } + }; + + const handleFileChange = async (e: any) => { + const selectedFile = e.target.files[0]; + const reader = new FileReader(); + reader.onload = () => { + if (companyLogoImg.current) { + companyLogoImg.current.src = reader.result as string; + } + setPreviewImg(reader.result as string); + }; + reader.readAsDataURL(selectedFile); + if (selectedFile) { + setFile(selectedFile); + } + }; + const handleFormSubmit = async (data: JobPostSchemaType) => { setIsLoading(true); + data.companyLogo = (await submitImage(file)) ?? ''; await processPayment({ amount: 1000, data, @@ -59,6 +135,8 @@ const PostJobForm = () => { router.push('/'); //Note: Check where to redirect after payment }, }); + setPreviewImg(null); + form.reset(form.formState.defaultValues); setIsLoading(false); }; const watchHasSalaryRange = form.watch('hasSalaryRange'); @@ -69,201 +147,377 @@ const PostJobForm = () => { form.setValue('minSalary', 0); form.setValue('maxSalary', 0); } + form.setValue('companyLogo', 'https://wwww.example.com'); }, [watchHasSalaryRange, form]); return ( -
- -
- ( - - - Job Title - - - - - - - )} - /> +
+
+
+ +

Posted for

+

30 days

-
- ( - - - Description - - -