Compare commits
22 Commits
536fdc63d2
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf7367c53 | ||
|
|
c0125af058 | ||
|
|
d620433990 | ||
|
|
45a2c8252e | ||
|
|
97b7e8726d | ||
|
|
256bd8a0ee | ||
|
|
10103fb1a1 | ||
|
|
7020cdeb4e | ||
|
|
ee75b0f2eb | ||
|
|
07038e720b | ||
|
|
e64435143b | ||
|
|
99e6e227e3 | ||
|
|
b3adb4f8fb | ||
|
|
2fddcf9ddd | ||
|
|
9e63b87ba6 | ||
|
|
11b1ff68e2 | ||
|
|
adbd76403f | ||
|
|
74f199ea8e | ||
|
|
3f8624efd6 | ||
|
|
a5f58d1bdb | ||
|
|
88adb39b19 | ||
|
|
4c6c4cf2da |
@@ -5,16 +5,15 @@ name: default
|
||||
steps:
|
||||
- name: build
|
||||
commands:
|
||||
- docker build --no-cache -t izaac-frontend:latest .
|
||||
- docker tag izaac-frontend:latest registry.izaac.pl:5000/izaac-frontend:latest
|
||||
- docker push registry.izaac.pl:5000/izaac-frontend:latest
|
||||
- docker build --no-cache -t izaac-frontend-master:latest .
|
||||
- docker tag izaac-frontend-master:latest registry.izaac.pl:5000/izaac-frontend-master:latest
|
||||
- docker push registry.izaac.pl:5000/izaac-frontend-master:latest
|
||||
|
||||
- name: delete
|
||||
environment:
|
||||
KUBECONFIG: /home/drone-runner/drone-kubeconfig
|
||||
commands:
|
||||
- kubectl delete deployment izaac-frontend || true
|
||||
- kubectl delete service izaac-frontend || true
|
||||
- kubectl delete ingress izaac-frontend || true
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: izaac-frontend
|
||||
image: registry.izaac.pl:5000/izaac-frontend:latest
|
||||
image: registry.knck.pl:5000/izaac-frontend-master:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
|
||||
10
eslint.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
|
||||
|
||||
|
||||
export default [
|
||||
{languageOptions: { globals: globals.browser }},
|
||||
pluginJs.configs.recommended,
|
||||
pluginReactConfig,
|
||||
];
|
||||
@@ -3,9 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link href="/dist/output.css" rel="stylesheet">
|
||||
<link href="/dist/output.css" rel="stylesheet" />
|
||||
<link rel="icon" href="../icon/favicon.ico"/>
|
||||
<!-- todo ICONs -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>izaac frontend</title>
|
||||
<title>Izaac - praca dla inżynierów</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
13328
package-lock.json
generated
13
package.json
@@ -6,27 +6,32 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint -c eslint.config.js --ext .js,.jsx,.ts,.tsx .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-build-classic": "^40.0.0",
|
||||
"@ckeditor/ckeditor5-build-multi-root": "^40.0.0",
|
||||
"@ckeditor/ckeditor5-react": "^6.1.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^43.2.0",
|
||||
"axios": "^0.24.0",
|
||||
"dompurify": "^3.0.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"globals": "^15.2.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"tailwindcss": "^3.3.5",
|
||||
|
||||
42
src/App.jsx
@@ -1,42 +1,44 @@
|
||||
import styles from './style';
|
||||
import NavBar from './components/NavBar';
|
||||
import WorkApp from './components/WorkApp';
|
||||
import Home from './components/Home';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Contact from './components/Contact';
|
||||
import JobPosting from './components/JobPosting';
|
||||
import Mininav from './components/Mininav';
|
||||
import Register from './components/Register';
|
||||
import styles from "./style";
|
||||
import NavBar from "./components/NavBar";
|
||||
import WorkApp from "./components/WorkApp";
|
||||
import Home from "./components/Home";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import Contact from "./components/Contact";
|
||||
import Mininav from "./components/Mininav";
|
||||
import AddJobListing from "./components/AddJobListing";
|
||||
import Cookies from "./components/Cookies";
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className='bg-white w-full overflow-hidden'>
|
||||
<div className={`bg-white w-full`}>
|
||||
<div className={`${styles.paddingX} ${styles.flexCenter}`}>
|
||||
<Mininav />
|
||||
{/* <Mininav /> */}
|
||||
<div className={`${styles.boxWidth} `}>
|
||||
<NavBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.flexStart}`}>
|
||||
<div className={`${styles.boxWidth2} h-full`}>
|
||||
<div className={`${styles.flexStart} mt-18 sm:mt-[4.5rem] md:mt-[6.5rem]`}>
|
||||
<div className={`${styles.boxWidth2} `}>
|
||||
<Routes>
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/work" element={<WorkApp />} />
|
||||
<Route path="/work/jobpostings" element={<WorkApp />} />
|
||||
<Route path="/work/joboffers" element={<WorkApp />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/work/jobposting" element={<JobPosting />} />
|
||||
<Route path="/work/addjoboffer" element={<AddJobListing />} />
|
||||
<Route path="/work/joboffers/:id" element={<WorkApp/>} />
|
||||
{/* <Route path="/employerpanel" element={<EmployerPanel />} /> */}
|
||||
{/* Add more routes as needed */}
|
||||
</Routes>
|
||||
<Cookies />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
};
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -2,9 +2,11 @@ import close from './close.svg'
|
||||
import menu from './menu.svg'
|
||||
import search from './search.svg'
|
||||
import IzaacLOGO from './IzaacLOGO.svg'
|
||||
import placeholderImage from './placeholderImage.svg'
|
||||
export {
|
||||
close,
|
||||
menu,
|
||||
search,
|
||||
IzaacLOGO
|
||||
IzaacLOGO,
|
||||
placeholderImage
|
||||
}
|
||||
23
src/assets/placeholderImage.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="800.000000pt" height="600.000000pt" viewBox="0 0 800.000000 600.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,600.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2780 3873 c-40 -14 -40 -4 -40 -883 0 -808 1 -848 18 -863 17 -16
|
||||
118 -17 1254 -17 1029 0 1237 2 1247 14 8 10 10 246 9 872 l-3 858 -25 13
|
||||
c-20 10 -278 13 -1235 12 -665 0 -1217 -3 -1225 -6z m2390 -878 l0 -775 -1160
|
||||
0 -1160 0 0 775 0 775 1160 0 1160 0 0 -775z"/>
|
||||
<path d="M3075 3596 c-64 -33 -97 -81 -103 -151 -12 -144 129 -245 260 -187
|
||||
67 29 101 80 106 157 5 77 -20 133 -77 170 -58 39 -124 43 -186 11z"/>
|
||||
<path d="M4208 3330 c-14 -10 -129 -161 -257 -335 -128 -173 -233 -315 -235
|
||||
-315 -2 0 -39 48 -82 108 -103 141 -126 162 -175 162 -31 0 -47 -7 -73 -32
|
||||
-31 -30 -476 -631 -476 -643 0 -3 493 -5 1096 -5 931 0 1095 2 1091 14 -3 8
|
||||
-173 242 -378 521 -226 307 -385 515 -404 526 -39 24 -76 24 -107 -1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
118
src/components/AddJobListing.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import StepOneJoblisting from './StepOneJoblisting';
|
||||
import StepTwoJoblisting from './StepTwoJoblisting';
|
||||
import StepThreeJoblisting from './StepThreeJoblisting';
|
||||
import StepFourJoblisting from './StepFourJoblisting'
|
||||
import Success from './Success';
|
||||
import axios from 'axios';
|
||||
|
||||
const AddJobListing = () => {
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [skills, setSkills] = useState([])
|
||||
const [formData, setFormData] = useState({
|
||||
'posting_option': 'S',
|
||||
'require_salary': true
|
||||
})
|
||||
const nextStep = () => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
const removeFields = (fieldNames) => {
|
||||
// Utwórz nowy obiekt, który początkowo jest kopią formData
|
||||
let newFormData = { ...formData };
|
||||
|
||||
// Iteruj przez każde pole do usunięcia
|
||||
fieldNames.forEach(fieldName => {
|
||||
// Usuń pole z nowego obiektu danych
|
||||
delete newFormData[fieldName];
|
||||
});
|
||||
|
||||
// Aktualizuj stan z nowym obiektem
|
||||
setFormData(newFormData);
|
||||
};
|
||||
|
||||
const handleChange = input => value => {
|
||||
// Sprawdzenie, czy 'value' jest obiektem zdarzenia
|
||||
const isEvent = value && value.target;
|
||||
let newValue;
|
||||
|
||||
if (isEvent) {
|
||||
// Jeśli 'value' jest obiektem zdarzenia, użyj jego właściwości 'value'
|
||||
newValue = value.target.value;
|
||||
} else {
|
||||
// W przeciwnym razie użyj 'value' bezpośrednio
|
||||
newValue = value;
|
||||
}
|
||||
|
||||
setFormData({ ...formData, [input]: newValue });
|
||||
console.log('Aktualny stan formularza:', formData);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const response = await axios.post('https://izaac.knck.pl/api/jobposting/joboffers/', formData);
|
||||
console.log('Data posted:', response.data);
|
||||
nextStep();
|
||||
} catch (error) {
|
||||
console.error("Error posting data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const getSkills = async () => {
|
||||
try {
|
||||
const response = await axios.get('https://izaac.knck.pl/api/jobposting/skills/');
|
||||
setSkills(response.data.sort((a, b) => a.skill_name.localeCompare(b.skill_name)));
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Aktualny stan formularza:', formData);
|
||||
}, [formData]);
|
||||
|
||||
useEffect(() => {
|
||||
getSkills();
|
||||
} , [currentStep])
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return <StepOneJoblisting
|
||||
nextStep={nextStep}
|
||||
handleChange={handleChange}
|
||||
formData={formData} />;
|
||||
case 2:
|
||||
return <StepFourJoblisting
|
||||
nextStep={nextStep}
|
||||
formData={formData}
|
||||
prevStep={prevStep}
|
||||
handleChange={handleChange} />;
|
||||
case 3:
|
||||
return <StepTwoJoblisting
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
removeFields={removeFields}
|
||||
setFormData={setFormData}
|
||||
handleChange={handleChange}
|
||||
formData={formData}
|
||||
skills={skills} />;
|
||||
case 4:
|
||||
return <StepThreeJoblisting
|
||||
prevStep={prevStep}
|
||||
handleSubmit={handleSubmit}
|
||||
formData={formData}
|
||||
skills={skills}
|
||||
/>;
|
||||
case 5:
|
||||
return <Success />;
|
||||
default:
|
||||
return <div>Unknown step</div>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default AddJobListing;
|
||||
17
src/components/Button.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
const Button = (props) => {
|
||||
return (
|
||||
<button
|
||||
disabled={props.isDiasbled}
|
||||
type="button"
|
||||
onClick={props.nextStep}
|
||||
className={`h-16 md:h-12 w-80 md:w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] text-white hover:scale-125 duration-300 ${props.isDiasbled ? 'cursor-not-allowed hover:scale-100 duration-300' : ''}`}
|
||||
>
|
||||
{props.Text}
|
||||
<span className="text-[18px]">→</span>{" "}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
25
src/components/Category.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const Category = (props) => {
|
||||
return (
|
||||
<div className='flex flex-shrink-0 items-center gap-6 mr-6'>
|
||||
<button onClick={props.func} className='mx-auto text-center text-l
|
||||
font-poppins font-semibold text-slate-800
|
||||
py-4 hover:scale-110 duration-200'
|
||||
href='' >
|
||||
{props.name}
|
||||
</button>
|
||||
<div className={`w-0.5 h-6 bg-black opacity-10
|
||||
${props.small || props.last && !props.small ? 'hidden' : 'block'}`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Category.propTypes = {
|
||||
name: propTypes.string,
|
||||
func: propTypes.func,
|
||||
small: propTypes.bool,
|
||||
last: propTypes.bool
|
||||
}
|
||||
|
||||
export default Category
|
||||
106
src/components/CategorySelect.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { categories } from '../consts';
|
||||
import { search } from '../assets';
|
||||
|
||||
const CategorySelect = props => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [selectedCategoryIds, setSelectedCategoryIds] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(suggestions);
|
||||
console.log(selectedCategoryIds);
|
||||
}, [suggestions, selectedCategoryIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchQuery && props.searchQuery.categories) {
|
||||
setSelectedCategoryIds(props.searchQuery.categories);
|
||||
}
|
||||
}, [props.searchQuery]);
|
||||
|
||||
// useEffect(() =>{
|
||||
// setSelectedCategoryIds([])
|
||||
// }, [props.clearSearchQuery])
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
if (value.length > 0) {
|
||||
setSuggestions(categories.filter(category =>
|
||||
category.name.toLowerCase().includes(value.toLowerCase()) &&
|
||||
!selectedCategoryIds.includes(category.id)
|
||||
));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
if (!selectedCategoryIds.includes(suggestion.id)) {
|
||||
const newSelectedIds = [...selectedCategoryIds, suggestion.id];
|
||||
setSelectedCategoryIds(newSelectedIds);
|
||||
props.setSearchQuery(prevState => ({...prevState, categories: newSelectedIds }));
|
||||
}
|
||||
setInputValue('');
|
||||
setSuggestions([]);
|
||||
}
|
||||
|
||||
const handleRemoveCategory = (id) => {
|
||||
const newSelectedIds = selectedCategoryIds.filter(prevId => prevId !== id);
|
||||
setSelectedCategoryIds(newSelectedIds);
|
||||
props.setSearchQuery({ categories: newSelectedIds });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-3'>
|
||||
<p className='text-center'>Branża</p>
|
||||
<div className='flex flex-wrap gap-y-2 mt-1 '>
|
||||
{selectedCategoryIds.map(id => {
|
||||
const category = categories.find(cat => cat.id === id);
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
onClick={() => handleRemoveCategory(id)}
|
||||
className='min-w-fit w-8 py-1 px-2 mr-3 border-2 hover:bg-gray-300 rounded-xl hover:cursor-pointer '
|
||||
>
|
||||
<p className='font-poppins text-sm'>{category.name}
|
||||
<span className='relative -top-[2px] left-1'>×</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className='border-2 rounded-lg px-3 py-1 w-full mt-2'
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder='Wpisz branżę...'
|
||||
/>
|
||||
{inputValue.length >= 1 && suggestions.length > 0 && (
|
||||
<div className='absolute h-40 w-[90%] overflow-y-auto'>
|
||||
{suggestions.map(suggestion => (
|
||||
<div
|
||||
className='mx-3 p-3 border-2 border-gray-300 bg-gray-100 hover:bg-gray-200 cursor-pointer'
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
>
|
||||
{suggestion.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CategorySelect.propTypes = {
|
||||
setSearchQuery: PropTypes.func.isRequired,
|
||||
searchQuery: PropTypes.shape({
|
||||
categories: PropTypes.arrayOf(PropTypes.string)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
export default CategorySelect
|
||||
29
src/components/Cookies.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import propTypes from 'prop-types'
|
||||
import { useState } from 'react'
|
||||
const Cookies = props => {
|
||||
const handleCookies = () => {
|
||||
localStorage.setItem('cookies', true)
|
||||
}
|
||||
const cookies = localStorage.getItem('cookies')
|
||||
// if(cookies) return null
|
||||
const [show, setShow] = useState(true)
|
||||
if (cookies) return null;
|
||||
if (!show) return null;
|
||||
return (
|
||||
<>
|
||||
<div className='bg-neutral-900 opacity-[99%] h-24 sm:h-16 w-full fixed bottom-0 z-40 flex justify-center items-center'>
|
||||
<p className='mx-5 text-justify text-white font-'>Korzystamy z plików cookie, aby zapewnić prawidłowe działanie witryny. </p>
|
||||
<button className= 'bg-white px-4 py-2 rounded-md ml-4 mr-4 ring-2 ring-dimWhite hover:ring-4 duration-300 font-semibold'
|
||||
onClick={() => {handleCookies; setShow(!show)}}>
|
||||
Wyrażenie zgody</button>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Cookies.propTypes = {
|
||||
|
||||
}
|
||||
|
||||
export default Cookies
|
||||
96
src/components/EmployerPanel.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
import { ogloszenia } from "../consts";
|
||||
import ListingSmall from "./ListingSmall";
|
||||
import { useState } from "react";
|
||||
import SkillRender from "./SkillRender";
|
||||
const EmployerPanel = () => {
|
||||
const [showActions, setShowActions] = useState(true);
|
||||
const [selectedOgloszenie, setSelectedOgloszenie] = useState(ogloszenia[0]);
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false); // Dodany stan
|
||||
|
||||
const handleOgloszenieClick = (ogloszenie) => {
|
||||
setSelectedOgloszenie(ogloszenie);
|
||||
setIsDetailsVisible(true); // Pokaż szczegóły na urządzeniach mobilnych
|
||||
};
|
||||
|
||||
// Funkcja do powrotu do listy ogłoszeń
|
||||
const handleBackToList = () => {
|
||||
setIsDetailsVisible(false);
|
||||
};
|
||||
const handleOnClick = () => {
|
||||
setShowActions(showActions);
|
||||
handleOgloszenieClick(selectedOgloszenie);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-10 pt-2 grid grid-cols-3 relative">
|
||||
<div className={`md:col-span-1 col-span-3 ${isDetailsVisible ? 'hidden md:block' : 'block'} h-[80vh] overflow-y-auto`}>
|
||||
<p className="sticky top-2 z-50 h-16 text-center font-bold text-[32px] mr-3 mt-2 mb-2 bg-gray-300 rounded-xl text-gray-800 p-2 ">
|
||||
Twoje ogłoszenia
|
||||
</p>
|
||||
{ogloszenia.map((ogloszenie, index) => (
|
||||
<ListingSmall
|
||||
key={ogloszenie.id}
|
||||
name={ogloszenie.name}
|
||||
company_name={ogloszenie.company_name}
|
||||
salaryrange={ogloszenie.salaryRange}
|
||||
requiresalary={ogloszenie.requiresalary}
|
||||
index={index}
|
||||
onClick={() => handleOgloszenieClick(ogloszenie)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="md:col-span-2 col-span-3">
|
||||
{isDetailsVisible && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 place-items-center">
|
||||
<button className="font-bold sm:w-[80%] w-[90%] px-1 sm:h-12 h-16 ml-6 border-2 rounded-xl mt-6 bg-gray-300 hover:bg-gray-400 duration-300">
|
||||
Przedłuż ważność ogłoszenia
|
||||
</button>
|
||||
<button className="font-bold sm:w-[80%] w-[90%] px-1 sm:h-12 h-16 ml-6 border-2 rounded-xl mt-6 bg-gray-300 hover:bg-gray-400 duration-300">
|
||||
Wyróżnij ogłoszenie
|
||||
</button>
|
||||
<button className="font-bold sm:w-[80%] w-[90%] px-1 sm:h-12 h-16 ml-6 border-2 rounded-xl mt-6 bg-gray-300 hover:bg-gray-400 duration-300">
|
||||
Zakończ ogłoszenie
|
||||
</button>
|
||||
<button className="font-bold sm:w-[80%] w-[90%] px-1 sm:h-12 h-16 ml-6 border-2 rounded-xl mt-6 bg-gray-300 hover:bg-gray-400 duration-300">
|
||||
Dodaj nowe ogłoszenie
|
||||
</button>
|
||||
{isDetailsVisible && (
|
||||
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="md:hidden block font-poppins text-white font-semibold bg-gray-500 px-4 py-2 rounded-md mx-4 mt-6 w-[90%] col-span-2 place-self-center"
|
||||
>
|
||||
Powrót do listy
|
||||
</button>
|
||||
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`bg-gray-200 md:h-[68vh] h-[95%] overflow-y-auto p-4 my-4 ${
|
||||
isDetailsVisible ? "block" : "hidden md:block"
|
||||
}`}
|
||||
>
|
||||
<div className="grid md:grid-cols-5 sm:grid-cols-4 grid-cols-2">
|
||||
{Object.entries(selectedOgloszenie.neededSkills).map(
|
||||
([skill, level]) => (
|
||||
<SkillRender skill={skill} level={level} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="text-slate-800 font-medium mt-4">
|
||||
{selectedOgloszenie.tresc}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployerPanel;
|
||||
185
src/components/Filter.jsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Search from './Search';
|
||||
import Selector from './Selector';
|
||||
import RangeSlider from './RangeSlider';
|
||||
import CategorySelect from './CategorySelect';
|
||||
import { categories, work_from_home, employment_types } from '../consts';
|
||||
|
||||
const Filter = ({ isOpen, searchQuery, setSearchQuery, clearSearchQuery, onClick }) => {
|
||||
const [min_salary, max_salary] = [0, 20000];
|
||||
|
||||
useEffect(() => {
|
||||
console.log(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
|
||||
const handleRangeChange = useCallback((newMin, newMax) => {
|
||||
setSearchQuery(prevState => ({
|
||||
...prevState,
|
||||
min_salary: newMin,
|
||||
max_salary: newMax
|
||||
}));
|
||||
}, [setSearchQuery]);
|
||||
|
||||
const handleChange = useCallback((e, name) => {
|
||||
const value = e.target.value;
|
||||
const numValue = parseInt(value, 10);
|
||||
|
||||
setSearchQuery(prevState => {
|
||||
switch (name) {
|
||||
case 'name':
|
||||
return { ...prevState, name: value };
|
||||
case 'min_salary':
|
||||
if (value === '' || isNaN(numValue)) {
|
||||
return { ...prevState, min_salary: '' };
|
||||
}
|
||||
return { ...prevState, min_salary: numValue };
|
||||
case 'max_salary':
|
||||
if (value === '' || isNaN(numValue)) {
|
||||
return { ...prevState, max_salary: '' };
|
||||
}
|
||||
return { ...prevState, max_salary: numValue };
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
});
|
||||
}, [setSearchQuery]);
|
||||
|
||||
const handleCheckedChange = useCallback((e) => {
|
||||
const { name, value, checked } = e.target;
|
||||
setSearchQuery(prevState => {
|
||||
// Sprawdź, czy dla danej nazwy już istnieje tablica w stanie
|
||||
if (!prevState[name]) {
|
||||
prevState[name] = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Jeśli checkbox jest zaznaczony, dodaj wartość do odpowiedniej tablicy
|
||||
return {
|
||||
...prevState,
|
||||
[name]: [...prevState[name], value]
|
||||
};
|
||||
} else {
|
||||
// Jeśli checkbox jest odznaczony, usuń wartość z odpowiedniej tablicy
|
||||
return {
|
||||
...prevState,
|
||||
[name]: prevState[name].filter(item => item !== value)
|
||||
};
|
||||
}
|
||||
});
|
||||
}, [setSearchQuery]);
|
||||
|
||||
const filterButtonClass = useMemo(() => (
|
||||
'bg-gray-600 text-white py-1 sm:py-3 px-12 mx-auto sm:mx-40 col-span-2 rounded-md font-semibold ' +
|
||||
'text-xl hover:bg-gray-500 duration-300 hover:scale-110 my-3 sm:my-5 sm:min-h-full'
|
||||
), []);
|
||||
|
||||
return (
|
||||
<div className={`z-10 flex flex-col bg-dimWhite collapsible px-3 sm:px-4 ${isOpen ? 'expanded border-4' : ''}`}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 mb-2">
|
||||
<div className="hidden md:block" />
|
||||
<p className="mx-auto mt-3 place-self-center text-center font-poppins font-semibold text-xl text-gray-600">Filtry</p>
|
||||
<button
|
||||
className="rounded-xl mt-4 w-32 py-1 bg-slate-400 hover:bg-slate-600 duration-100"
|
||||
onClick={clearSearchQuery}
|
||||
>
|
||||
<span className="p-2 text-sm font-bold text-center font-poppins text-white">Wyczyść filtry</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Search
|
||||
label="Wyszukaj ogłoszenie"
|
||||
placeholder="Wpisz nazwę stanowiska..."
|
||||
name="name"
|
||||
type="text"
|
||||
value={searchQuery.name || ''}
|
||||
onChange={(e) => handleChange(e, 'name')}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Search
|
||||
label="Min. wynagrodzenie"
|
||||
label2="wynagrodzenie"
|
||||
placeholder="Wpisz kwotę minimalną..."
|
||||
name="min_salary"
|
||||
type="text"
|
||||
value={searchQuery.min_salary || ''}
|
||||
onChange={(e) => handleChange(e, 'min_salary')}
|
||||
/>
|
||||
<Search
|
||||
label="Max. wynagrodzenie"
|
||||
placeholder="Wpisz kwotę maksymalną..."
|
||||
name="max_salary"
|
||||
type="text"
|
||||
value={searchQuery.max_salary || ''}
|
||||
onChange={(e) => handleChange(e, 'max_salary')}
|
||||
/>
|
||||
<RangeSlider
|
||||
min={min_salary}
|
||||
max={max_salary}
|
||||
minVal={searchQuery.min_salary || min_salary}
|
||||
maxVal={searchQuery.max_salary || max_salary}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onRangeChange={handleRangeChange}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
<CategorySelect
|
||||
setSearchQuery = {setSearchQuery}
|
||||
searchQuery = {searchQuery}
|
||||
clearSearchQuery = {clearSearchQuery}
|
||||
/>
|
||||
{/* <Selector
|
||||
value_to_map_from={categories}
|
||||
name="Kategorie"
|
||||
inputname="categories"
|
||||
onChange={handleCheckedChange}
|
||||
state={searchQuery || {}}
|
||||
/> */}
|
||||
{/* <Selector
|
||||
value_to_map_from={work_from_home}
|
||||
name="Praca zdalna"
|
||||
inputname="employment_types"
|
||||
onChange={handleCheckedChange}
|
||||
state={searchQuery || {}}
|
||||
/>
|
||||
<Selector
|
||||
value_to_map_from={employment_types}
|
||||
name="Typ kontraktu"
|
||||
inputname="employment_types"
|
||||
onChange={handleCheckedChange}
|
||||
state={searchQuery || {}}
|
||||
/> */}
|
||||
|
||||
<button className={filterButtonClass} onClick={onClick}>
|
||||
Filtruj
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
searchQuery: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
min_salary: PropTypes.number,
|
||||
max_salary: PropTypes.number,
|
||||
categories: PropTypes.arrayOf(PropTypes.string),
|
||||
work_from_home: PropTypes.arrayOf(PropTypes.string),
|
||||
employment_types: PropTypes.arrayOf(PropTypes.string)
|
||||
}),
|
||||
setSearchQuery: PropTypes.func.isRequired,
|
||||
clearSearchQuery: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Filter.defaultProps = {
|
||||
searchQuery: {
|
||||
categories: [],
|
||||
work_from_home: [],
|
||||
employment_types: []
|
||||
}
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from '../style'
|
||||
const Home = () => {
|
||||
return (
|
||||
|
||||
91
src/components/ImageUpload.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { placeholderImage } from '../assets';
|
||||
import axios from 'axios';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const ImageUpload = ({setFormData, data}) => {
|
||||
const [imageSrc, setImageSrc] = useState(placeholderImage);
|
||||
const fileInputRef = useRef(); // Referencja do ukrytego inputu plików
|
||||
const [uploadedImageData, setUploadedImageData] = useState(null);
|
||||
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
// Aktualizujemy stan obrazkiem użytkownika
|
||||
const uploadedImageData = await uploadImage(file);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDivClick = () => {
|
||||
fileInputRef.current.click(); // Programowe kliknięcie na ukrytym inpucie plików
|
||||
};
|
||||
|
||||
const uploadImage = async (imageFile) => {
|
||||
const formData = new FormData();
|
||||
formData.append('company_logo', imageFile);
|
||||
formData.append('company_name', data.company_name)
|
||||
try {
|
||||
const response = await axios.post('https://izaac.knck.pl/api/jobposting/companylogo/', formData, {
|
||||
headers: {
|
||||
"Content-Type": 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
console.log(response)
|
||||
const url = response.data.company_logo
|
||||
setImageSrc(url)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Błąd podczas przesyłania obrazka:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Aktualizujemy dane w hooku nadrzędnym za każdym razem, gdy zmienia się imageSrc
|
||||
if (imageSrc !== placeholderImage) {
|
||||
setFormData(prevFormData => ({
|
||||
...prevFormData,
|
||||
image: imageSrc
|
||||
}));
|
||||
}
|
||||
}, [imageSrc, setFormData]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={handleDivClick}
|
||||
className='h-[12rem] rounded-lg border-[0.3rem] border-slate-300'
|
||||
style={{
|
||||
backgroundImage: `url(${imageSrc})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
accept="image/png, image/jpeg"
|
||||
onChange={handleImageChange}
|
||||
style={{ display: 'none' }} // Ukryj input plików
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
setFormData: propTypes.func,
|
||||
data: propTypes.object
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
66
src/components/JobOfferContent.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import DOMPurify from 'dompurify';
|
||||
import SkillRender from './SkillRender';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const JobOfferContent = ({ id, skills }) => {
|
||||
const [jobOffer, setJobOffer] = useState(null);
|
||||
|
||||
const fetchJobOfferById = async (jobId) => {
|
||||
try {
|
||||
const response = await axios.get(`https://izaac.knck.pl/api/jobposting/joboffers/${jobId}/?format=json`);
|
||||
setJobOffer(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job offer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const processHTML = (htmlString) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
doc.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((tag) => {
|
||||
tag.classList.add('no-tailwindcss-base', 'text-xl');
|
||||
});
|
||||
doc.querySelectorAll('p').forEach((tag) => {
|
||||
tag.classList.add('no-tailwindcss-base', 'text-slate-800', 'text-lg');
|
||||
});
|
||||
doc.querySelectorAll('ol, ul').forEach((tag) => tag.classList.add('no-tailwindcss-base'));
|
||||
doc.querySelectorAll('li').forEach((tag) => tag.classList.add('lista'));
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
const cleanAndProcessData = (userInput) => {
|
||||
const sanitizedHTML = DOMPurify.sanitize(userInput, { USE_PROFILES: { html: true } });
|
||||
return processHTML(sanitizedHTML);
|
||||
};
|
||||
|
||||
const renderContent = (htmlString) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: cleanAndProcessData(htmlString) }} />;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobOfferById(id);
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{jobOffer && (
|
||||
<>
|
||||
<h1 className="my-4 mx-6 text-3xl font-bold text-slate-800">{jobOffer.name} w {jobOffer.company_name}</h1>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-4">
|
||||
{jobOffer.skill_levels && jobOffer.skill_levels.map((skillLevel) => {
|
||||
const skill = skills.find((s) => s.id === parseInt(skillLevel.skill_id));
|
||||
return skill ? (
|
||||
<SkillRender key={skill.id} skill={skill.skill_name} level={skillLevel.skill_level} />
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
<div className="text-slate-800 font-medium mt-4">{renderContent(jobOffer.content)}</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobOfferContent;
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import styles from '../style'
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
|
||||
const JobPosting = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-3'>
|
||||
<div className='sm:px-20 px-16 sm:pt-12 pt-6 pb-6 xl:col-span-2 col-span-3'>
|
||||
<span className='justify-center'>
|
||||
<p className={`${styles.paragraph}`}>Zacznij dodawać ogłoszenie o pracę z Izaac.pl </p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className='no-tailwindcss-base'>
|
||||
<div className='editor-container max-w-none h-[70vh]'>
|
||||
<form action="">
|
||||
<div className='grid mb-4'>
|
||||
<label className={`${styles.paragraph} px-4`} htmlFor="">Nazwa ogłoszenia</label>
|
||||
<input type="text" name="" id="" placeholder='Wpisz nazwę stanowiska...' className={`border-b-2 px-4 my-3 mx-2 ${styles.paragraph} `}/>
|
||||
</div>
|
||||
<CKEditor
|
||||
editor={ ClassicEditor }
|
||||
className=""
|
||||
config={{}}
|
||||
data="<h1>Zacznij pisać ogłoszenie!</h1>"
|
||||
onReady={ editor => {
|
||||
// You can store the "editor" and use when it is needed.
|
||||
console.log( 'Editor is ready to use!', editor );
|
||||
} }
|
||||
onChange={ ( event, editor ) => {
|
||||
const data = editor.getData();
|
||||
console.log( { event, editor, data } );
|
||||
} }
|
||||
onBlur={ ( event, editor ) => {
|
||||
console.log( 'Blur.', editor );
|
||||
} }
|
||||
onFocus={ ( event, editor ) => {
|
||||
console.log( 'Focus.', editor );
|
||||
} }
|
||||
/>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobPosting
|
||||
43
src/components/ListingSmall.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
import { IzaacLOGO } from '../assets'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const ListingSmall = ({ id, name, company_name, min_salary, max_salary, require_salary, index, onClick, _class, image, selected, blur }) => {
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`flex flex-row ml-1 w-[98vw] sm:w-full items-center gap-4 drop-shadow cursor-pointer mb-2 sm:mr-4 mr-4 text-xl font-bold border rounded-[10px] p-2 sm:px-4 hover:border-l-8 hover:border-zinc-500 duration-300 blur-transition ${index % 2 === 0 ? 'bg-gray-200' : 'bg-gray-300'} ${_class} ${selected ? 'border-l-8 border-zinc-500 bg-gray-400' : 'border-l-8 border-transparent'} `}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex-shrink-0 w-[6rem] h-[6rem] bg-white rounded-lg flex items-center justify-center'>
|
||||
{/* Obrazek jest wyśrodkowany i zachowuje proporcje */}
|
||||
<img src={image || IzaacLOGO} alt={name} className='object-contain object-center p-2' />
|
||||
</div>
|
||||
<div className='flex-grow flex flex-col justify-between'>
|
||||
<p className='text-base font-bold text-left tracking-wide'>{name}</p>
|
||||
<p className='text-xs text-left text-gray-600'>{company_name}</p>
|
||||
{/* Wyświetlenie wynagrodzenia pod nazwą, jeśli jest wymagane */}
|
||||
{require_salary && (
|
||||
<p className='text-xs font-semibold text-left tracking-widest text-slate-800 mt-2'>{min_salary} - {max_salary} PLN</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListingSmall;
|
||||
|
||||
ListingSmall.propTypes = {
|
||||
id: propTypes.number,
|
||||
name: propTypes.string,
|
||||
company_name: propTypes.string,
|
||||
min_salary: propTypes.number,
|
||||
max_salary: propTypes.number,
|
||||
require_salary: propTypes.bool,
|
||||
index: propTypes.number,
|
||||
onClick: propTypes.func,
|
||||
_class: propTypes.string,
|
||||
image: propTypes.string,
|
||||
selected: propTypes.bool,
|
||||
blur: propTypes.string
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import propTypes from 'prop-types';
|
||||
import styles from '../style';
|
||||
|
||||
const Login = ({isOpen, onClose}) => {
|
||||
@@ -31,4 +31,9 @@ const Login = ({isOpen, onClose}) => {
|
||||
)
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
isOpen: propTypes.bool,
|
||||
onClose: propTypes.func
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Register from './Register';
|
||||
import Login from './Login';
|
||||
|
||||
|
||||
const Mininav = () => {
|
||||
const [isPopupOpen, setPopupOpen] = useState(false)
|
||||
const [isLoginOpen, setLoginOpen] = useState(false)
|
||||
@@ -13,14 +14,16 @@ const Mininav = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='bg-gray-500 h-5 absolute w-full top-0 flex justify-end items-center'>
|
||||
<div className='bg-gray-500 h-5 absolute w-full top-0 flex justify-end items-center z-50'>
|
||||
{/* Link otwierający modal */}
|
||||
{/* <a href="/employerpanel" className='text-white font-poppins font-semibold text-xs'>Panel pracodawcy</a> */}
|
||||
<button onClick={openPopup} className='text-white font-poppins font-semibold text-xs mx-12'>
|
||||
Rejestracja
|
||||
</button>
|
||||
<button onClick={openLogin} className='text-white font-poppins font-semibold text-xs sm:mr-12 mr-6 sm:pr-12'>
|
||||
Zaloguj się
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{/* Komponent modalu */}
|
||||
<Register isOpen={isPopupOpen} onClose={closePopup} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import { linki, linki_home } from '../consts';
|
||||
import { close, search, menu, IzaacLOGO} from '../assets';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Mininav from './Mininav';
|
||||
|
||||
const NavBar = () => {
|
||||
const[toggle, setToggle] = useState(false)
|
||||
@@ -20,15 +20,18 @@ const NavBar = () => {
|
||||
const currentLinks = getLinks()
|
||||
|
||||
return (
|
||||
<nav className='w-full flex py-6 justify-between items-center navbar '>
|
||||
|
||||
<nav className='fixed top-0 left-0 z-30 w-full flex pt-4 justify-between items-center navbar bg-white px-8 border-b-2 xl:px-64'>
|
||||
<a href='/' className='font-popins font-semibold text-[24px] text-slate-900'>
|
||||
<img src={IzaacLOGO} alt="izaac" className='md:w-[200px] w-[120px]'/>
|
||||
|
||||
</a>
|
||||
<ul className='list-none sm:flex hidden justify-end items-center flex-1'>
|
||||
{currentLinks.map((nav, index) => (
|
||||
<li
|
||||
key={nav.id}
|
||||
className={`font-popins font-semibold cursor-pointer xs:text-[18px] sm:text-[14px] lg:text-[18px] text-[10px] ${index === linki.length - 1 ? 'mr-7' : 'sm:mr-7 mr-10'} text-slate-900 hover:text-sky-600`}
|
||||
className={`font-popins font-semibold cursor-pointer
|
||||
xs:text-[18px] sm:text-[14px] lg:text-[18px] text-[10px]
|
||||
${index === linki.length - 1 ? 'mr-7' : 'sm:mr-7 mr-10'}
|
||||
text-slate-900 hover:text-sky-600`}
|
||||
>
|
||||
<Link to={`/${nav.name}`}>{nav.title}</Link>
|
||||
</li>
|
||||
@@ -50,12 +53,18 @@ const NavBar = () => {
|
||||
alt="menu"
|
||||
className='w-[28px] h-[28px] object-contain'
|
||||
onClick={() => setToggle((prev) => !prev)}/>
|
||||
<div className={`${toggle ? 'sidebar-show' : 'hidden'} p-6 bg-white absolute top-20 right-0 mx-4 my-2 min-w-[140px] rounded-xl border-4 border-slate-200 z-20`}>
|
||||
<ul className='list-none flex flex-col justify-end items-center flex-1'>
|
||||
<div className={`${toggle ? 'sidebar-show' : 'hidden'} p-6
|
||||
bg-white absolute top-20 right-0 mx-4 my-2 min-w-[140px]
|
||||
rounded-xl border-4 border-slate-200 z-20`}>
|
||||
<ul className='list-none flex flex-col justify-end
|
||||
items-center flex-1'>
|
||||
{currentLinks.map((nav, index) =>(
|
||||
<div
|
||||
key={nav.id}
|
||||
className={`w fulltext-center font-popins font-normal cursor-pointer text-[16px] ${index === linki.length - 1 ? 'mb-0' : 'mb-3'} text-slate-800`}
|
||||
className={`w fulltext-center font-popins font-normal
|
||||
cursor-pointer text-[16px]
|
||||
${index === linki.length - 1 ? 'mb-0' : 'mb-3'}
|
||||
text-slate-800`}
|
||||
>
|
||||
<Link to={`/${nav.name}`}>{nav.title}</Link>
|
||||
</div>
|
||||
@@ -67,5 +76,4 @@ const NavBar = () => {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default NavBar
|
||||
|
||||
76
src/components/RangeSlider.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
|
||||
const RangeSlider = ({ min, max, minVal, maxVal, setSearchQuery, onRangeChange, isOpen }) => {
|
||||
const minValRef = useRef(minVal);
|
||||
const maxValRef = useRef(maxVal);
|
||||
const range = useRef(null);
|
||||
|
||||
const getPercent = useCallback(
|
||||
(value) => Math.round(((value - min) / (max - min)) * 100),
|
||||
[min, max]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const minPercent = getPercent(minVal === '' ? min : minVal);
|
||||
const maxPercent = getPercent(maxVal === '' ? max : maxVal);
|
||||
|
||||
if (range.current) {
|
||||
range.current.style.left = `${minPercent}%`;
|
||||
range.current.style.width = `${maxPercent - minPercent}%`;
|
||||
}
|
||||
}, [minVal, maxVal, getPercent, min, max]);
|
||||
|
||||
const handleChange = useCallback((event) => {
|
||||
const { id, value } = event.target;
|
||||
const numValue = Number(value);
|
||||
|
||||
let newMinVal = minVal === '' ? min : minVal;
|
||||
let newMaxVal = maxVal === '' ? max : maxVal;
|
||||
|
||||
if (id === 'range1') {
|
||||
newMinVal = numValue;
|
||||
minValRef.current = newMinVal;
|
||||
} else if (id === 'range2') {
|
||||
newMaxVal = numValue;
|
||||
maxValRef.current = newMaxVal;
|
||||
}
|
||||
|
||||
onRangeChange(newMinVal, newMaxVal);
|
||||
}, [minVal, maxVal, min, max, onRangeChange]);
|
||||
|
||||
return (
|
||||
<div className='col-span-2 my-5 px-8 slider-wrapper'>
|
||||
<input
|
||||
id="range1"
|
||||
type="range"
|
||||
min={min}
|
||||
step={100}
|
||||
max={max}
|
||||
value={minVal === '' ? min : minVal}
|
||||
onChange={handleChange}
|
||||
className={`thumb w-[85%] sm:w-[84%] thumb--left thumb--animatedin thumb--animatedout`}
|
||||
style={isOpen ? { zIndex: (minVal === '' ? min : minVal) > (maxVal === '' ? max - 100 : maxVal - 100) ? 5 : undefined, } : { display: 'none' }}
|
||||
/>
|
||||
<input
|
||||
id="range2"
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={100}
|
||||
value={maxVal === '' ? max : maxVal}
|
||||
onChange={handleChange}
|
||||
className={`thumb w-[85%] sm:w-[84%] thumb--right thumb--animatedin thumb--animatedout sm`}
|
||||
style={isOpen ? {} : { display: 'none'}}
|
||||
/>
|
||||
|
||||
<div className="slider" >
|
||||
<div className="slider__track " />
|
||||
<div ref={range} className="slider__range" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RangeSlider;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import styles from '../style';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const Register = ({isOpen, onClose}) => {
|
||||
if (!isOpen) return null;
|
||||
@@ -25,10 +25,14 @@ const Register = ({isOpen, onClose}) => {
|
||||
<div className='mb-6 pr-4'>
|
||||
<input type="password" name="" id="" placeholder='Powtórz hasło' className={`border-b px-4 my-3 mx-2 ${styles.paragraph} w-full text-center `}/>
|
||||
</div>
|
||||
<div className='mb-6 pr-4'>
|
||||
<div className='pr-4'>
|
||||
<input type="checkbox" name="" id="" placeholder='' className={`border-b-2 px-4 my-3 mx-2 ${styles.paragraph} text-center `}/>
|
||||
<label>Zapoznałem sie z <a href='#' className='hover:text-blue-600 font-bold sm'>regulaminem portalu Izaac.pl</a> i go akceptuję</label>
|
||||
</div>
|
||||
<div className='mb-6 pr-4'>
|
||||
<input type="checkbox" name="" id="" placeholder='' className={`border-b-2 px-4 my-3 mx-2 ${styles.paragraph} text-center `}/>
|
||||
<label>Rejestruję się jako pracodawca</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="bg-gray-700 text-white px-4 py-2 rounded w-full font-semibold text">
|
||||
Z a r e j e s t r u j s i ę
|
||||
@@ -40,5 +44,8 @@ const Register = ({isOpen, onClose}) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Register.propTypes = {
|
||||
isOpen: propTypes.bool,
|
||||
onClose: propTypes.func
|
||||
}
|
||||
export default Register
|
||||
|
||||
129
src/components/Salary.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
import {employment_types, work_from_home } from '../consts'
|
||||
|
||||
const Salary = ({handleChange, formData, removeFields, setFormData}) => {
|
||||
const [require_salary, setrequire_salary] = useState(true)
|
||||
|
||||
const handlerequire_salary = () => {
|
||||
setrequire_salary(prevrequire_salary => !prevrequire_salary);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFormData({...formData, 'require_salary': require_salary});
|
||||
}, [require_salary]);
|
||||
|
||||
const [minBigger, setMinBigger] = useState(true);
|
||||
const handleCheck = () => {
|
||||
let min_salary = parseInt(formData.min_salary);
|
||||
let max_salary = parseInt(formData.max_salary);
|
||||
if (min_salary > max_salary) {
|
||||
setMinBigger(true)
|
||||
}
|
||||
else {
|
||||
setMinBigger(false)
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
handleCheck();
|
||||
console.log(minBigger)
|
||||
console.log(formData.min_salary)
|
||||
console.log(formData.max_salary)
|
||||
|
||||
}, [formData.min_salary, formData.max_salary]);
|
||||
useEffect(() => {
|
||||
})
|
||||
useEffect( () =>{
|
||||
if (formData.require_salary === false) {
|
||||
removeFields(['min_salary', 'max_salary']);
|
||||
setrequire_salary(formData.require_salary)
|
||||
}
|
||||
}, [formData.require_salary])
|
||||
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-4 mt-4 gap-6 ${minBigger ? 'mb-10' : ''} duration-300`}>
|
||||
<div className='col-span-4'>
|
||||
<input
|
||||
className='ml-6'
|
||||
type="checkbox"
|
||||
checked={!require_salary}
|
||||
name="require_salary"
|
||||
onChange={handlerequire_salary}
|
||||
id="" />
|
||||
<span className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'> Nie chce podawać wysokości wynagrodzenia</span>
|
||||
</div>
|
||||
|
||||
<div className='relative'>
|
||||
<p className='ml-5 font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Min. wynagrodzenie</p>
|
||||
<input
|
||||
disabled={!require_salary}
|
||||
name='min_salary'
|
||||
value={formData['min_salary'] || '' }
|
||||
|
||||
onChange={handleChange('min_salary')}
|
||||
type='number'
|
||||
className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!require_salary ? 'bg-slate-200' : ''}`}/>
|
||||
{minBigger && (
|
||||
<div className='absolute top-[5rem] left-8 border-2 border-red-500 bg-red-200 rounded-b-xl p-2 opacity-90' >
|
||||
<p className='font-poppins text-red-700 text-[11px] mt-1 mb-1'>Minimalne wynagrodzenie nie może być większe od maks. wynagrodzenia</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div >
|
||||
<p className='ml-5 font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Maks. wynagrodzenie</p>
|
||||
<input
|
||||
disabled={!require_salary}
|
||||
value={formData['max_salary'] || '' }
|
||||
name="max_salary"
|
||||
onChange={handleChange('max_salary')}
|
||||
type='number'
|
||||
className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!require_salary ? 'bg-slate-200' : ''} `}/></div>
|
||||
|
||||
<div>
|
||||
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Typ <br className='hidden sm:block md:hidden'/>kontraktu</p>
|
||||
<select
|
||||
value={formData['employment_type'] || 'default' }
|
||||
name='employmentType'
|
||||
onChange={handleChange('employment_type')}
|
||||
className='h-12 border-2 rounded-xl px-5 mx-4 w-full '>
|
||||
<option value={'default'} disabled >Wybierz opcję</option>
|
||||
{employment_types.map(employment_type => (
|
||||
<option key={employment_type.id} value={employment_type.id}>{employment_type.name}</option>
|
||||
))}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Praca <br className='block md:hidden'/> zdalna</p>
|
||||
<select
|
||||
value={formData['work_from_home'] || 'default' }
|
||||
name='workFromHome'
|
||||
onChange={handleChange('work_from_home')}
|
||||
className='h-12 border-2 rounded-xl px-5 mx-4 w-full'>
|
||||
<option value={'default'} disabled >Wybierz opcję</option>
|
||||
{work_from_home.map(work_from_home => (
|
||||
<option key={work_from_home.id} value={work_from_home.id}>{work_from_home.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Salary.propTypes = {
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
removeFields: propTypes.func,
|
||||
setFormData: propTypes.func,
|
||||
min_salary: propTypes.string,
|
||||
max_salary: propTypes.string,
|
||||
require_salary: propTypes.bool,
|
||||
employment_type: propTypes.string,
|
||||
work_from_home: propTypes.string
|
||||
|
||||
}
|
||||
|
||||
export default Salary
|
||||
|
||||
62
src/components/Search.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const Search = (props) => {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const check_errors = (value, name) => {
|
||||
if ((name === 'max_salary' || name === 'min_salary') && !(value === '' || value === undefined)) {
|
||||
if (isNaN(parseInt(value))) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(props.value)
|
||||
}, [props.value])
|
||||
|
||||
return (
|
||||
<div className='grid sm:py-1 '
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<label className="mx-2 text-l text-center ml-3 font-poppins no-spin-buttons"
|
||||
htmlFor="search">
|
||||
<p className='text-sm'>{props.label}</p>
|
||||
<span className='text-sm text-red-700'>
|
||||
{check_errors(props.value, props.name) ? '' : ' *'}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type={props._type}
|
||||
value={props.value || ''}
|
||||
placeholder={props.placeholder}
|
||||
name={props.name}
|
||||
id={props.name}
|
||||
onChange={props.onChange}
|
||||
className={`border-2 ${ props.name === 'min_salary' || props.name === 'max_salary' ? 'w-[12rem] xs:w-[20rem] sm:w-full md:w-full lg:w-full' : '' }
|
||||
bg-white h-8 px-5 rounded-lg
|
||||
text-sm focus:outline-none ${check_errors(props.value, props.name) ? 'border-gray-300': 'border-red-700' }`}/>
|
||||
{isHovered && <div className='text-red-700 text-sm relative'>
|
||||
{check_errors(props.value, props.name) ? '' : 'Wartość musi być liczbą!'}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Search.propTypes = {
|
||||
label: propTypes.string,
|
||||
placeholder: propTypes.string,
|
||||
name: propTypes.string,
|
||||
setSearchQuery: propTypes.func,
|
||||
value: propTypes.string,
|
||||
_type: propTypes.string,
|
||||
onChange: propTypes.func
|
||||
}
|
||||
|
||||
export default Search
|
||||
64
src/components/SelectedSkill.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const levelMappings = {
|
||||
'N': 'Nice to have',
|
||||
'B': 'Podstawowy',
|
||||
'M': 'Średnio zaawansowany',
|
||||
'A': 'Zaawansowany',
|
||||
'E': 'Ekspert',
|
||||
};
|
||||
const skillLevels = ['N', 'B', 'M', 'A', 'E'];
|
||||
|
||||
function renderCircles(letter, handleCircleClick) {
|
||||
const level = levelMappings[letter];
|
||||
const numberOfFilledCircles = Object.keys(levelMappings).indexOf(letter) + 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleCircleClick(skillLevels[index])}
|
||||
className={`justify-self-center w-3.5 h-3.5 rounded-full
|
||||
${index < numberOfFilledCircles ? 'bg-red-300 hover:bg-red-700' : 'bg-gray-500 hover:bg-gray-600'}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const SelectedSkill = ({ skill_name, skillId ,letter, onLevelChange, removeSkillFromList }) => {
|
||||
const handleCircleClick = (newLetter) => {
|
||||
// const letter = Object.keys(levelMappings)[levelIndex];
|
||||
console.log(`Circle clicked: ${newLetter}`);
|
||||
onLevelChange(skillId, newLetter);
|
||||
};
|
||||
return (
|
||||
<div className="mr-12">
|
||||
<div key={skillId} className="relative selected-skill bg-slate-200 rounded-2xl h-min w-full p-2 mb-5 m-2 ">
|
||||
<div className='flex justify-center items-center'>
|
||||
<span className='text-slate-700 font-semibold text-[12px] text-center w-48 '>{skill_name}</span>
|
||||
</div>
|
||||
<button onClick={(e) => {e.preventDefault(); removeSkillFromList(skill_name)}} className="absolute top-1 right-5 text-black">
|
||||
×
|
||||
</button>
|
||||
<div className='h-0.5 w-full bg-dimWhite opacity-60 mt-1'></div>
|
||||
<div className='grid grid-cols-5 mt-2'>
|
||||
{renderCircles(letter, handleCircleClick)}
|
||||
</div>
|
||||
<p className='font-poppins font-semibold text-slate-700 text-center text-[12px] mt-1'>{levelMappings[letter]}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectedSkill.propTypes = {
|
||||
skill_name: propTypes.string,
|
||||
skillId: propTypes.number,
|
||||
letter: propTypes.string,
|
||||
onLevelChange: propTypes.func,
|
||||
removeSkillFromList: propTypes.func,
|
||||
};
|
||||
|
||||
export default SelectedSkill;
|
||||
35
src/components/Selector.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const Selector = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-2 sm:gap-[1px]'>
|
||||
<span className='col-span-2 mx-auto text-l font- text-center font-poppins'>{props.name}</span>
|
||||
{props.value_to_map_from.map((value_to_map_from) => (
|
||||
<label key={value_to_map_from.id} className='flex items-center justify-start text-sm font-poppins font-semibold text-gray-600'>
|
||||
<input
|
||||
type='checkbox'
|
||||
name={props.inputname}
|
||||
value={value_to_map_from.id}
|
||||
onChange={props.onChange}
|
||||
className='mr-2'
|
||||
checked={Array.isArray(props.state[props.inputname]) && props.state[props.inputname].includes(value_to_map_from.id)}
|
||||
/>
|
||||
{value_to_map_from.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Selector.propTypes = {
|
||||
value_to_map_from: PropTypes.array,
|
||||
setSearchQuery: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
inputname: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
state: PropTypes.object
|
||||
}
|
||||
|
||||
export default Selector
|
||||
23
src/components/SkillList.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import propTypes from 'prop-types';
|
||||
import SkillRender from './SkillRender';
|
||||
|
||||
|
||||
const SkillsList = ({skillData, skill_names}) => {
|
||||
return (
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3'>
|
||||
{skillData.map((skillLevel) => {
|
||||
const skill = skill_names.find((s) => s.id === parseInt(skillLevel.skill_id));
|
||||
return skill ? (
|
||||
<SkillRender key={skill.id} skill={skill.skill_name} level={skillLevel.skill_level} />
|
||||
) : null;
|
||||
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SkillsList.propTypes = {
|
||||
skillData: propTypes.array,
|
||||
skill_names: propTypes.array,
|
||||
};
|
||||
|
||||
export default SkillsList;
|
||||
50
src/components/SkillRender.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const levelMappings = {
|
||||
'N': 'Nice to have',
|
||||
'B': 'Podstawowy',
|
||||
'M': 'Średnio zaawansowany',
|
||||
'A': 'Zaawansowany',
|
||||
'E': 'Ekspert',
|
||||
};
|
||||
|
||||
function renderCircles(letter) {
|
||||
const level = levelMappings[letter];
|
||||
const numberOfFilledCircles = Object.keys(levelMappings).indexOf(letter) + 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`justify-self-center w-3.5 h-3.5 rounded-full
|
||||
${index < numberOfFilledCircles ? 'bg-red-300 hover:bg-red-700' : 'bg-gray-500 hover:bg-gray-600'}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const SkillRender = ({key, skill, level,}) => {
|
||||
|
||||
return (
|
||||
|
||||
<div className='h-42 rounded-[15px] bg-slate-200 py-4 m-2 hover:bg-slate-300 drop-shadow-sm' key={key}>
|
||||
<p className='text-center font-bold mb-1 px-2'>{skill}</p>
|
||||
<div className='h-0.5 mx-auto w-52 mt-2 justify-center bg-dimWhite opacity-60'></div>
|
||||
|
||||
<div className='mt-3 grid grid-cols-5'>{renderCircles(level)}</div>
|
||||
<p className='mt-3 text-xs text-center font-bold'>{levelMappings[level]}</p>
|
||||
</div>
|
||||
|
||||
)
|
||||
};
|
||||
|
||||
SkillRender.propTypes = {
|
||||
key: propTypes.number,
|
||||
skill: propTypes.string,
|
||||
level: propTypes.string,
|
||||
};
|
||||
|
||||
export default SkillRender;
|
||||
110
src/components/SkillsSelector.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import SelectedSkill from './SelectedSkill';
|
||||
|
||||
const SkillsSelector = ({ formData, setFormData, skills }) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [selectedSkills, setSelectedSkills] = useState([]);
|
||||
const [skill_levels, setskill_levels] = useState([]);
|
||||
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
if (value.length > 0) {
|
||||
setSuggestions(skills.filter(skill =>
|
||||
skill.skill_name.toLowerCase().includes(value.toLowerCase())
|
||||
));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle click on suggestion
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
const skill = skills.find(skill => skill.skill_name === suggestion);
|
||||
|
||||
if (skill && !selectedSkills.some(selectedSkill => selectedSkill.id === skill.id)) {
|
||||
setSelectedSkills(prevSkills => [...prevSkills, skill]);
|
||||
setskill_levels(prevLevels => [...prevLevels, { skill_id: skill.id, skill_level: 'N' }]);
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
// Handle removal of a skill from the list
|
||||
const removeSkillFromList = (skillToRemove) => {
|
||||
setSelectedSkills(prevSkills => prevSkills.filter(skill => skill.id !== skillToRemove.id));
|
||||
|
||||
setskill_levels(prevLevels => prevLevels.filter(level => level.skill_id !== skillToRemove.id));
|
||||
};
|
||||
|
||||
// Handle skill level change
|
||||
const handleLevelChange = (skillId, newLevel) => {
|
||||
setskill_levels(prevLevels =>
|
||||
prevLevels.map(level =>
|
||||
level.skill_id === skillId ? { ...level, skill_level: newLevel } : level
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Update formData on skill_levels change
|
||||
useEffect(() => {
|
||||
setFormData(prevFormData => ({
|
||||
...prevFormData,
|
||||
skill_levels
|
||||
}));
|
||||
}, [skill_levels, setFormData]);
|
||||
|
||||
return (
|
||||
<div className='relative mt-3 grid grid-cols-2'>
|
||||
<div className='col-span-5 grid mb-4'>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Wpisz umiejętność..."
|
||||
className="selected-skill bg-slate-200 rounded-2xl h-20 w-full
|
||||
sm:w-56 py-2 px-4 text-slate-700 font-semibold text-[16px]
|
||||
col-span-3"
|
||||
/>
|
||||
{inputValue.length >= 1 && suggestions.length > 0 && (
|
||||
<div className={`absolute border-4 w-56 top-16 z-10 overflow-y-auto overflow-x-hidden ${suggestions.length > 6 ? 'h-52' : 'h-min' }`}>
|
||||
{suggestions.map(suggestion => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSuggestionClick(suggestion.skill_name)}
|
||||
className="w-56 h-max px-3 py-1 bg-slate-300 border-b-2 cursor-pointer"
|
||||
>
|
||||
<p className='text-slate-700 font-semibold text-[12px]'>{suggestion.skill_name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='col-span-5 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3'>
|
||||
{selectedSkills.map(skill => (
|
||||
<SelectedSkill
|
||||
key={skill.id}
|
||||
skill_name={skill.skill_name}
|
||||
skillId={skill.id}
|
||||
letter={skill_levels.find(
|
||||
level => level.skill_id === skill.id)?.skill_level || 'N'}
|
||||
onLevelChange={handleLevelChange}
|
||||
removeSkillFromList={() => removeSkillFromList(skill)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SkillsSelector.propTypes = {
|
||||
formData: propTypes.object.isRequired,
|
||||
setFormData: propTypes.func.isRequired,
|
||||
skills: propTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default SkillsSelector;
|
||||
151
src/components/StepFourJoblisting.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import propTypes from "prop-types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import styles from "../style";
|
||||
import TextDivider from "./TextDivider";
|
||||
import Button from "./Button";
|
||||
|
||||
const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
|
||||
|
||||
const validateForm = () => {
|
||||
const { first_name, last_name, contact_email, company_name, vat_number } = formData;
|
||||
if (!first_name || !last_name || !contact_email || !company_name || !vat_number) {
|
||||
return true;
|
||||
};}
|
||||
useEffect(() => {
|
||||
validateForm();
|
||||
}, [formData]);
|
||||
|
||||
|
||||
const validateEmail = (e) => {
|
||||
const email = e.target.value;
|
||||
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
|
||||
if (!emailPattern.test(email)) {
|
||||
alert("Niepoprawny adres e-mail");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.flexCenter}`}>
|
||||
<div className="grid grid-cols-5">
|
||||
<div className="col-span-3 col-start-2 mt-12">
|
||||
<TextDivider text={`Podaj dane ogłoszeniodawcy`} />
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="col-span-1 mt-4 mx-2">
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Imię
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={formData["first_name"] || ""}
|
||||
required
|
||||
id="first_name"
|
||||
onChange={handleChange("first_name")}
|
||||
placeholder="Twoje imię..."
|
||||
className={`px-4 mt-2 ${styles.paragraph}`}
|
||||
/>
|
||||
<div className="bg-black h-0.5 opacity-10"></div>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4 mx-2">
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Nazwisko
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={formData["last_name"] || ""}
|
||||
required
|
||||
id="last_name"
|
||||
onChange={handleChange("last_name")}
|
||||
placeholder="Twoje nazwisko..."
|
||||
className={`px-4 mt-2 ${styles.paragraph}`}
|
||||
/>
|
||||
<div className="bg-black h-0.5 opacity-10"></div>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4 mx-2">
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Adres e-mail
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
name="contact_email"
|
||||
value={formData["contact_email"] || ""}
|
||||
required
|
||||
id="contact_email"
|
||||
onChange={handleChange("contact_email")}
|
||||
onBlur={validateEmail}
|
||||
placeholder="Adres mailowy..."
|
||||
className={`px-4 mt-2 ${styles.paragraph}`}
|
||||
/>
|
||||
<div className="bg-black h-0.5 opacity-10"></div>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4 mx-2">
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Nazwa firmy
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="company_name"
|
||||
value={formData["company_name"] || ""}
|
||||
required
|
||||
onChange={handleChange("company_name")}
|
||||
id="company_name"
|
||||
placeholder="Wpisz nazwę firmy..."
|
||||
className={`px-4 mt-2 ${styles.paragraph}`}
|
||||
/>
|
||||
<div className="bg-black h-0.5 opacity-10"></div>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4 mx-2">
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Nip firmy
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
name="vat_number"
|
||||
value={formData["vat_number"] || ""}
|
||||
required
|
||||
maxLength="10"
|
||||
onChange={handleChange("vat_number")}
|
||||
id="vat_number"
|
||||
placeholder="Wpisz nip..."
|
||||
className={`px-4 mt-2 ${styles.paragraph}`}
|
||||
/>
|
||||
<div className="bg-black h-0.5 opacity-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.flexCenter} gap-16 mt-16`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="h-16 md:h-12 w-80 md:w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] text-white hover:scale-125 duration-300"
|
||||
>
|
||||
<span className="text-[18px]">←</span> Przejdź do <br className="block md:hidden"/>
|
||||
poprzedniego kroku
|
||||
</button>
|
||||
|
||||
<Button
|
||||
isDiasbled={validateForm()}
|
||||
nextStep={nextStep}
|
||||
Text="Przejdź do następnego kroku"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StepFourJoblisting.propTypes = {
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
nextStep: propTypes.func,
|
||||
prevStep: propTypes.func,
|
||||
};
|
||||
|
||||
export default StepFourJoblisting;
|
||||
107
src/components/StepOneJoblisting.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
import styles from "../style";
|
||||
import propTypes from "prop-types";
|
||||
import { lorem_ipsum, lorem_ipsum_premium, lorem_ipsum_starter } from "../consts";
|
||||
|
||||
const StepOneJoblisting = ({ nextStep, handleChange, formData }) => {
|
||||
// Funkcja do obsługi kliknięcia na div i aktualizacji stanu
|
||||
const handleDivClick = (value) => () => {
|
||||
handleChange("posting_option")({ target: { value: value } });
|
||||
};
|
||||
|
||||
const activeStyle =
|
||||
"h-[200px] sm:h-[350px] md:h-min w-64 pb-4 border-4 rounded-xl border-stone-200 bg-gray-200 div-transition scale-105 sm:scale-110";
|
||||
const inactiveStyle =
|
||||
"h-[200px] sm:h-[350px] md:h-min w-64 pb-4 rounded-xl bg-gray-200 border-gray-200 div-transition cursor-pointer hover:bg-gray-300";
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`grid grid-cols-1 ${styles.paddingX} pb-8 mt-24 xs:mt-2 xs:pt-8 gap-1 h-full`}
|
||||
>
|
||||
<h1 className={`text-center text-2xl sm:text-[40px] font-bold`}>
|
||||
Zacznij dodawać <br className="block md:hidden" /> ogłoszenie z izaac.pl
|
||||
</h1>
|
||||
<p className={`text-center text-xl mt-3`}>
|
||||
Wybierz pakiet najlepiej <br className="block md:hidden" />odpowiadający Twoim potrzebom
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-[300px] sm:h-[600px]">
|
||||
<div className={` ${styles.flexStart} ${styles.paddingX} gap-3 xs:gap-5 sm:gap-8 mt-6`}>
|
||||
|
||||
<div
|
||||
className={
|
||||
formData.posting_option === "M" ? activeStyle : inactiveStyle
|
||||
}
|
||||
onClick={handleDivClick("M")}
|
||||
>
|
||||
<p
|
||||
className={`font-poppins text-l
|
||||
sm:text-xl text-center mt-4`}>
|
||||
Starter
|
||||
</p>
|
||||
<ul className="hidden md:block">
|
||||
{lorem_ipsum_starter.map((lorem_ipsum) =>
|
||||
(<li className="text-sm lista-outside mx-8 text-justify list-inside">{lorem_ipsum}</li>)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="h-[150px] sm:h-[250px] md:h-[300px] w-0.5 bg-gray-300"></div>
|
||||
<div
|
||||
className={
|
||||
formData.posting_option === "S" ? activeStyle : inactiveStyle
|
||||
}
|
||||
onClick={handleDivClick("S")}
|
||||
>
|
||||
<p className={`font-poppins text-l sm:text-xl text-center mt-4`}>Standard</p>
|
||||
<div className="mt-2">
|
||||
<p className={`font-poppins text-sm sm:text-[14px] text-center mb-2`}>
|
||||
najczęsciej wybierany
|
||||
</p>
|
||||
<ul className="hidden md:block">
|
||||
{lorem_ipsum.map((lorem_ipsum) =>
|
||||
(<li className="text-sm lista-outside mx-8 text-justify list-inside">{lorem_ipsum}</li>)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[150px] sm:h-[250px] md:h-[300px] w-0.5 bg-gray-300"></div>
|
||||
<div
|
||||
className={
|
||||
formData.posting_option === "P" ? activeStyle : inactiveStyle
|
||||
}
|
||||
onClick={handleDivClick("P")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<p className={`font-poppins text-l sm:text-xl text-center mt-4`}>Premium</p>
|
||||
<ul className="hidden md:block">
|
||||
{lorem_ipsum_premium.map((lorem_ipsum) =>
|
||||
(<li className="text-sm lista-outside mx-8 text-justify list-inside">{lorem_ipsum}</li>)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="absolute inset-x-0 md:-left-4 bottom-[4rem] mx-auto h-12 w-72 rounded-xl bg-gray-700
|
||||
font-poppins font-semibold text-[14px] text-white
|
||||
hover:scale-125 duration-300"
|
||||
>
|
||||
Przejdź do następnego kroku
|
||||
<span className="text-[18px]">→</span>{" "}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepOneJoblisting.propTypes = {
|
||||
nextStep: propTypes.func,
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
};
|
||||
|
||||
export default StepOneJoblisting;
|
||||
114
src/components/StepThreeJoblisting.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import TextDivider from './TextDivider';
|
||||
import ListingSmall from './ListingSmall';
|
||||
import { IzaacLOGO } from '../assets'
|
||||
|
||||
import SkillsList from './SkillList';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const StepThreeJoblisting = ({ formData, prevStep, skills, handleSubmit }) => {
|
||||
// Funkcja do przetwarzania HTML i dodawania klas
|
||||
const processHTML = (htmlString) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// Dodaj klasy do tagów h1, h2, ..., h6
|
||||
doc.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(tag => {
|
||||
tag.classList.add('no-tailwindcss-base');
|
||||
});
|
||||
|
||||
// Dodaj klasy do tagów ol, ul
|
||||
doc.querySelectorAll('ol, ul').forEach(tag => {
|
||||
tag.classList.add('no-tailwindcss-base');
|
||||
});
|
||||
doc.querySelectorAll('li').forEach(tag => {tag.classList.add('lista')})
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
// Sanituj i przetwórz dane
|
||||
const cleanAndProcessData = (userInput) => {
|
||||
const sanitizedHTML = DOMPurify.sanitize(userInput);
|
||||
return processHTML(sanitizedHTML);
|
||||
};
|
||||
|
||||
// Przykład renderowania przetworzonej treści jako komponentów React
|
||||
const renderContent = (htmlString) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: cleanAndProcessData(htmlString) }} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<p className='mt-8 text-[24px] font-poppins font-bold '>
|
||||
Wybrałeś opcję {formData.posting_option}
|
||||
</p>
|
||||
<TextDivider text={`Tak będzie wyglądało Twoje ogłoszenie na liście ogłoszeń`} />
|
||||
<div className={`grid grid-cols-4`}>
|
||||
<ListingSmall
|
||||
id={9999}
|
||||
name={'Starszy Inżynier ds. ciągłości produkcji'}
|
||||
company_name={'F.H.U. Januszex'}
|
||||
min_salary={'12000'}
|
||||
max_salary={'15000 PLN'}
|
||||
image={IzaacLOGO}
|
||||
|
||||
require_salary={true}
|
||||
index={0}
|
||||
onClick={() => {}}
|
||||
_class={`col-start-2 col-end-4 job-listing`}
|
||||
/>
|
||||
<ListingSmall
|
||||
id={999999}
|
||||
name={formData.name}
|
||||
company_name={formData.company_name}
|
||||
min_salary={formData.min_salary}
|
||||
max_salary={formData.max_salary}
|
||||
requiresalary={formData.require_salary}
|
||||
image={formData.image}
|
||||
index={0}
|
||||
onClick={() => {}}
|
||||
_class={`col-start-2 col-end-4`}/>
|
||||
<ListingSmall
|
||||
id={99999}
|
||||
name={'Konstruktor - inżynier mechanik'}
|
||||
company_name={'Papieżeks Sp. z o. o.'}
|
||||
image={IzaacLOGO}
|
||||
min_salary={'9000'}
|
||||
max_salary={'12000'}
|
||||
requiresalary={true}
|
||||
index={1}
|
||||
onClick={() => {}}
|
||||
_class={`col-start-2 col-end-4 job-listing2`} />
|
||||
</div>
|
||||
<TextDivider text={`Tak będzie się prezentowała treść Twojego ogłoszenia`} />
|
||||
<div className='grid grid-cols-5 mt-4 mb-4'>
|
||||
<div className='col-span-3 col-start-2 bg-gray-100 text-justify text-[18px] leading-[1.75rem] p-4 rounded-xl'>
|
||||
<SkillsList skillData={formData.skill_levels} skill_names={skills}/>
|
||||
{renderContent(formData.content)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className='h-12 w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] text-white hover:scale-125 duration-300 mb-5 mx-12'>
|
||||
<span className='text-[18px]'>←</span> Przejdź do poprzedniego kroku
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className='h-12 w-72 rounded-xl bg-green-700 font-poppins font-semibold text-[14px] text-white hover:scale-125 duration-300 mb-5 mx-12'>
|
||||
Dodaj ogłoszenie! <span className='text-[18px]'>→</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepThreeJoblisting.propTypes = {
|
||||
formData: PropTypes.object,
|
||||
prevStep: PropTypes.func,
|
||||
skills: PropTypes.array,
|
||||
handleSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default StepThreeJoblisting;
|
||||
271
src/components/StepTwoJoblisting.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import propTypes from 'prop-types';
|
||||
import MaximumLength from '@ckeditor/ckeditor5-react'
|
||||
import { categories, experience_levels } from '../consts';
|
||||
import styles from '../style'
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
import ImageUpload from './ImageUpload';
|
||||
import SkillsSelector from './SkillsSelector';
|
||||
import Salary from './Salary';
|
||||
import { placeholderImage } from '../assets';
|
||||
import Button from './Button';
|
||||
const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setFormData, removeFields, skills }) => {
|
||||
const [editorData, setEditorData] = useState('');
|
||||
const [imageSrc, setImageSrc] = useState(placeholderImage);
|
||||
const max_words = 500;
|
||||
const countWords = (text) => {
|
||||
return text.split(/\s/).filter(function(n) { return n != '' }).length;
|
||||
}
|
||||
|
||||
const validateForm2 = () => {
|
||||
const { name, company_name, localization, content, min_salary, max_salary, work_from_home, employment_type, image, skill_levels } = formData;
|
||||
if (!name || !company_name || !localization || !content || !min_salary || !max_salary || !work_from_home || !employment_type || !image || !skill_levels) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (formData.content) {
|
||||
console.log(countWords(formData.content))
|
||||
if (countWords(formData.content) > max_words) {
|
||||
alert('Maksymalna liczba słów wynosi 500')
|
||||
}
|
||||
}
|
||||
|
||||
}, [formData.content])
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (validateForm()) {
|
||||
console.log(errors)
|
||||
nextStep(); // Przejście do następnego kroku
|
||||
} else {
|
||||
alert('Proszę wypełnić wszystkie wymagane pola.'); // Wyświetl komunikat o błędzie
|
||||
}
|
||||
};
|
||||
const errorStyleInput = 'border-red-600'
|
||||
const [errors, setErrors] = useState({});
|
||||
const validateForm = () => {
|
||||
let newErrors = {};
|
||||
if (countWords(formData.content) > max_words) {
|
||||
newErrors.content = 'Maksymalna liczba słów wynosi 600';
|
||||
}
|
||||
// Sprawdź każde wymagane pole
|
||||
if (!formData.name) {
|
||||
newErrors.name = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.company_name) {
|
||||
newErrors.company_name = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.localization) {
|
||||
newErrors.localization = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.content) {
|
||||
newErrors.content = 'To pole jest wymagane';
|
||||
}
|
||||
console.log(formData.require_salary)
|
||||
if (formData.require_salary === true)
|
||||
{
|
||||
console.log(formData.require_salary)
|
||||
if (!formData.min_salary) {
|
||||
newErrors.min_salary = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.max_salary) {
|
||||
newErrors.max_salary = 'To pole jest wymagane';
|
||||
}
|
||||
}
|
||||
|
||||
if (!formData.work_from_home) {
|
||||
newErrors.work_from_home = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.employment_type) {
|
||||
newErrors.employment_type = 'To pole jest wymagane';
|
||||
}
|
||||
if (!formData.image) {
|
||||
newErrors.image = 'To pole jest wymagane';
|
||||
}
|
||||
if (Object.keys(formData.skill_levels).length === 0) {
|
||||
newErrors.skill_levels = 'To pole jest wymagane';
|
||||
}
|
||||
setErrors(newErrors);
|
||||
|
||||
// Zwróć true, jeśli nie ma błędów
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-3'>
|
||||
<div className='sm:px-20 px-16 sm:pt-3 pt-6 pb-6 xl:col-span-3 col-span-3'>
|
||||
<span className='justify-center'>
|
||||
<p className={`${styles.heading1} text-center`}>Dodajesz ogłoszenie z izaac.pl</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='no-tailwindcss-base'>
|
||||
<div className='mx-auto max-w-none h-[70vh] w-[60vw]'>
|
||||
<form action="">
|
||||
<div className='grid mb-4'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-3 items-center'>
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>Nazwa ogłoszenia
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
|
||||
<input type="text"
|
||||
name="name"
|
||||
value={formData['name'] || ''}
|
||||
required
|
||||
id="name"
|
||||
onChange={handleChange('name')}
|
||||
placeholder='Wpisz nazwę stanowiska...'
|
||||
className={`border-b-2 px-4 my-2 ${styles.paragraph}
|
||||
col-span-2 ${errors.jobtitle ? errorStyleInput : ''}`}/>
|
||||
|
||||
<div className={`${styles.paragraph} px-4 py-2 `}>Strona internetowa</div>
|
||||
<input type="text"
|
||||
name="webpage"
|
||||
value={formData['webpage'] || ''}
|
||||
onChange={handleChange('webpage')}
|
||||
id="webpage"
|
||||
placeholder='Wpisz adres strony internetowej...'
|
||||
className={`border-b-2 px-4 my-2 ${styles.paragraph} col-span-2`}/>
|
||||
<div className={`${styles.paragraph} px-4 py-2 `}>Adres siedziby
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
|
||||
<input type="text"
|
||||
name="localization"
|
||||
required
|
||||
value={formData['localization'] || ''}
|
||||
onChange={handleChange('localization')}
|
||||
id="localization"
|
||||
placeholder='Wpisz adres siedziby...'
|
||||
className={`border-b-2 px-4 my-2 ${styles.paragraph} col-span-2`}/>
|
||||
<div className={`${styles.paragraph} px-4 py-2 `}>Logo firmy
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
|
||||
<div className='mt-6 col-span-2'>
|
||||
<ImageUpload
|
||||
setFormData={setFormData}
|
||||
data={formData}
|
||||
_setImageSrc={setImageSrc}
|
||||
_imageSrc={imageSrc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full border-b-2'></div>
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>Treść ogłoszenia
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
|
||||
<CKEditor
|
||||
editor={ClassicEditor}
|
||||
data={formData['content'] || ''}
|
||||
required
|
||||
// config={{
|
||||
// plugins: [WordCount],
|
||||
// toolbar: ['wordCount'],
|
||||
// wordCount: {
|
||||
// onUpdate: stats => {
|
||||
// console.log(stats.words, stats.characters)
|
||||
// }
|
||||
// }
|
||||
|
||||
// }}
|
||||
onChange={(event, editor) => {
|
||||
const data = editor.getData();
|
||||
handleChange('content')(data); // Wywołanie zmodyfikowanej funkcji handleChange
|
||||
}}
|
||||
onReady={ editor => {
|
||||
// You can store the "editor" and use when it is needed.
|
||||
// console.log( 'Editor is ready to use!', editor );
|
||||
} }
|
||||
onBlur={ ( event, editor ) => {
|
||||
// console.log( 'Blur.', editor );
|
||||
} }
|
||||
onFocus={ ( event, editor ) => {
|
||||
// console.log( 'Focus.', editor );
|
||||
} }
|
||||
/>
|
||||
<div className='grid grid-cols-3 items-center mt-4'>
|
||||
<span className={`${styles.paragraph} col-span-3 px-4`}>Dodaj wybrane umiejętności <span className={`${styles.paragraph} text-red-700`}>*</span></span>
|
||||
<div className='col-span-3'>
|
||||
<SkillsSelector
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
skills={skills}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full border-b-2'></div>
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>Kategoria ogłoszenia i poziom doświadczenia
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
|
||||
<div className='grid grid-cols-2 items-center mt-4 mb-4 gap-6'>
|
||||
|
||||
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Wybierz kategorię Twojego ogłoszenia</p>
|
||||
<select
|
||||
value={formData['category'] || 'default' }
|
||||
name='category'
|
||||
onChange={handleChange('category')}
|
||||
className='h-12 border-2 rounded-xl px-5 mx-4 w-full '>
|
||||
<option value={'default'} disabled >Wybierz opcję</option>
|
||||
{categories.map((category, index) => (
|
||||
<option key={index} value={category.id}>{category.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Poziom doświadczenia</p>
|
||||
<select
|
||||
value={formData['experience_level'] || 'default' }
|
||||
name='experience_level'
|
||||
onChange={handleChange('experience_level')}
|
||||
className='h-12 border-2 rounded-xl px-5 mx-4 w-full '>
|
||||
<option value={'default'} disabled >Wybierz opcję</option>
|
||||
{experience_levels.map((experience_level, index) => (
|
||||
<option key={index} value={experience_level.id}>{experience_level.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full border-b-2'></div>
|
||||
<div className={`${styles.paragraph} px-4 py-2`}>
|
||||
Wynagrodzenie
|
||||
<span className={`${styles.paragraph} text-red-700`}>*</span>
|
||||
</div>
|
||||
<div className=''>
|
||||
<Salary
|
||||
removeFields={removeFields}
|
||||
setFormData={setFormData}
|
||||
handleChange={handleChange}
|
||||
formData={formData}/>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.flexCenter} py-10 gap-20`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className='h-16 md:h-12 w-80 md:w-72 rounded-xl bg-gray-700
|
||||
font-poppins font-semibold text-[14px] scale-100
|
||||
text-white hover:scale-125 duration-300'>
|
||||
<span className='text-[18px]'>←</span>
|
||||
Przejdź do poprzedniego kroku</button>
|
||||
|
||||
<Button
|
||||
isDiasbled={validateForm2()}
|
||||
nextStep={handleNextStep}
|
||||
Text="Przejdź do następnego kroku"/>
|
||||
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
StepTwoJoblisting.propTypes = {
|
||||
nextStep: propTypes.func,
|
||||
prevStep: propTypes.func,
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
setFormData: propTypes.func,
|
||||
removeFields: propTypes.func,
|
||||
skills: propTypes.array
|
||||
}
|
||||
|
||||
export default StepTwoJoblisting
|
||||
20
src/components/Success.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import TextDivider from './TextDivider'
|
||||
|
||||
const Success = () => {
|
||||
return (
|
||||
<>
|
||||
<TextDivider text={`Twoje ogłoszenie zostało dodane!`} />
|
||||
<div className='text-center'>
|
||||
<p className='mt-8 text-[24px] font-poppins font-bold '>
|
||||
Dziękujemy za dodanie ogłoszenia
|
||||
</p>
|
||||
<p className='mt-8 text-[16px] font-poppins font-bold '>
|
||||
Twoje ogłoszenie zostanie opublikowane po weryfikacji
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Success
|
||||
20
src/components/TextDivider.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const TextDivider = ({ text }) => {
|
||||
return (
|
||||
<div className='flex items-center justify-center mt-4 mb-6'>
|
||||
<div className='h-0.5 bg-black w-[14rem] opacity-30'></div>
|
||||
<p className='text-[24px] mx-8 font-semibold text-center'>
|
||||
{text}
|
||||
</p>
|
||||
<div className='h-0.5 bg-black w-[14rem] opacity-30'></div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
TextDivider.propTypes = {
|
||||
text: propTypes.string
|
||||
}
|
||||
export default TextDivider
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from 'react'
|
||||
import { ogloszenia } from '../consts'
|
||||
import { useState } from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import ListingSmall from "./ListingSmall";
|
||||
// import SkillRender from "./SkillRender";
|
||||
// import Search from "./Search";
|
||||
import Filter from "./Filter";
|
||||
// import Category from "./Category";
|
||||
import JobOfferContent from "./JobOfferContent";
|
||||
import { categories } from "../consts";
|
||||
import axios from "axios";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
|
||||
function renderCircles(level) {
|
||||
let numberOfFilledCircles;
|
||||
|
||||
switch (level) {
|
||||
case 'Podstawowy':
|
||||
case "Podstawowy":
|
||||
numberOfFilledCircles = 1;
|
||||
break;
|
||||
case 'Zaawansowany':
|
||||
case "Zaawansowany":
|
||||
numberOfFilledCircles = 3;
|
||||
break;
|
||||
case 'Ekspert':
|
||||
case "Ekspert":
|
||||
numberOfFilledCircles = 5;
|
||||
break;
|
||||
default:
|
||||
@@ -24,16 +34,97 @@ function renderCircles(level) {
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`inline-block w-3.5 h-3.5 mr-1 rounded-full ${index < numberOfFilledCircles ? 'bg-red-300 hover:bg-red-700' : 'bg-gray-500 hover:bg-gray-600'}`}
|
||||
className={`inline-block w-3.5 h-3.5 mr-1 rounded-full ${
|
||||
index < numberOfFilledCircles
|
||||
? "bg-red-300 hover:bg-red-700"
|
||||
: "bg-gray-500 hover:bg-gray-600"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const WorkApp = () => {
|
||||
let { id } = useParams();
|
||||
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||
// Czy szczegóły są widoczne
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!isOpen) {
|
||||
// setSearchQuery({ name: "", localization: "", min_salary: "", max_salary: "" });
|
||||
// }}, [isOpen]);
|
||||
|
||||
const [ogloszenia, setOgloszenia] = useState([]);
|
||||
|
||||
const [skills, setSkills] = useState([]);
|
||||
|
||||
const getOgloszenia = async (queryParams = {}) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
"https://izaac.knck.pl/api/jobposting/joboffers_list/",
|
||||
{ params: queryParams }
|
||||
);
|
||||
const data = response.data;
|
||||
setOgloszenia(data);
|
||||
// console.log(data);
|
||||
if (data.length > 0) {
|
||||
setSelectedOgloszenie(data[0]);
|
||||
console.log(data[0])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
if (id === undefined) {
|
||||
id = ogloszenia[0]?.id;
|
||||
}
|
||||
|
||||
const getSkills = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://izaac.knck.pl/api/jobposting/skills/`
|
||||
);
|
||||
const data = response.data;
|
||||
setSkills(data);
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState({
|
||||
min_salary: '',
|
||||
max_salary: '',
|
||||
categories: [],
|
||||
name: ''
|
||||
});
|
||||
const clearSearchQuery = () => {
|
||||
setSearchQuery({
|
||||
min_salary: '',
|
||||
max_salary: '',
|
||||
categories: [],
|
||||
name: ''
|
||||
});
|
||||
getSkills();
|
||||
getOgloszenia()
|
||||
setIsOpen(!isOpen)
|
||||
};
|
||||
|
||||
const [selectedOgloszenie, setSelectedOgloszenie] = useState(ogloszenia[0]);
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false); // Dodany stan
|
||||
|
||||
useEffect(() => {
|
||||
getOgloszenia();
|
||||
getSkills();
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Izaac - ${selectedOgloszenie?.name}`;
|
||||
}, [selectedOgloszenie?.name]);
|
||||
|
||||
const handleOgloszenieClick = (ogloszenie) => {
|
||||
setSelectedOgloszenie(ogloszenie);
|
||||
@@ -44,10 +135,87 @@ const WorkApp = () => {
|
||||
const handleBackToList = () => {
|
||||
setIsDetailsVisible(false);
|
||||
};
|
||||
const [isSmallViewport, setIsSmallViewport] = React.useState(window.innerWidth <= 1025);
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsSmallViewport(window.innerWidth <= 1025);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Clean up the event listener when the component unmounts
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className=' mx-auto bg-white w-full'>
|
||||
<section className={`bg-white`}>
|
||||
<div className="h-[88vh] flex flex-row ">
|
||||
|
||||
|
||||
<div
|
||||
className={`relative bg-gray-100 w-full sm:w-[40%]
|
||||
${isOpen ? 'h-[100vh] overflow-y-hidden' : 'sm:overflow-y-auto'}
|
||||
sm:h-[89vh] ${isDetailsVisible ? "hidden sm:block" : "block" }`}
|
||||
>
|
||||
<div className="z-10 sticky top-[70px] sm:top-0 w-[100vw] sm:w-full">
|
||||
<div className="z-10 w-full fixed sm:sticky top-[4.4rem] sm:top-0 min-h-fit bg-slate-300 flex items-center place-content-center py-1 sm:py-2">
|
||||
<h1 className="text-center text-xl sm:text-l md:text-2xl font-poppins font-semibold text-slate-800 py-2 mx-2">
|
||||
Oferty pracy
|
||||
</h1>
|
||||
<button
|
||||
className="block font-poppins text-white font-semibold
|
||||
bg-gray-500 px-4 py-2 mx-6 rounded-md hover:bg-gray-600 duration-100"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
|
||||
{!isOpen ? 'Pokaż filtry' : 'Ukryj filtry'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div className={`z-10 absolute top-[3rem] sm:top-16 w-[100vw] sm:w-full`}>
|
||||
<Filter isOpen={isOpen}
|
||||
searchQuery={searchQuery || {}}
|
||||
setSearchQuery={setSearchQuery}
|
||||
categories={categories}
|
||||
clearSearchQuery={clearSearchQuery}
|
||||
onClick={()=> {getOgloszenia(searchQuery)
|
||||
setIsOpen(!isOpen)
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`sm:mt-2 mr-2 mt-[8rem] noblur ${!isOpen ? '' : 'blur'}`}
|
||||
|
||||
>
|
||||
{ogloszenia.map((ogloszenie, index) => (
|
||||
<Link
|
||||
key={ogloszenie.id}
|
||||
to={`/work/joboffers/${ogloszenie.id}`} >
|
||||
<ListingSmall
|
||||
key={ogloszenie.id}
|
||||
name={ogloszenie.name}
|
||||
company_name={ogloszenie.company_name}
|
||||
min_salary={ogloszenie.min_salary}
|
||||
max_salary={ogloszenie.max_salary}
|
||||
require_salary={ogloszenie.require_salary}
|
||||
image={ogloszenie.image}
|
||||
index={index}
|
||||
onClick={() => {handleOgloszenieClick(ogloszenie)}}
|
||||
selected={selectedOgloszenie?.id === ogloszenie.id}
|
||||
blur={isOpen ? 'blur' : ''}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute sm:relative w-full sm:w-[60%] bg-gray-100 sm:h-[89vh] h-[100vh] top-16 sm:top-0 -left-4 sm:left-0 right-0 overflow-y-auto p-8 sm:p-4 border-separate sm:border-l-8 blur-transition ${
|
||||
isDetailsVisible ? "block" : "hidden sm:block"
|
||||
} noblur ${isOpen ? 'blur' : ''}`}
|
||||
>
|
||||
{isDetailsVisible && (
|
||||
<div className='grid grid-cols-6'>
|
||||
<div className="grid grid-cols-6">
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="sm:hidden block font-poppins text-white font-semibold bg-gray-500 px-4 py-2 rounded-md mx-4 col-span-4 col-start-2"
|
||||
@@ -57,40 +225,14 @@ const WorkApp = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex-grow h-5/6 grid grid-cols-10 '>
|
||||
|
||||
<div className={`rounded-xl col-span-11 sm:col-span-4 bg-gray-100 overflow-y-auto p-2 my-4 mx-2 sm:h-[84vh] h-[84vh] ${isDetailsVisible ? 'hidden sm:block' : 'block'}`}>
|
||||
{ogloszenia.map((ogloszenie, index) => (
|
||||
<div
|
||||
key={ogloszenie.id}
|
||||
onClick={() => handleOgloszenieClick(ogloszenie)}
|
||||
className={`grid grid-cols-4 drop-shadow cursor-pointer mb-2 sm:mr-4 mr-0 text-xl font-bold border rounded-[10px] p-2 hover:border-l-8 hover:border-zinc-500 duration-300 ${index % 2 === 0 ? 'bg-gray-200' : 'bg-gray-300'}`}
|
||||
>
|
||||
|
||||
<p className='col-span-3 text-sm'>{ogloszenie.company_name}</p>
|
||||
<p className='place-self-end text-sm text-slate-800'>{ogloszenie.salaryRange}</p>
|
||||
<p className='col-span-4'>{ogloszenie.name}</p>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`sm:col-span-6 col-span-11 bg-gray-100 sm:h-[84vh] h-[84vh] overflow-y-auto p-4 my-4 ${isDetailsVisible ? 'block' : 'hidden sm:block'}`}>
|
||||
<div className='grid sm:grid-cols-2 md:grid-cols-5 grid-cols-2'>
|
||||
{Object.entries(selectedOgloszenie.neededSkills).map(([skill, level]) => (
|
||||
<div className='col-span-1 w-36 h-30 rounded-[15px] bg-slate-300 py-4 pl-3 m-2 hover:bg-slate-200' key={skill}>
|
||||
<p className='font-bold mb-1 '>{skill}</p>
|
||||
{renderCircles(level)}
|
||||
<p className='text-xs font-bold'>{level.toLowerCase()}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='text-slate-800 font-medium mt-4'>
|
||||
{selectedOgloszenie.tresc}
|
||||
</div>
|
||||
{selectedOgloszenie && (
|
||||
<JobOfferContent id={id} skills={skills} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkApp
|
||||
export default WorkApp;
|
||||
export { renderCircles };
|
||||
|
||||
@@ -13,7 +13,8 @@ export const ogloszenia = [
|
||||
"Coś innego": "Zaawansowany",
|
||||
"Papieżworks": "Podstawowy",
|
||||
"Budowa papieży": "Ekspert",
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
@@ -26,7 +27,8 @@ export const ogloszenia = [
|
||||
"Elektronika": "Ekspert",
|
||||
"PLC": "Zaawansowany",
|
||||
"Automatyka przemysłowa": "Podstawowy"
|
||||
}
|
||||
},
|
||||
"requiresalary": false
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
@@ -39,7 +41,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Ekspert",
|
||||
"Zarządzanie projektem": "Zaawansowany",
|
||||
"Nadzór budowlany": "Zaawansowany"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
@@ -52,7 +55,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Zaawansowany",
|
||||
"SolidWorks": "Podstawowy",
|
||||
"Budowa statków": "Ekspert"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
@@ -65,7 +69,8 @@ export const ogloszenia = [
|
||||
"Elektronika": "Ekspert",
|
||||
" PLC": "Zaawansowany",
|
||||
"Automatyka przemysłowa": "Podstawowy"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
@@ -78,7 +83,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Ekspert",
|
||||
"Zarządzanie projektem": "Zaawansowany",
|
||||
"Nadzór budowlany": "Zaawansowany"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
@@ -91,7 +97,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Zaawansowany",
|
||||
"SolidWorks": "Podstawowy",
|
||||
"Budowa statków": "Ekspert"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
@@ -104,8 +111,10 @@ export const ogloszenia = [
|
||||
"Elektronika": "Ekspert",
|
||||
"PLC": "Zaawansowany",
|
||||
"Automatyka przemysłowa": "Podstawowy"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
}
|
||||
,
|
||||
{
|
||||
"id": "9",
|
||||
"company_name": "Kompania S. A",
|
||||
@@ -117,7 +126,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Ekspert",
|
||||
"Zarządzanie projektem": "Zaawansowany",
|
||||
"Nadzór budowlany": "Zaawansowany"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
@@ -130,7 +140,8 @@ export const ogloszenia = [
|
||||
"AutoCAD": "Zaawansowany",
|
||||
"SolidWorks": "Podstawowy",
|
||||
"Budowa statków": "Ekspert"
|
||||
}
|
||||
},
|
||||
"requiresalary": true
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
@@ -143,64 +154,13 @@ export const ogloszenia = [
|
||||
"Elektronika": "Ekspert",
|
||||
"PLC": "Zaawansowany",
|
||||
"Automatyka przemysłowa": "Podstawowy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"company_name": "Kompania S. A",
|
||||
"name": "Inżynier budowlany",
|
||||
"tresc": "Zapraszamy do aplikowania osoby z doświadczeniem w nadzorowaniu i zarządzaniu dużymi projektami budowlanymi.",
|
||||
"salaryRange": "14,000 - 22,000 PLN",
|
||||
"lokalizacja": "Gdańsk, Poland",
|
||||
"neededSkills": {
|
||||
"AutoCAD": "Ekspert",
|
||||
"Zarządzanie projektem": "Zaawansowany",
|
||||
"Nadzór budowlany": "Zaawansowany"
|
||||
"requiresalary": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"company_name": "Kompania S. A",
|
||||
"name": "Inżynier mechanik w branży okrętowniczej",
|
||||
"tresc": "Poszukujemy doświadczonego inżyniera mechanika specjalizującego się w budowie i konserwacji statków. Wymagane minimum 5 lat doświadczenia w branży okrętowniczej.",
|
||||
"salaryRange": "10,000 - 15,000 PLN",
|
||||
"lokalizacja": "Gdańsk, Poland",
|
||||
"neededSkills": {
|
||||
"AutoCAD": "Zaawansowany",
|
||||
"SolidWorks": "Podstawowy",
|
||||
"Budowa statków": "Ekspert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"company_name": "Kompania S. A",
|
||||
"name": "Inżynier elektryk w branży energetycznej",
|
||||
"tresc": "Szukamy inżyniera elektryka z doświadczeniem w projektowaniu i implementacji systemów energetycznych.",
|
||||
"salaryRange": "12,000 - 18,000 PLN",
|
||||
"lokalizacja": "Gdańsk, Poland",
|
||||
"neededSkills": {
|
||||
"Elektronika": "Ekspert",
|
||||
" PLC": "Zaawansowany",
|
||||
"Automatyka przemysłowa": "Podstawowy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"company_name": "Kompania S. A",
|
||||
"name": "Inżynier budowlany",
|
||||
"tresc": "Zapraszamy do aplikowania osoby z doświadczeniem w nadzorowaniu i zarządzaniu dużymi projektami budowlanymi.",
|
||||
"salaryRange": "14,000 - 22,000 PLN",
|
||||
"lokalizacja": "Gdańsk, Poland",
|
||||
"neededSkills": {
|
||||
"AutoCAD": "Ekspert",
|
||||
"Zarządzanie projektem": "Zaawansowany",
|
||||
"Nadzór budowlany": "Zaawansowany"
|
||||
}
|
||||
},
|
||||
//... Możesz dodać więcej ogłoszeń według tego wzoru
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const linki = [
|
||||
{
|
||||
"id": "1",
|
||||
@@ -209,12 +169,12 @@ export const linki = [
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "work/jobpostings",
|
||||
"name": "work/joboffers",
|
||||
"title": "Ogłoszenia",
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "work/jobposting",
|
||||
"name": "work/addjoboffer",
|
||||
"title": "Dodaj ogłoszenie",
|
||||
},
|
||||
{
|
||||
@@ -251,3 +211,150 @@ export const linki_home = [
|
||||
"title": "Kontakt",
|
||||
},
|
||||
]
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
"id": "A",
|
||||
"name": "Budownictwo",
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"name": "IT",
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"name": "Elektryka i Elektronika",
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"name": "Produkcja",
|
||||
},
|
||||
{
|
||||
"id": "E",
|
||||
"name": "Mechanika i konstrukcje",
|
||||
},
|
||||
{
|
||||
"id": "F",
|
||||
"name": "Chemia i Biotechnologia",
|
||||
},
|
||||
{
|
||||
"id": "G",
|
||||
"name": "Biomedyczne",
|
||||
},
|
||||
{
|
||||
"id": "H",
|
||||
"name": "Automatyka i Robotyka",
|
||||
},
|
||||
{
|
||||
"id": "I",
|
||||
"name": "Logistyka i Transport",
|
||||
},
|
||||
{
|
||||
"id": "J",
|
||||
"name": "Sprzedaż",
|
||||
},
|
||||
{
|
||||
"id": "Z",
|
||||
"name": "Inne",
|
||||
},
|
||||
]
|
||||
|
||||
export const experience_levels = [
|
||||
{
|
||||
"id": "A",
|
||||
"name": "Stażysta",
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"name": "Junior",
|
||||
},
|
||||
{
|
||||
"id": "C",
|
||||
"name": "Mid",
|
||||
},
|
||||
{
|
||||
"id": "D",
|
||||
"name": "Senior",
|
||||
},
|
||||
{
|
||||
"id": "E",
|
||||
"name": "Lead",
|
||||
},
|
||||
{
|
||||
"id": "F",
|
||||
"name": "Manager",
|
||||
},
|
||||
{
|
||||
"id": "G",
|
||||
"name": "Inne",
|
||||
},
|
||||
]
|
||||
|
||||
export const work_from_home = [
|
||||
{
|
||||
"id": "wfh",
|
||||
"name": "Praca zdalna",
|
||||
},
|
||||
{
|
||||
"id": "hyb",
|
||||
"name": "Hybrydowa",
|
||||
},
|
||||
{
|
||||
"id": "off",
|
||||
"name": "Stacjonarna",
|
||||
},
|
||||
]
|
||||
|
||||
export const employment_types = [
|
||||
{
|
||||
"id": "B2B",
|
||||
"name": "Kontrakt B2B",
|
||||
},
|
||||
{
|
||||
"id": "FT",
|
||||
"name": "Umowa o pracę",
|
||||
},
|
||||
{
|
||||
"id": "MC",
|
||||
"name": "Umowa zlecenie",
|
||||
},
|
||||
{
|
||||
"id": "CW",
|
||||
"name": "Umowa o dzieło",
|
||||
},
|
||||
{
|
||||
"id": "INT",
|
||||
"name": "Staż",
|
||||
},
|
||||
]
|
||||
|
||||
export const lorem_ipsum = [
|
||||
"Lorem ipsum dolor sit amet",
|
||||
"consectetur adipiscing elit",
|
||||
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
"Ut enim ad minim veniam",
|
||||
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ',
|
||||
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
]
|
||||
|
||||
export const lorem_ipsum_starter = [
|
||||
"Lorem ipsum dolor sit amet",
|
||||
"consectetur adipiscing elit",
|
||||
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
"Ut enim ad minim veniam",
|
||||
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
]
|
||||
|
||||
export const lorem_ipsum_premium = [
|
||||
"Lorem ipsum dolor sit amet",
|
||||
"consectetur adipiscing elit",
|
||||
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
"Ut enim ad minim veniam",
|
||||
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ',
|
||||
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
"Ut enim ad minim veniam",
|
||||
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
|
||||
]
|
||||
BIN
src/icon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/icon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/icon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/icon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 502 B |
BIN
src/icon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/icon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
src/icon/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
301
src/index.css
@@ -22,9 +22,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* @layer components {
|
||||
.no-spin-buttons::-webkit-inner-spin-button,
|
||||
.no-spin-buttons::-webkit-outer-spin-button {
|
||||
@apply -webkit-appearance-none margin-0;
|
||||
}
|
||||
.no-spin-buttons {
|
||||
@apply -moz-appearance-textfield;
|
||||
}
|
||||
} */
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dla Firefox */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:root {
|
||||
--black-gradient: linear-gradient(
|
||||
144.39deg,
|
||||
@@ -37,21 +67,95 @@
|
||||
* {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.editor-container {
|
||||
|
||||
.category-hover:hover {
|
||||
transform: scale(1.1);
|
||||
/* Apply negative margin if needed */
|
||||
}
|
||||
|
||||
.slide-container {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height 0.5s ease-out, opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Expanded state styles */
|
||||
.collapsible {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition-property: all;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
/* Styl dla elementu, gdy jest rozwinięty */
|
||||
.collapsible.expanded {
|
||||
max-height: 1000px; /* Przykładowa maksymalna wysokość, może wymagać dostosowania */
|
||||
opacity: 1;
|
||||
transition-property: all;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
.div-transition {
|
||||
transition-property: filter;
|
||||
transition-duration: 1s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.noblur {
|
||||
filter: blur(0px);
|
||||
transition-property: filter;
|
||||
transition-duration: 0.5s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.blur {
|
||||
filter: blur(5px);
|
||||
transition-property: filter;
|
||||
transition-duration: 0.5s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
.minus-z-index {
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 4rem;
|
||||
padding-right: 4rem;
|
||||
padding-left: 12rem;
|
||||
padding-right: 12rem;
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
.div-transition {
|
||||
transition: all 0.3s ease;
|
||||
scale: 100%;
|
||||
}
|
||||
|
||||
.ck-editor__editable {
|
||||
min-height: 30rem;
|
||||
min-height: 12rem;
|
||||
max-height: 12rem;
|
||||
|
||||
}
|
||||
.ck.ck-editor {
|
||||
min-height: 140rem;
|
||||
|
||||
.job-listing {
|
||||
-webkit-mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
}
|
||||
.job-listing2 {
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.lista {
|
||||
list-style: inside;
|
||||
}
|
||||
|
||||
.lista-outside {
|
||||
list-style: outside;
|
||||
}
|
||||
|
||||
.sidebar-show {
|
||||
@@ -64,57 +168,146 @@
|
||||
animation: slide-down 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-top {
|
||||
0% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes slide-top {
|
||||
0% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-down {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.details-section {
|
||||
display: none;
|
||||
}
|
||||
.list-section {
|
||||
display: block;
|
||||
}
|
||||
.slider__track,
|
||||
.slider__range,
|
||||
.slider__left-value,
|
||||
.slider__right-value {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slider__track,
|
||||
.slider__range {
|
||||
border-radius: 3px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.slider__track {
|
||||
background-color: #ced4da;
|
||||
width: 100;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slider__range {
|
||||
background-color: #9fe5e1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.slider__left-value,
|
||||
.slider__right-value {
|
||||
color: #dee2e6;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.slider__left-value {
|
||||
left: 6px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.slider__right-value {
|
||||
right: -4px;
|
||||
color: black;
|
||||
|
||||
}
|
||||
|
||||
/* Removing the default appearance */
|
||||
.thumb,
|
||||
.thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 0;
|
||||
outline: none;
|
||||
transition-duration: 0.5s;
|
||||
transition-property: display;
|
||||
}
|
||||
|
||||
.thumb--left {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.thumb--right {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* For Chrome browsers */
|
||||
.thumb::-webkit-slider-thumb {
|
||||
background-color: #f1f5f7;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 1px 1px #ced4da;
|
||||
cursor: pointer;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-top: 4px;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* For Firefox browsers */
|
||||
.thumb::-moz-range-thumb {
|
||||
background-color: #f1f5f7;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 1px 1px #ced4da;
|
||||
cursor: pointer;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-top: 4px;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.thumb--animatedin {
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.thumb--animatedout {
|
||||
animation: fadeOut 0.5s;
|
||||
}
|
||||
|
||||
.t-width {
|
||||
width: 84%;
|
||||
}
|
||||
|
||||
.search-width {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const styles = {
|
||||
boxWidth: "xl:max-w-[1280px] w-full",
|
||||
boxWidth2: "w-full",
|
||||
heading1: "font-poppins font-semibold xs:text-[40px] text-[40px] text-black xs:leading-[76.8px] leading-[66.8px] w-full",
|
||||
|
||||
heading2: "font-poppins font-semibold xs:text-[48px] text-[40px] text-black xs:leading-[76.8px] leading-[66.8px] w-full",
|
||||
paragraph: "font-poppins font-normal text-black text-[18px] leading-[30.8px]",
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
colors: {
|
||||
primary: "#00040f",
|
||||
secondary: "#00f6ff",
|
||||
dimWhite: "rgba(255, 255, 255, 0.7)",
|
||||
dimWhite: "rgba(255, 255, 255, 0.8)",
|
||||
dimBlue: "rgba(9, 151, 124, 0.1)",
|
||||
},
|
||||
fontFamily: {
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// Dodaj to jeśli korzystasz z HTTPS i potrzebujesz ignorować błędy certyfikatu
|
||||
// import https from 'https';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://izaac.knck.pl',
|
||||
changeOrigin: false,
|
||||
secure: false, // Ustaw na true jeśli łączysz się przez HTTPS
|
||||
// Jeśli twoje połączenie HTTPS wymaga niestandardowego certyfikatu:
|
||||
// https: {
|
||||
// agent: new https.Agent({
|
||||
// rejectUnauthorized: false,
|
||||
// }),
|
||||
// },
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
configure: (proxy, options) => {
|
||||
// Funkcja konfigurująca, gdzie możesz dodać dodatkowe nagłówki
|
||||
proxy.on('proxyReq', function(proxyReq, req) {
|
||||
// Dodaj tutaj swój nagłówek autoryzacyjny
|
||||
proxyReq.setHeader('Authorization', 'Basic cnJnTkxTRXFsY2w0NVJWTVFhMEx4UUxPSE9nWjJMN1psR3BYVXJDcDpsS3NtcmFlU21paFk4clRRZ2Q4VFRHSW5jVWxvVzdYb2tSOFdMSTBvWnE4akNKNTlndUFrb3BnOVpPVWVYRmR5cnF3dUxpNlR5WExaSkRwMUtCOERXRXVzMDV0dFMzTlFIb0x2ZlJvT1F0SnBRYXh6eUlvODVveWtqUW4yNUtlYg==');
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||