Compare commits

...

21 Commits

Author SHA1 Message Date
68d32a775f Merge pull request 'IZCWRK-2' (#2) from develop into master
Some checks failed
continuous-integration/drone Build is failing
Reviewed-on: http://gitea.knck.pl/jakub.kaniecki/izaac-frontend/pulls/2
2024-10-08 19:37:24 +00:00
Jakub Kaniecki
dbf7367c53 IZCWRK-2 Poprawa zgodnie z komentarzem od Mariusza 2024-10-08 21:34:27 +02:00
Jakub Kaniecki
c0125af058 Merge branch 'master' into develop 2024-08-31 23:35:29 +02:00
Jakub Kaniecki
d620433990 testowa zmiana
All checks were successful
continuous-integration/drone/pr Build is passing
2024-08-26 21:59:35 +02:00
Jakub Kaniecki
45a2c8252e zmiany do mobile, filtrowanie, poprawki
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-27 01:42:21 +02:00
Jakub Kaniecki
97b7e8726d filtry + ciastko
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-09 22:37:54 +02:00
Jakub Kaniecki
256bd8a0ee dodanie nowych komponentow oraz dodanie walidacji propTypes
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-08 20:52:43 +02:00
Jakub Kaniecki
10103fb1a1 fix merge conflicts and merge develop to master
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-08 19:59:36 +02:00
Jakub Kaniecki
7020cdeb4e jakies tam poprawki
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 20:33:48 +02:00
Jakub Kaniecki
ee75b0f2eb cicd
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 19:41:53 +02:00
Jakub Kaniecki
07038e720b cicd
Some checks failed
continuous-integration/drone/pr Build is failing
2024-06-07 19:37:42 +02:00
Jakub Kaniecki
e64435143b zmiana adresu 2024-06-07 19:36:50 +02:00
Jakub K
99e6e227e3 przezutka do nowego kompa 2024-04-28 14:43:08 +02:00
Jakub K
b3adb4f8fb zmiana adresow
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2024-03-27 19:13:32 +01:00
Jakub K
2fddcf9ddd zmiana adresow
Some checks failed
continuous-integration/drone/push Build is failing
2024-03-27 19:10:37 +01:00
Jakub K
9e63b87ba6 ci/cd 2024-03-27 19:05:00 +01:00
Jakub K
11b1ff68e2 mala zmiana w widoku 2024-03-27 19:04:09 +01:00
Jakub K
adbd76403f add axios @ package.json
Some checks failed
continuous-integration/drone Build was killed
2024-03-17 19:53:13 +01:00
Jakub K
74f199ea8e deployment - bez usuwania serwisu
Some checks failed
continuous-integration/drone Build is failing
2024-03-17 19:50:49 +01:00
Jakub K
3f8624efd6 RC-0.1 2024-03-17 19:49:59 +01:00
Jakub K
a5f58d1bdb misc fixes, new features 2024-02-13 21:19:37 +01:00
42 changed files with 15203 additions and 985 deletions

View File

@@ -5,16 +5,15 @@ name: default
steps: steps:
- name: build - name: build
commands: commands:
- docker build --no-cache -t izaac-frontend:latest . - docker build --no-cache -t izaac-frontend-master:latest .
- docker tag izaac-frontend:latest registry.izaac.pl:5000/izaac-frontend:latest - docker tag izaac-frontend-master:latest registry.izaac.pl:5000/izaac-frontend-master:latest
- docker push registry.izaac.pl:5000/izaac-frontend:latest - docker push registry.izaac.pl:5000/izaac-frontend-master:latest
- name: delete - name: delete
environment: environment:
KUBECONFIG: /home/drone-runner/drone-kubeconfig KUBECONFIG: /home/drone-runner/drone-kubeconfig
commands: commands:
- kubectl delete deployment izaac-frontend || true - kubectl delete deployment izaac-frontend || true
- kubectl delete service izaac-frontend || true
- kubectl delete ingress izaac-frontend || true - kubectl delete ingress izaac-frontend || true
- name: deploy - name: deploy

View File

@@ -31,7 +31,7 @@ spec:
spec: spec:
containers: containers:
- name: izaac-frontend - name: izaac-frontend
image: registry.izaac.pl:5000/izaac-frontend:latest image: registry.knck.pl:5000/izaac-frontend-master:latest
ports: ports:
- containerPort: 80 - containerPort: 80
volumeMounts: volumeMounts:

10
eslint.config.js Normal file
View 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,
];

View File

@@ -7,7 +7,7 @@
<link rel="icon" href="../icon/favicon.ico"/> <link rel="icon" href="../icon/favicon.ico"/>
<!-- todo ICONs --> <!-- todo ICONs -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>izaac frontend</title> <title>Izaac - praca dla inżynierów</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

13322
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,28 +6,32 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ckeditor/ckeditor5-build-classic": "^40.0.0", "@ckeditor/ckeditor5-build-classic": "^40.0.0",
"@ckeditor/ckeditor5-build-multi-root": "^40.0.0", "@ckeditor/ckeditor5-build-multi-root": "^40.0.0",
"@ckeditor/ckeditor5-react": "^6.1.0", "@ckeditor/ckeditor5-react": "^6.1.0",
"@ckeditor/ckeditor5-word-count": "^43.2.0",
"axios": "^0.24.0",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.18.0" "react-router-dom": "^6.18.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.45.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0", "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": "^8.4.31",
"postcss-nesting": "^12.0.1", "postcss-nesting": "^12.0.1",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",

View File

@@ -1,48 +1,44 @@
import styles from './style'; import styles from "./style";
import NavBar from './components/NavBar'; import NavBar from "./components/NavBar";
import WorkApp from './components/WorkApp'; import WorkApp from "./components/WorkApp";
import Home from './components/Home'; import Home from "./components/Home";
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from "react-router-dom";
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from "react-router-dom";
import Contact from './components/Contact'; import Contact from "./components/Contact";
import Mininav from './components/Mininav'; import Mininav from "./components/Mininav";
import AddJobListing from './components/AddJobListing'; import AddJobListing from "./components/AddJobListing";
import Cookies from "./components/Cookies";
function App() { function App() {
const loc = () =>{
if (location.pathname.startsWith('work/jobposting')){
return ""
} else {
return "overflow-hidden"
}
}
return ( return (
<Router> <Router>
<div className={`bg-white w-full ${loc}`}> <div className={`bg-white w-full`}>
<div className={`${styles.paddingX} ${styles.flexCenter} border-b-2`}> <div className={`${styles.paddingX} ${styles.flexCenter}`}>
<Mininav /> {/* <Mininav /> */}
<div className={`${styles.boxWidth}`}> <div className={`${styles.boxWidth} `}>
<NavBar /> <NavBar />
</div> </div>
</div> </div>
<div className={`${styles.flexStart}`}> <div className={`${styles.flexStart} mt-18 sm:mt-[4.5rem] md:mt-[6.5rem]`}>
<div className={`${styles.boxWidth2} `}> <div className={`${styles.boxWidth2} `}>
<Routes> <Routes>
<Route path="/home" element={<Home />} /> <Route path="/home" element={<Home />} />
<Route path="/work" element={<WorkApp />} /> <Route path="/work" element={<WorkApp />} />
<Route path="/work/postings" element={<WorkApp />} /> <Route path="/work/joboffers" element={<WorkApp />} />
<Route path="/contact" element={<Contact />} /> <Route path="/contact" element={<Contact />} />
<Route path="/work/jobposting" element={<AddJobListing />} /> <Route path="/work/addjoboffer" element={<AddJobListing />} />
<Route path="/work/joboffers/:id" element={<WorkApp/>} />
{/* <Route path="/employerpanel" element={<EmployerPanel />} /> */}
{/* Add more routes as needed */} {/* Add more routes as needed */}
</Routes> </Routes>
<Cookies />
</div> </div>
</div> </div>
</div> </div>
</Router> </Router>
) );
}; }
export default App; export default App;

View File

@@ -1,13 +1,17 @@
import React, { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import StepOneJoblisting from './StepOneJoblisting'; import StepOneJoblisting from './StepOneJoblisting';
import StepTwoJoblisting from './StepTwoJoblisting'; import StepTwoJoblisting from './StepTwoJoblisting';
import StepThreeJoblisting from './StepThreeJoblisting'; import StepThreeJoblisting from './StepThreeJoblisting';
import StepFourJoblisting from './StepFourJoblisting' import StepFourJoblisting from './StepFourJoblisting'
import Success from './Success';
import axios from 'axios';
const AddJobListing = () => { const AddJobListing = () => {
const [currentStep, setCurrentStep] = useState(1) const [currentStep, setCurrentStep] = useState(1)
const [skills, setSkills] = useState([])
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
'postingOption': 'Opcja 2', 'posting_option': 'S',
'requireSalary': true 'require_salary': true
}) })
const nextStep = () => { const nextStep = () => {
setCurrentStep(currentStep + 1); setCurrentStep(currentStep + 1);
@@ -47,13 +51,34 @@ const AddJobListing = () => {
console.log('Aktualny stan formularza:', formData); console.log('Aktualny stan formularza:', formData);
}; };
const handleSubmit = () => { 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(() => { useEffect(() => {
console.log('Aktualny stan formularza:', formData); console.log('Aktualny stan formularza:', formData);
}, [formData]); }, [formData]);
useEffect(() => {
getSkills();
} , [currentStep])
switch (currentStep) { switch (currentStep) {
case 1: case 1:
return <StepOneJoblisting return <StepOneJoblisting
@@ -64,6 +89,7 @@ const AddJobListing = () => {
return <StepFourJoblisting return <StepFourJoblisting
nextStep={nextStep} nextStep={nextStep}
formData={formData} formData={formData}
prevStep={prevStep}
handleChange={handleChange} />; handleChange={handleChange} />;
case 3: case 3:
return <StepTwoJoblisting return <StepTwoJoblisting
@@ -72,12 +98,17 @@ const AddJobListing = () => {
removeFields={removeFields} removeFields={removeFields}
setFormData={setFormData} setFormData={setFormData}
handleChange={handleChange} handleChange={handleChange}
formData={formData} />; formData={formData}
skills={skills} />;
case 4: case 4:
return <StepThreeJoblisting return <StepThreeJoblisting
prevStep={prevStep} prevStep={prevStep}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
formData={formData} />; formData={formData}
skills={skills}
/>;
case 5:
return <Success />;
default: default:
return <div>Unknown step</div>; return <div>Unknown step</div>;
} }

17
src/components/Button.jsx Normal file
View 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} &nbsp;&nbsp;
<span className="text-[18px]"></span>{" "}
</button>
)
}
export default Button

View 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

View 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'>&times;</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

View 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

View 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
View 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;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import styles from '../style' import styles from '../style'
const Home = () => { const Home = () => {
return ( return (

View File

@@ -1,16 +1,21 @@
import React, { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { IzaacLOGO, placeholderImage } from '../assets' import { placeholderImage } from '../assets';
import axios from 'axios';
import propTypes from 'prop-types';
const ImageUpload = ({setFormData}) => { const ImageUpload = ({setFormData, data}) => {
const [imageSrc, setImageSrc] = useState(placeholderImage); const [imageSrc, setImageSrc] = useState(placeholderImage);
const fileInputRef = useRef(); // Referencja do ukrytego inputu plików const fileInputRef = useRef(); // Referencja do ukrytego inputu plików
const [uploadedImageData, setUploadedImageData] = useState(null);
const handleImageChange = (e) => {
const handleImageChange = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = async (e) => {
setImageSrc(e.target.result); // Aktualizujemy stan obrazkiem użytkownika // Aktualizujemy stan obrazkiem użytkownika
const uploadedImageData = await uploadImage(file);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
@@ -20,6 +25,25 @@ const ImageUpload = ({setFormData}) => {
fileInputRef.current.click(); // Programowe kliknięcie na ukrytym inpucie plików 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(() => { useEffect(() => {
// Aktualizujemy dane w hooku nadrzędnym za każdym razem, gdy zmienia się imageSrc // Aktualizujemy dane w hooku nadrzędnym za każdym razem, gdy zmienia się imageSrc
if (imageSrc !== placeholderImage) { if (imageSrc !== placeholderImage) {
@@ -59,4 +83,9 @@ const ImageUpload = ({setFormData}) => {
); );
}; };
ImageUpload.propTypes = {
setFormData: propTypes.func,
data: propTypes.object
};
export default ImageUpload; export default ImageUpload;

View 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;

View File

@@ -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

View 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
}

View File

@@ -1,4 +1,4 @@
import React from 'react' import propTypes from 'prop-types';
import styles from '../style'; import styles from '../style';
const Login = ({isOpen, onClose}) => { const Login = ({isOpen, onClose}) => {
@@ -31,4 +31,9 @@ const Login = ({isOpen, onClose}) => {
) )
} }
Login.propTypes = {
isOpen: propTypes.bool,
onClose: propTypes.func
}
export default Login export default Login

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react' import { useState } from 'react'
import Register from './Register'; import Register from './Register';
import Login from './Login'; import Login from './Login';
const Mininav = () => { const Mininav = () => {
const [isPopupOpen, setPopupOpen] = useState(false) const [isPopupOpen, setPopupOpen] = useState(false)
const [isLoginOpen, setLoginOpen] = useState(false) const [isLoginOpen, setLoginOpen] = useState(false)
@@ -13,14 +14,16 @@ const Mininav = () => {
return ( 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 */} {/* 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'> <button onClick={openPopup} className='text-white font-poppins font-semibold text-xs mx-12'>
Rejestracja Rejestracja
</button> </button>
<button onClick={openLogin} className='text-white font-poppins font-semibold text-xs sm:mr-12 mr-6 sm:pr-12'> <button onClick={openLogin} className='text-white font-poppins font-semibold text-xs sm:mr-12 mr-6 sm:pr-12'>
Zaloguj się Zaloguj się
</button> </button>
</div> </div>
{/* Komponent modalu */} {/* Komponent modalu */}
<Register isOpen={isPopupOpen} onClose={closePopup} /> <Register isOpen={isPopupOpen} onClose={closePopup} />

View File

@@ -1,10 +1,10 @@
import React from 'react'
import { linki, linki_home } from '../consts'; import { linki, linki_home } from '../consts';
import { close, search, menu, IzaacLOGO} from '../assets'; import { close, search, menu, IzaacLOGO} from '../assets';
import { useState } from 'react'; import { useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Mininav from './Mininav'; import Mininav from './Mininav';
const NavBar = () => { const NavBar = () => {
const[toggle, setToggle] = useState(false) const[toggle, setToggle] = useState(false)
const[toggleSearch, setToggleSearch] = useState(false) const[toggleSearch, setToggleSearch] = useState(false)
@@ -20,15 +20,18 @@ const NavBar = () => {
const currentLinks = getLinks() const currentLinks = getLinks()
return ( return (
<nav className='w-full flex pt-7 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]'/> <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'> <ul className='list-none sm:flex hidden justify-end items-center flex-1'>
{currentLinks.map((nav, index) => ( {currentLinks.map((nav, index) => (
<li <li
key={nav.id} 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> <Link to={`/${nav.name}`}>{nav.title}</Link>
</li> </li>
@@ -50,12 +53,18 @@ const NavBar = () => {
alt="menu" alt="menu"
className='w-[28px] h-[28px] object-contain' className='w-[28px] h-[28px] object-contain'
onClick={() => setToggle((prev) => !prev)}/> 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`}> <div className={`${toggle ? 'sidebar-show' : 'hidden'} p-6
<ul className='list-none flex flex-col justify-end items-center flex-1'> 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) =>( {currentLinks.map((nav, index) =>(
<div <div
key={nav.id} 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> <Link to={`/${nav.name}`}>{nav.title}</Link>
</div> </div>
@@ -67,5 +76,4 @@ const NavBar = () => {
) )
} }
export default NavBar export default NavBar

View 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;

View File

@@ -1,5 +1,5 @@
import React from 'react'
import styles from '../style'; import styles from '../style';
import propTypes from 'prop-types';
const Register = ({isOpen, onClose}) => { const Register = ({isOpen, onClose}) => {
if (!isOpen) return null; if (!isOpen) return null;
@@ -25,10 +25,14 @@ const Register = ({isOpen, onClose}) => {
<div className='mb-6 pr-4'> <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 `}/> <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>
<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 `}/> <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> <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>
<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"> <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 &nbsp;&nbsp;s i ę Z a r e j e s t r u j &nbsp;&nbsp;s i ę
@@ -40,5 +44,8 @@ const Register = ({isOpen, onClose}) => {
</div> </div>
) )
} }
Register.propTypes = {
isOpen: propTypes.bool,
onClose: propTypes.func
}
export default Register export default Register

View File

@@ -1,35 +1,24 @@
import React, { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import styles from '../style' import propTypes from 'prop-types'
const EmploymentTypes = [
'Kontrakt B2B', import {employment_types, work_from_home } from '../consts'
'Umowa o pracę',
'Umowa zlecenie',
'Umowa o dzieło',
'Staż',
]
const WorkFromHomes = [
'Praca zdalna',
'Hybrydowo',
'W biurze',
'Inne'
]
const Salary = ({handleChange, formData, removeFields, setFormData}) => { const Salary = ({handleChange, formData, removeFields, setFormData}) => {
const [requireSalary, setRequireSalary] = useState(true) const [require_salary, setrequire_salary] = useState(true)
const handleRequireSalary = () => { const handlerequire_salary = () => {
setRequireSalary(prevRequireSalary => !prevRequireSalary); setrequire_salary(prevrequire_salary => !prevrequire_salary);
}; };
useEffect(() => { useEffect(() => {
setFormData({...formData, 'requireSalary': requireSalary}); setFormData({...formData, 'require_salary': require_salary});
}, [requireSalary]); }, [require_salary]);
const [minBigger, setMinBigger] = useState(true); const [minBigger, setMinBigger] = useState(true);
const handleCheck = () => { const handleCheck = () => {
let minSalary = parseInt(formData.minSalary); let min_salary = parseInt(formData.min_salary);
let maxSalary = parseInt(formData.maxSalary); let max_salary = parseInt(formData.max_salary);
if (minSalary > maxSalary) { if (min_salary > max_salary) {
setMinBigger(true) setMinBigger(true)
} }
else { else {
@@ -39,29 +28,29 @@ const Salary = ({handleChange, formData, removeFields, setFormData}) => {
useEffect(() => { useEffect(() => {
handleCheck(); handleCheck();
console.log(minBigger) console.log(minBigger)
console.log(formData.minSalary) console.log(formData.min_salary)
console.log(formData.maxSalary) console.log(formData.max_salary)
}, [formData.minSalary, formData.maxSalary]); }, [formData.min_salary, formData.max_salary]);
useEffect(() => { useEffect(() => {
}) })
useEffect( () =>{ useEffect( () =>{
if (formData.requireSalary === false) { if (formData.require_salary === false) {
removeFields(['minSalary', 'maxSalary']); removeFields(['min_salary', 'max_salary']);
setRequireSalary(formData.requireSalary) setrequire_salary(formData.require_salary)
} }
}, [formData.requireSalary]) }, [formData.require_salary])
return ( return (
<div className='grid grid-cols-4 mt-4 gap-6'> <div className={`grid grid-cols-4 mt-4 gap-6 ${minBigger ? 'mb-10' : ''} duration-300`}>
<div className='col-span-4'> <div className='col-span-4'>
<input <input
className='ml-6' className='ml-6'
type="checkbox" type="checkbox"
checked={!requireSalary} checked={!require_salary}
name="requireSalary" name="require_salary"
onChange={handleRequireSalary} onChange={handlerequire_salary}
id="" /> 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> <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>
@@ -69,53 +58,53 @@ const Salary = ({handleChange, formData, removeFields, setFormData}) => {
<div className='relative'> <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> <p className='ml-5 font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Min. wynagrodzenie</p>
<input <input
disabled={!requireSalary} disabled={!require_salary}
name='minSalary' name='min_salary'
value={formData['minSalary'] || '' } value={formData['min_salary'] || '' }
onChange={handleChange('minSalary')} onChange={handleChange('min_salary')}
type='number' type='number'
className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!requireSalary ? 'bg-slate-200' : ''}`}/> className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!require_salary ? 'bg-slate-200' : ''}`}/>
{minBigger && ( {minBigger && (
<div className='absolute top-[5rem] left-8 border-2 border-red-500 bg-red-200 rounded-b-xl p-2 opacity-90' > <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-[14px] mt-1 mb-1'>Minimalne wynagrodzenie nie może być większe od maks. wynagrodzenia</p> <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> </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> <p className='ml-5 font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Maks. wynagrodzenie</p>
<input <input
disabled={!requireSalary} disabled={!require_salary}
value={formData['maxSalary'] || '' } value={formData['max_salary'] || '' }
name="maxSalary" name="max_salary"
onChange={handleChange('maxSalary')} onChange={handleChange('max_salary')}
type='number' type='number'
className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!requireSalary ? 'bg-slate-200' : ''} `}/></div> className={`h-12 border-2 rounded-xl px-5 mx-4 w-full ${!require_salary ? 'bg-slate-200' : ''} `}/></div>
<div> <div>
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Typ kontraktu</p> <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 <select
value={formData['employmentType'] || 'default' } value={formData['employment_type'] || 'default' }
name='employmentType' name='employmentType'
onChange={handleChange('employmentType')} onChange={handleChange('employment_type')}
className='h-12 border-2 rounded-xl px-5 mx-4 w-full '> className='h-12 border-2 rounded-xl px-5 mx-4 w-full '>
<option value={'default'} disabled >Wybierz opcję</option> <option value={'default'} disabled >Wybierz opcję</option>
{EmploymentTypes.map(EmploymentType => ( {employment_types.map(employment_type => (
<option key={EmploymentType}>{EmploymentType}</option> <option key={employment_type.id} value={employment_type.id}>{employment_type.name}</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<p className='font-poppins font-semibold text-slate-700 text-center text-[14px] mt-1 mb-2'>Praca zdalna</p> <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 <select
value={formData['workFromHome'] || 'default' } value={formData['work_from_home'] || 'default' }
name='workFromHome' name='workFromHome'
onChange={handleChange('workFromHome')} onChange={handleChange('work_from_home')}
className='h-12 border-2 rounded-xl px-5 mx-4 w-full'> className='h-12 border-2 rounded-xl px-5 mx-4 w-full'>
<option value={'default'} disabled >Wybierz opcję</option> <option value={'default'} disabled >Wybierz opcję</option>
{work_from_home.map(work_from_home => (
{WorkFromHomes.map(WorkFromHomes => ( <option key={work_from_home.id} value={work_from_home.id}>{work_from_home.name}</option>
<option key={WorkFromHomes}>{WorkFromHomes}</option>
))} ))}
</select> </select>
</div> </div>
@@ -123,5 +112,18 @@ const Salary = ({handleChange, formData, removeFields, setFormData}) => {
</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 export default Salary

62
src/components/Search.jsx Normal file
View 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

View File

@@ -1,37 +1,26 @@
import React from 'react'; import propTypes from 'prop-types';
const levels = ['Nice to have','Podstawowy', 'Średnio zaawansowany', 'Zaawansowany', 'Ekspert']; const levelMappings = {
'N': 'Nice to have',
'B': 'Podstawowy',
'M': 'Średnio zaawansowany',
'A': 'Zaawansowany',
'E': 'Ekspert',
};
const skillLevels = ['N', 'B', 'M', 'A', 'E'];
function renderCircles(level, handleCircleClick) { function renderCircles(letter, handleCircleClick) {
let numberOfFilledCircles; const level = levelMappings[letter];
const numberOfFilledCircles = Object.keys(levelMappings).indexOf(letter) + 1;
switch (level) {
case 'Nice to have':
numberOfFilledCircles = 1;
break;
case 'Podstawowy':
numberOfFilledCircles = 2;
break;
case 'Średnio zaawansowany':
numberOfFilledCircles = 3;
break
case 'Zaawansowany':
numberOfFilledCircles = 4;
break;
case 'Ekspert':
numberOfFilledCircles = 5;
break;
default:
numberOfFilledCircles = 1;
}
return ( return (
<> <>
{[...Array(5)].map((_, index) => ( {[...Array(5)].map((_, index) => (
<div <div
key={index} key={index}
onClick={() => handleCircleClick(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'}`} 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'}`}
/> />
))} ))}
</> </>
@@ -39,27 +28,37 @@ function renderCircles(level, handleCircleClick) {
}; };
const SelectedSkill = ({ skill, level, onLevelChange, removeSkillFromList, formData }) => { const SelectedSkill = ({ skill_name, skillId ,letter, onLevelChange, removeSkillFromList }) => {
const handleCircleClick = (levelIndex) => { const handleCircleClick = (newLetter) => {
const level = levels[levelIndex]; // const letter = Object.keys(levelMappings)[levelIndex];
onLevelChange(skill, level); console.log(`Circle clicked: ${newLetter}`);
onLevelChange(skillId, newLetter);
}; };
return ( return (
<div className="skill-block"> <div className="mr-12">
<div key={skill} className="relative selected-skill bg-slate-200 rounded-2xl h-20 w-56 p-2 mb-5"> <div key={skillId} className="relative selected-skill bg-slate-200 rounded-2xl h-min w-full p-2 mb-5 m-2 ">
<p className='text-slate-700 font-semibold text-[12px] text-center'>{skill}</p> <div className='flex justify-center items-center'>
<button onClick={(e) => {e.preventDefault(); removeSkillFromList(skill)}} className="absolute top-1 right-5 text-black"> <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">
&times; &times;
</button> </button>
<div className='h-0.5 w-full bg-dimWhite opacity-60 mt-1'></div> <div className='h-0.5 w-full bg-dimWhite opacity-60 mt-1'></div>
<div className='grid grid-cols-5 mt-2'> <div className='grid grid-cols-5 mt-2'>
{renderCircles(level, handleCircleClick)} {renderCircles(letter, handleCircleClick)}
</div> </div>
<p className='font-poppins font-semibold text-slate-700 text-center text-[12px] mt-1'>{level}</p> <p className='font-poppins font-semibold text-slate-700 text-center text-[12px] mt-1'>{levelMappings[letter]}</p>
</div> </div>
</div> </div>
); );
}; };
SelectedSkill.propTypes = {
skill_name: propTypes.string,
skillId: propTypes.number,
letter: propTypes.string,
onLevelChange: propTypes.func,
removeSkillFromList: propTypes.func,
};
export default SelectedSkill; export default SelectedSkill;

View 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

View 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;

View 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;

View File

@@ -1,143 +1,110 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import propTypes from 'prop-types';
import SelectedSkill from './SelectedSkill'; import SelectedSkill from './SelectedSkill';
const initialSkillsList = [
'Inżynieria oprogramowania',
'Analiza danych',
'Projektowanie CAD',
'Automatyka',
'Robotyka',
'Inżynieria materiałowa',
'Inżynieria elektryczna',
'Inżynieria mechaniczna',
'Programowanie mikrokontrolerów',
'Analiza strukturalna',
'Modelowanie 3D',
'Projektowanie układów elektronicznych',
'Programowanie PLC',
'Sztuczna inteligencja',
'Machine learning',
'Inżynieria chemiczna',
'Inżynieria biomedyczna',
'Inżynieria środowiska',
'Automatyka przemysłowa',
'Zarządzanie projektami technicznymi',
'Prototypowanie',
'Przetwarzanie sygnałów',
'Inżynieria systemów',
'Analiza termodynamiczna',
'Fizyka stosowana',
'Inżynieria procesowa',
'Wytwarzanie addytywne',
'Technologia nanomateriałów',
'Projektowanie systemów wbudowanych',
'Inżynieria bezpieczeństwa',
// ... więcej umiejętności ...
];
const SkillsSelector = ({formData, setFormData}) => { const SkillsSelector = ({ formData, setFormData, skills }) => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [suggestions, setSuggestions] = useState([]); const [suggestions, setSuggestions] = useState([]);
const [selectedSkills, setSelectedSkills] = useState([]); const [selectedSkills, setSelectedSkills] = useState([]);
const [skill_levels, setskill_levels] = useState([]);
const handleInputChange = (e) => { const handleInputChange = (e) => {
const value = e.target.value; const value = e.target.value;
setInputValue(value); setInputValue(value);
if (value.length >= 0) { if (value.length > 0) {
setSuggestions(initialSkillsList.filter(skill => setSuggestions(skills.filter(skill =>
skill.toLowerCase().includes(value.toLowerCase()))); skill.skill_name.toLowerCase().includes(value.toLowerCase())
));
} else { } else {
setSuggestions([]); setSuggestions([]);
} }
}; };
const handleSuggestionClick = (skill) => { // Handle click on suggestion
if (!selectedSkills.includes(skill)) { const handleSuggestionClick = (suggestion) => {
setSelectedSkills(prevSkills => [skill, ...prevSkills]); const skill = skills.find(skill => skill.skill_name === suggestion);
};
setSkillLevels(prevLevels => ({ if (skill && !selectedSkills.some(selectedSkill => selectedSkill.id === skill.id)) {
...prevLevels, setSelectedSkills(prevSkills => [...prevSkills, skill]);
[skill]: 'Nice to have' // lub inny domyślny poziom setskill_levels(prevLevels => [...prevLevels, { skill_id: skill.id, skill_level: 'N' }]);
})); }
setInputValue(''); setInputValue('');
setSuggestions([]); setSuggestions([]);
}; };
// Handle removal of a skill from the list
const removeSkillFromList = (skillToRemove) => { const removeSkillFromList = (skillToRemove) => {
setSelectedSkills(prevSkills => prevSkills.filter(skill => skill !== skillToRemove)); setSelectedSkills(prevSkills => prevSkills.filter(skill => skill.id !== skillToRemove.id));
setSkillLevels(prevLevels => { setskill_levels(prevLevels => prevLevels.filter(level => level.skill_id !== skillToRemove.id));
const newLevels = { ...prevLevels };
delete newLevels[skillToRemove];
return newLevels;
});
}; };
const [skillLevels, setSkillLevels] = useState({}); // Handle skill level change
const handleLevelChange = (skill, newLevel) => { const handleLevelChange = (skillId, newLevel) => {
setSkillLevels(prevLevels => ({ ...prevLevels, [skill]: newLevel })); setskill_levels(prevLevels =>
prevLevels.map(level =>
level.skill_id === skillId ? { ...level, skill_level: newLevel } : level
)
);
}; };
// Update formData on skill_levels change
useEffect(() => { useEffect(() => {
// Aktualizacja formData, aby zawierała skillLevels
setFormData(prevFormData => ({ setFormData(prevFormData => ({
...prevFormData, ...prevFormData,
skillLevels: skillLevels skill_levels
})); }));
}, [skillLevels]); }, [skill_levels, setFormData]);
useEffect(() => {
// Jeśli formData zawiera już wybrane umiejętności i ich poziomy
if (formData.skillLevels) {
setSkillLevels(formData.skillLevels);
// Ustaw wybrane umiejętności na podstawie kluczy w formData.skillLevels
setSelectedSkills(Object.keys(formData.skillLevels));
}
}, []);
useEffect(() => {
console.log('Zaktualizowane poziomy umiejętności:', skillLevels);
}, [skillLevels, ]);
return ( return (
<div className='relative mt-3 grid grid-cols-5'> <div className='relative mt-3 grid grid-cols-2'>
<div className='col-span-5 grid mb-4'> <div className='col-span-5 grid mb-4'>
<div> <input
<input type="text"
type="text" value={inputValue}
value={inputValue} onChange={handleInputChange}
onChange={handleInputChange} placeholder="Wpisz umiejętność..."
placeholder="Wpisz umiejętność" className="selected-skill bg-slate-200 rounded-2xl h-20 w-full
className="selected-skill bg-slate-200 rounded-2xl h-20 w-56 py-2 px-4 text-slate-700 font-semibold text-[12px] col-span-3" 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>
{inputValue.length >= 1 && suggestions.length > 0 && ( <div className='col-span-5 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3'>
<div className="absolute border-4 w-56 top-16 z-10 overflow-y-auto overflow-x-hidden m" >
{suggestions.map(suggestion => (
<div
key={suggestion}
onClick={() => handleSuggestionClick(suggestion)}
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}</p>
</div>
))}
</div>
)}
</div>
{/* Lista wybranych umiejętności z możliwością określenia poziomu */}
{selectedSkills.map(skill => ( {selectedSkills.map(skill => (
<SelectedSkill <SelectedSkill
key={skill} key={skill.id}
skill={skill} skill_name={skill.skill_name}
level={skillLevels[skill] || 'Nice to have'} // Domyślny poziom, jeśli nie ustawiony skillId={skill.id}
onLevelChange={handleLevelChange} letter={skill_levels.find(
removeSkillFromList={removeSkillFromList} level => level.skill_id === skill.id)?.skill_level || 'N'}
onLevelChange={handleLevelChange}
removeSkillFromList={() => removeSkillFromList(skill)}
/> />
))} ))}
</div>
</div> </div>
); );
}; };
SkillsSelector.propTypes = {
formData: propTypes.object.isRequired,
setFormData: propTypes.func.isRequired,
skills: propTypes.array.isRequired
};
export default SkillsSelector; export default SkillsSelector;

View File

@@ -1,13 +1,36 @@
import React from "react"; import propTypes from "prop-types";
import { useEffect } from "react";
import styles from "../style"; import styles from "../style";
import TextDivider from "./TextDivider"; import TextDivider from "./TextDivider";
import Button from "./Button";
const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => { 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 ( return (
<div className={`${styles.flexCenter}`}> <div className={`${styles.flexCenter}`}>
<div className="grid grid-cols-5"> <div className="grid grid-cols-5">
<div className="col-span-3 col-start-2 mt-12"> <div className="col-span-3 col-start-2 mt-12">
<TextDivider text={`Podaj dane ogłoszeniodawcy`} /> <TextDivider text={`Podaj dane ogłoszeniodawcy`} />
<div className="grid grid-cols-3"> <div className="grid grid-cols-2">
<div className="col-span-1 mt-4 mx-2"> <div className="col-span-1 mt-4 mx-2">
<div className={`${styles.paragraph} px-4 py-2`}> <div className={`${styles.paragraph} px-4 py-2`}>
Imię Imię
@@ -21,8 +44,9 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
id="first_name" id="first_name"
onChange={handleChange("first_name")} onChange={handleChange("first_name")}
placeholder="Twoje imię..." placeholder="Twoje imię..."
className={`border-b-2 px-4 my-2 ${styles.paragraph}`} className={`px-4 mt-2 ${styles.paragraph}`}
/> />
<div className="bg-black h-0.5 opacity-10"></div>
</div> </div>
<div className="col-span-1 mt-4 mx-2"> <div className="col-span-1 mt-4 mx-2">
<div className={`${styles.paragraph} px-4 py-2`}> <div className={`${styles.paragraph} px-4 py-2`}>
@@ -37,8 +61,9 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
id="last_name" id="last_name"
onChange={handleChange("last_name")} onChange={handleChange("last_name")}
placeholder="Twoje nazwisko..." placeholder="Twoje nazwisko..."
className={`border-b-2 px-4 my-2 ${styles.paragraph}`} className={`px-4 mt-2 ${styles.paragraph}`}
/> />
<div className="bg-black h-0.5 opacity-10"></div>
</div> </div>
<div className="col-span-1 mt-4 mx-2"> <div className="col-span-1 mt-4 mx-2">
<div className={`${styles.paragraph} px-4 py-2`}> <div className={`${styles.paragraph} px-4 py-2`}>
@@ -52,16 +77,17 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
required required
id="contact_email" id="contact_email"
onChange={handleChange("contact_email")} onChange={handleChange("contact_email")}
onBlur={validateEmail}
placeholder="Adres mailowy..." placeholder="Adres mailowy..."
className={`border-b-2 px-4 my-2 ${styles.paragraph}`} className={`px-4 mt-2 ${styles.paragraph}`}
/> />
<div className="bg-black h-0.5 opacity-10"></div>
</div> </div>
<div className="col-span-2 mt-4 mx-2"> <div className="col-span-1 mt-4 mx-2">
<div className={`${styles.paragraph} px-4 py-2`}> <div className={`${styles.paragraph} px-4 py-2`}>
Nazwa firmy Nazwa firmy
<span className={`${styles.paragraph} text-red-700`}>*</span> <span className={`${styles.paragraph} text-red-700`}>*</span>
</div> </div>
<div className="grid grid-cols-2">
<input <input
type="text" type="text"
name="company_name" name="company_name"
@@ -70,13 +96,13 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
onChange={handleChange("company_name")} onChange={handleChange("company_name")}
id="company_name" id="company_name"
placeholder="Wpisz nazwę firmy..." placeholder="Wpisz nazwę firmy..."
className={`border-b-2 px-4 mr-12 my-2 ${styles.paragraph} col-span-2 `} className={`px-4 mt-2 ${styles.paragraph}`}
/> />
</div> <div className="bg-black h-0.5 opacity-10"></div>
</div> </div>
<div className="col-span-1 mt-4 mx-2"> <div className="col-span-1 mt-4 mx-2">
<div className={`${styles.paragraph} px-4 py-2`}> <div className={`${styles.paragraph} px-4 py-2`}>
Nazwa firmy Nip firmy
<span className={`${styles.paragraph} text-red-700`}>*</span> <span className={`${styles.paragraph} text-red-700`}>*</span>
</div> </div>
<input <input
@@ -88,27 +114,25 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
onChange={handleChange("vat_number")} onChange={handleChange("vat_number")}
id="vat_number" id="vat_number"
placeholder="Wpisz nip..." placeholder="Wpisz nip..."
className={`border-b-2 px-4 my-2 ${styles.paragraph} `} className={`px-4 mt-2 ${styles.paragraph}`}
/> />
<div className="bg-black h-0.5 opacity-10"></div>
</div> </div>
</div> </div>
<div className={`${styles.flexCenter} gap-16 mt-16`}> <div className={`${styles.flexCenter} gap-16 mt-16`}>
<button <button
type="button" type="button"
onClick={prevStep} onClick={prevStep}
className="h-12 w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] scale-100 text-white hover:scale-125 duration-300" 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>&nbsp;&nbsp;Przejdź do <span className="text-[18px]"></span>&nbsp;&nbsp;Przejdź do <br className="block md:hidden"/>
poprzedniego kroku poprzedniego kroku
</button> </button>
<button
type="button" <Button
onClick={nextStep} isDiasbled={validateForm()}
className="h-12 w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] scale-100 text-white hover:scale-125 duration-300" nextStep={nextStep}
> Text="Przejdź do następnego kroku"/>
Przejdź do następnego kroku &nbsp;&nbsp;
<span className="text-[18px]"></span>{" "}
</button>
</div> </div>
</div> </div>
@@ -117,4 +141,11 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
); );
}; };
StepFourJoblisting.propTypes = {
handleChange: propTypes.func,
formData: propTypes.object,
nextStep: propTypes.func,
prevStep: propTypes.func,
};
export default StepFourJoblisting; export default StepFourJoblisting;

View File

@@ -1,68 +1,107 @@
import React from 'react'
import styles from '../style' 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 }) => { const StepOneJoblisting = ({ nextStep, handleChange, formData }) => {
// Funkcja do obsługi kliknięcia na div i aktualizacji stanu // Funkcja do obsługi kliknięcia na div i aktualizacji stanu
const handleDivClick = (value) => () => { const handleDivClick = (value) => () => {
handleChange('postingOption')({ target: { value: value } }); handleChange("posting_option")({ target: { value: value } });
}; };
const activeStyle = "h-[500px] w-64 border-4 rounded-xl border-stone-200 bg-gray-200 div-transition scale-125"; const activeStyle =
const inactiveStyle = "h-[500px] w-64 rounded-xl bg-gray-200 border-gray-200 div-transition cursor-pointer hover:bg-gray-300"; // Dodaj tę samą klasę przejścia tutaj "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`}>
return ( <div
<form> className={
<div className={` grid grid-cols-1 ${styles.paddingX} pb-8 pt-8 gap-1 h-full`}> formData.posting_option === "M" ? activeStyle : inactiveStyle
<h1 className={`${styles.heading1} text-center`}>Zacznij dodawać ogłoszenie z izaac.pl</h1> }
<p className={`${styles.paragraph} text-center`}>Wybierz pakiet najlepiej odpowiadający Twoim potrzebom</p> 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> <div className="h-[150px] sm:h-[250px] md:h-[300px] w-0.5 bg-gray-300"></div>
<div className={` ${styles.flexStart} ${styles.paddingX} gap-16 mt-20`}> <div
{/* Przykładowe divy jako przyciski wyboru */} className={
formData.posting_option === "P" ? activeStyle : inactiveStyle
<div }
className={formData.postingOption === 'Opcja 1' ? activeStyle : inactiveStyle} onClick={handleDivClick("P")}
onClick={handleDivClick('Opcja 1')} style={{ cursor: "pointer" }}
>
> <p className={`font-poppins text-l sm:text-xl text-center mt-4`}>Premium</p>
<p className={`${styles.paragraph} text-center mt-4`}>Starter</p> <ul className="hidden md:block">
{lorem_ipsum_premium.map((lorem_ipsum) =>
</div> (<li className="text-sm lista-outside mx-8 text-justify list-inside">{lorem_ipsum}</li>)
<div className='h-[400px] w-0.5 bg-gray-300'> )}
</ul>
</div> </div>
<div </div>
className={formData.postingOption === 'Opcja 2' ? activeStyle : inactiveStyle} </div>
onClick={handleDivClick('Opcja 2')} <div className="w-full">
> <button
<p className={`${styles.paragraph} text-center mt-4`}>Standard</p> type="button"
<div className='mt-6'> onClick={nextStep}
className="absolute inset-x-0 md:-left-4 bottom-[4rem] mx-auto h-12 w-72 rounded-xl bg-gray-700
<p className={`${styles.paragraph} text-center`}>najczęsciej wybierany</p> font-poppins font-semibold text-[14px] text-white
hover:scale-125 duration-300"
</div> >
</div> Przejdź do następnego kroku &nbsp;&nbsp;
<span className="text-[18px]"></span>{" "}
<div className='h-[400px] w-0.5 bg-gray-300'> </button>
</div>
</div> </>
<div );
className={formData.postingOption === 'Opcja 3' ? activeStyle : inactiveStyle}
onClick={handleDivClick('Opcja 3')}
style={{ cursor: 'pointer' }}
>
<p className={`${styles.paragraph} text-center mt-4`}>Premium</p>
</div>
</div>
<div className='grid pt-32 items-center justify-center'>
<button
type="button"
onClick={nextStep}
className='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 &nbsp;&nbsp;<span className='text-[18px]'></span> </button>
<div className='h-12 w-full'></div>
</div>
</form>
);
}; };
export default StepOneJoblisting StepOneJoblisting.propTypes = {
nextStep: propTypes.func,
handleChange: propTypes.func,
formData: propTypes.object,
};
export default StepOneJoblisting;

View File

@@ -1,9 +1,14 @@
import React from 'react';
import DOMPurify from 'dompurify';
import styles from '../style';
import TextDivider from './TextDivider';
const StepThreeJoblisting = ({ formData, prevStep }) => { 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 // Funkcja do przetwarzania HTML i dodawania klas
const processHTML = (htmlString) => { const processHTML = (htmlString) => {
const parser = new DOMParser(); const parser = new DOMParser();
@@ -36,53 +41,74 @@ const StepThreeJoblisting = ({ formData, prevStep }) => {
return ( return (
<div className='text-center'> <div className='text-center'>
<p className='mt-8 text-[24px] font-poppins font-bold '> <p className='mt-8 text-[24px] font-poppins font-bold '>
Wybrałeś opcję {formData.postingOption} Wybrałeś opcję {formData.posting_option}
</p> </p>
<TextDivider text={`Tak będzie wyglądało Twoje ogłoszenie na liście ogłoszeń`} /> <TextDivider text={`Tak będzie wyglądało Twoje ogłoszenie na liście ogłoszeń`} />
<div className={`grid grid-cols-4`}> <div className={`grid grid-cols-4`}>
<div <ListingSmall
className={`job-listing col-start-2 col-end-4 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 bg-gray-200`} id={9999}
> name={'Starszy Inżynier ds. ciągłości produkcji'}
<p className='col-span-3 text-xl text-left tracking-wide'>F.H.U. Januszex</p> company_name={'F.H.U. Januszex'}
<p className='place-self-end text-[16px] font-semibold tracking-widest text-slate-800'>12000 PLN - 15000 PLN</p> min_salary={'12000'}
<p className='col-span-4 text-xl text-left tracking-wide'>Starszy Inżynier ds. ciągłości produkcji</p> max_salary={'15000 PLN'}
</div> image={IzaacLOGO}
<div
className={`col-start-2 col-end-4 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 bg-gray-200`} require_salary={true}
> index={0}
<p className='col-span-3 text-base text-left tracking-wide'>{cleanAndProcessData(formData.company_name)}</p> onClick={() => {}}
{ _class={`col-start-2 col-end-4 job-listing`}
formData.requireSalary && (<p className='place-self-end text-[16px] font-semibold tracking-widest text-slate-800'>{cleanAndProcessData(formData.minSalary)} PLN - {cleanAndProcessData(formData.maxSalary)} PLN</p> ) />
} <ListingSmall
{ id={999999}
!formData.requireSalary && (<p className='place-self-end text-xl text-slate-800'></p> ) name={formData.name}
} company_name={formData.company_name}
<p className='col-span-4 text-xl text-left'>{cleanAndProcessData(formData.name)}</p> min_salary={formData.min_salary}
</div> max_salary={formData.max_salary}
<div requiresalary={formData.require_salary}
className={`job-listing2 col-start-2 col-end-4 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 bg-gray-200`} image={formData.image}
> index={0}
<p className='col-span-3 text-xl text-left tracking-wide'>Papieżeks Sp. z o. o.</p> onClick={() => {}}
<p className='place-self-end text-[16px] font-semibold tracking-widest text-slate-800'>9000 PLN - 12000 PLN</p> _class={`col-start-2 col-end-4`}/>
<p className='col-span-4 text-xl text-left tracking-wide'>Konstruktor - inżynier mechanik</p> <ListingSmall
</div> 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> </div>
<TextDivider text={`Tak będzie się prezentowała treść Twojego ogłoszenia`} /> <TextDivider text={`Tak będzie się prezentowała treść Twojego ogłoszenia`} />
<div className='grid grid-cols-5 mt-4 mb-4'> <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'> <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)} {renderContent(formData.content)}
</div> </div>
</div> </div>
<button <button
type="button" type="button"
onClick={prevStep} 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'> 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>&nbsp;&nbsp;Przejdź do poprzedniego kroku <span className='text-[18px]'></span>&nbsp;&nbsp;Przejdź do poprzedniego kroku
</button> </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!&nbsp;&nbsp;<span className='text-[18px]'></span>
</button>
</div> </div>
); );
} }
StepThreeJoblisting.propTypes = {
formData: PropTypes.object,
prevStep: PropTypes.func,
skills: PropTypes.array,
handleSubmit: PropTypes.func
}
export default StepThreeJoblisting; export default StepThreeJoblisting;

View File

@@ -1,18 +1,39 @@
import React, { useState } from 'react' 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 styles from '../style'
import { CKEditor } from '@ckeditor/ckeditor5-react'; import { CKEditor } from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import ImageUpload from './ImageUpload'; import ImageUpload from './ImageUpload';
import SkillsSelector from './SkillsSelector'; import SkillsSelector from './SkillsSelector';
import Salary from './Salary'; import Salary from './Salary';
import { placeholderImage } from '../assets';
const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setFormData, removeFields }) => { import Button from './Button';
const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setFormData, removeFields, skills }) => {
const [editorData, setEditorData] = useState(''); const [editorData, setEditorData] = useState('');
const handleEditorChange = (event, editor) => { const [imageSrc, setImageSrc] = useState(placeholderImage);
const data = editor.getData(); const max_words = 500;
setEditorData(data); 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 = () => { const handleNextStep = () => {
if (validateForm()) { if (validateForm()) {
console.log(errors) console.log(errors)
@@ -25,10 +46,12 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const validateForm = () => { const validateForm = () => {
let newErrors = {}; let newErrors = {};
if (countWords(formData.content) > max_words) {
newErrors.content = 'Maksymalna liczba słów wynosi 600';
}
// Sprawdź każde wymagane pole // Sprawdź każde wymagane pole
if (!formData.jobtitle) { if (!formData.name) {
newErrors.jobtitle = 'To pole jest wymagane'; newErrors.name = 'To pole jest wymagane';
} }
if (!formData.company_name) { if (!formData.company_name) {
newErrors.company_name = 'To pole jest wymagane'; newErrors.company_name = 'To pole jest wymagane';
@@ -39,29 +62,29 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
if (!formData.content) { if (!formData.content) {
newErrors.content = 'To pole jest wymagane'; newErrors.content = 'To pole jest wymagane';
} }
console.log(formData.requireSalary) console.log(formData.require_salary)
if (formData.requireSalary === true) if (formData.require_salary === true)
{ {
console.log(formData.requireSalary) console.log(formData.require_salary)
if (!formData.minSalary) { if (!formData.min_salary) {
newErrors.minSalary = 'To pole jest wymagane'; newErrors.min_salary = 'To pole jest wymagane';
} }
if (!formData.maxSalary) { if (!formData.max_salary) {
newErrors.maxSalary = 'To pole jest wymagane'; newErrors.max_salary = 'To pole jest wymagane';
} }
} }
if (!formData.employmentType) { if (!formData.work_from_home) {
newErrors.employmentType = 'To pole jest wymagane'; newErrors.work_from_home = 'To pole jest wymagane';
} }
if (!formData.employmentType) { if (!formData.employment_type) {
newErrors.employmentType = 'To pole jest wymagane'; newErrors.employment_type = 'To pole jest wymagane';
} }
if (!formData.image) { if (!formData.image) {
newErrors.image = 'To pole jest wymagane'; newErrors.image = 'To pole jest wymagane';
} }
if (Object.keys(formData.skillLevels).length === 0) { if (Object.keys(formData.skill_levels).length === 0) {
newErrors.skillLevels = 'To pole jest wymagane'; newErrors.skill_levels = 'To pole jest wymagane';
} }
setErrors(newErrors); setErrors(newErrors);
@@ -82,8 +105,9 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
<div className='mx-auto max-w-none h-[70vh] w-[60vw]'> <div className='mx-auto max-w-none h-[70vh] w-[60vw]'>
<form action=""> <form action="">
<div className='grid mb-4'> <div className='grid mb-4'>
<div className='grid grid-cols-3 items-center'> <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> <div className={`${styles.paragraph} px-4 py-2`}>Nazwa ogłoszenia
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
<input type="text" <input type="text"
name="name" name="name"
value={formData['name'] || ''} value={formData['name'] || ''}
@@ -91,7 +115,8 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
id="name" id="name"
onChange={handleChange('name')} onChange={handleChange('name')}
placeholder='Wpisz nazwę stanowiska...' placeholder='Wpisz nazwę stanowiska...'
className={`border-b-2 px-4 my-2 ${styles.paragraph} col-span-2 ${errors.jobtitle ? errorStyleInput : ''}`}/> 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> <div className={`${styles.paragraph} px-4 py-2 `}>Strona internetowa</div>
<input type="text" <input type="text"
@@ -101,7 +126,8 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
id="webpage" id="webpage"
placeholder='Wpisz adres strony internetowej...' placeholder='Wpisz adres strony internetowej...'
className={`border-b-2 px-4 my-2 ${styles.paragraph} col-span-2`}/> 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> <div className={`${styles.paragraph} px-4 py-2 `}>Adres siedziby
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
<input type="text" <input type="text"
name="localization" name="localization"
required required
@@ -110,20 +136,35 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
id="localization" id="localization"
placeholder='Wpisz adres siedziby...' placeholder='Wpisz adres siedziby...'
className={`border-b-2 px-4 my-2 ${styles.paragraph} col-span-2`}/> 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={`${styles.paragraph} px-4 py-2 `}>Logo firmy
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
<div className='mt-6 col-span-2'> <div className='mt-6 col-span-2'>
<ImageUpload <ImageUpload
setFormData={setFormData} setFormData={setFormData}
data={formData}
_setImageSrc={setImageSrc}
_imageSrc={imageSrc}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className='w-full border-b-2'></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> <div className={`${styles.paragraph} px-4 py-2`}>Treść ogłoszenia
<span className={`${styles.paragraph} text-red-700`}>*</span></div>
<CKEditor <CKEditor
editor={ClassicEditor} editor={ClassicEditor}
data={formData['content'] || ''} data={formData['content'] || ''}
required required
// config={{
// plugins: [WordCount],
// toolbar: ['wordCount'],
// wordCount: {
// onUpdate: stats => {
// console.log(stats.words, stats.characters)
// }
// }
// }}
onChange={(event, editor) => { onChange={(event, editor) => {
const data = editor.getData(); const data = editor.getData();
handleChange('content')(data); // Wywołanie zmodyfikowanej funkcji handleChange handleChange('content')(data); // Wywołanie zmodyfikowanej funkcji handleChange
@@ -145,11 +186,46 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
<SkillsSelector <SkillsSelector
formData={formData} formData={formData}
setFormData={setFormData} setFormData={setFormData}
skills={skills}
/> />
</div> </div>
</div> </div>
<div className='w-full border-b-2'></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={`${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=''> <div className=''>
<Salary <Salary
removeFields={removeFields} removeFields={removeFields}
@@ -158,15 +234,22 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
formData={formData}/> formData={formData}/>
</div> </div>
<div className={`${styles.flexCenter} p-20 gap-20`}> <div className={`${styles.flexCenter} py-10 gap-20`}>
<button <button
type="button" type="button"
onClick={prevStep} onClick={prevStep}
className='h-12 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>&nbsp;&nbsp;Przejdź do poprzedniego kroku</button> className='h-16 md:h-12 w-80 md:w-72 rounded-xl bg-gray-700
<button font-poppins font-semibold text-[14px] scale-100
type="button" text-white hover:scale-125 duration-300'>
onClick={handleNextStep} <span className='text-[18px]'></span>&nbsp;&nbsp;
className='h-12 w-72 rounded-xl bg-gray-700 font-poppins font-semibold text-[14px] scale-100 text-white hover:scale-125 duration-300'>Przejdź do następnego kroku &nbsp;&nbsp;<span className='text-[18px]'></span> </button> Przejdź do poprzedniego kroku</button>
<Button
isDiasbled={validateForm2()}
nextStep={handleNextStep}
Text="Przejdź do następnego kroku"/>
</div> </div>
</form> </form>
</div> </div>
@@ -175,4 +258,14 @@ const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setForm
) )
} }
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 export default StepTwoJoblisting

View 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

View File

@@ -1,10 +1,11 @@
import React from 'react'
const TextDivider = ({text }) => { import propTypes from 'prop-types'
const TextDivider = ({ text }) => {
return ( return (
<div className='flex items-center justify-center mt-4 mb-6'> <div className='flex items-center justify-center mt-4 mb-6'>
<div className='h-0.5 bg-black w-[14rem] opacity-30'></div> <div className='h-0.5 bg-black w-[14rem] opacity-30'></div>
<p className='text-[24px] mx-8 font-semibold'> <p className='text-[24px] mx-8 font-semibold text-center'>
{text} {text}
</p> </p>
<div className='h-0.5 bg-black w-[14rem] opacity-30'></div> <div className='h-0.5 bg-black w-[14rem] opacity-30'></div>
@@ -13,4 +14,7 @@ const TextDivider = ({text }) => {
) )
} }
TextDivider.propTypes = {
text: propTypes.string
}
export default TextDivider export default TextDivider

View File

@@ -1,96 +1,238 @@
import React from 'react' import React from "react";
import { ogloszenia } from '../consts'
import { useState } 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) { function renderCircles(level) {
let numberOfFilledCircles; let numberOfFilledCircles;
switch (level) { switch (level) {
case 'Podstawowy': case "Podstawowy":
numberOfFilledCircles = 1; numberOfFilledCircles = 1;
break; break;
case 'Zaawansowany': case "Zaawansowany":
numberOfFilledCircles = 3; numberOfFilledCircles = 3;
break; break;
case 'Ekspert': case "Ekspert":
numberOfFilledCircles = 5; numberOfFilledCircles = 5;
break; break;
default: default:
numberOfFilledCircles = 0; numberOfFilledCircles = 0;
} }
return (
<>
{[...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'}`}
/>
))}
</>
);
};
const WorkApp = () => {
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);
};
return ( return (
<section className=' mx-auto bg-white w-full'> <>
{isDetailsVisible && ( {[...Array(5)].map((_, index) => (
<div className='grid grid-cols-6'> <div
<button key={index}
onClick={handleBackToList} className={`inline-block w-3.5 h-3.5 mr-1 rounded-full ${
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" index < numberOfFilledCircles
> ? "bg-red-300 hover:bg-red-700"
Powrót do listy : "bg-gray-500 hover:bg-gray-600"
</button> }`}
</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>
</div>
</div>
</section>
)
} }
export default WorkApp 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]);
useEffect(() => {
getOgloszenia();
getSkills();
}, []);
useEffect(() => {
document.title = `Izaac - ${selectedOgloszenie?.name}`;
}, [selectedOgloszenie?.name]);
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 [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={`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">
<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"
>
Powrót do listy
</button>
</div>
)}
{selectedOgloszenie && (
<JobOfferContent id={id} skills={skills} />
)}
</div>
</div>
</section>
);
};
export default WorkApp;
export { renderCircles };

View File

@@ -13,7 +13,8 @@ export const ogloszenia = [
"Coś innego": "Zaawansowany", "Coś innego": "Zaawansowany",
"Papieżworks": "Podstawowy", "Papieżworks": "Podstawowy",
"Budowa papieży": "Ekspert", "Budowa papieży": "Ekspert",
} },
"requiresalary": true
}, },
{ {
"id": "2", "id": "2",
@@ -26,7 +27,8 @@ export const ogloszenia = [
"Elektronika": "Ekspert", "Elektronika": "Ekspert",
"PLC": "Zaawansowany", "PLC": "Zaawansowany",
"Automatyka przemysłowa": "Podstawowy" "Automatyka przemysłowa": "Podstawowy"
} },
"requiresalary": false
}, },
{ {
"id": "3", "id": "3",
@@ -39,7 +41,8 @@ export const ogloszenia = [
"AutoCAD": "Ekspert", "AutoCAD": "Ekspert",
"Zarządzanie projektem": "Zaawansowany", "Zarządzanie projektem": "Zaawansowany",
"Nadzór budowlany": "Zaawansowany" "Nadzór budowlany": "Zaawansowany"
} },
"requiresalary": true
}, },
{ {
"id": "4", "id": "4",
@@ -52,7 +55,8 @@ export const ogloszenia = [
"AutoCAD": "Zaawansowany", "AutoCAD": "Zaawansowany",
"SolidWorks": "Podstawowy", "SolidWorks": "Podstawowy",
"Budowa statków": "Ekspert" "Budowa statków": "Ekspert"
} },
"requiresalary": true
}, },
{ {
"id": "5", "id": "5",
@@ -65,7 +69,8 @@ export const ogloszenia = [
"Elektronika": "Ekspert", "Elektronika": "Ekspert",
" PLC": "Zaawansowany", " PLC": "Zaawansowany",
"Automatyka przemysłowa": "Podstawowy" "Automatyka przemysłowa": "Podstawowy"
} },
"requiresalary": true
}, },
{ {
"id": "6", "id": "6",
@@ -78,7 +83,8 @@ export const ogloszenia = [
"AutoCAD": "Ekspert", "AutoCAD": "Ekspert",
"Zarządzanie projektem": "Zaawansowany", "Zarządzanie projektem": "Zaawansowany",
"Nadzór budowlany": "Zaawansowany" "Nadzór budowlany": "Zaawansowany"
} },
"requiresalary": true
}, },
{ {
"id": "7", "id": "7",
@@ -91,7 +97,8 @@ export const ogloszenia = [
"AutoCAD": "Zaawansowany", "AutoCAD": "Zaawansowany",
"SolidWorks": "Podstawowy", "SolidWorks": "Podstawowy",
"Budowa statków": "Ekspert" "Budowa statków": "Ekspert"
} },
"requiresalary": true
}, },
{ {
"id": "8", "id": "8",
@@ -102,10 +109,12 @@ export const ogloszenia = [
"lokalizacja": "Gdańsk, Poland", "lokalizacja": "Gdańsk, Poland",
"neededSkills": { "neededSkills": {
"Elektronika": "Ekspert", "Elektronika": "Ekspert",
" PLC": "Zaawansowany", "PLC": "Zaawansowany",
"Automatyka przemysłowa": "Podstawowy" "Automatyka przemysłowa": "Podstawowy"
} },
}, "requiresalary": true
}
,
{ {
"id": "9", "id": "9",
"company_name": "Kompania S. A", "company_name": "Kompania S. A",
@@ -117,7 +126,8 @@ export const ogloszenia = [
"AutoCAD": "Ekspert", "AutoCAD": "Ekspert",
"Zarządzanie projektem": "Zaawansowany", "Zarządzanie projektem": "Zaawansowany",
"Nadzór budowlany": "Zaawansowany" "Nadzór budowlany": "Zaawansowany"
} },
"requiresalary": true
}, },
{ {
"id": "10", "id": "10",
@@ -130,7 +140,8 @@ export const ogloszenia = [
"AutoCAD": "Zaawansowany", "AutoCAD": "Zaawansowany",
"SolidWorks": "Podstawowy", "SolidWorks": "Podstawowy",
"Budowa statków": "Ekspert" "Budowa statków": "Ekspert"
} },
"requiresalary": true
}, },
{ {
"id": "11", "id": "11",
@@ -141,66 +152,15 @@ export const ogloszenia = [
"lokalizacja": "Gdańsk, Poland", "lokalizacja": "Gdańsk, Poland",
"neededSkills": { "neededSkills": {
"Elektronika": "Ekspert", "Elektronika": "Ekspert",
" PLC": "Zaawansowany", "PLC": "Zaawansowany",
"Automatyka przemysłowa": "Podstawowy" "Automatyka przemysłowa": "Podstawowy"
} },
}, "requiresalary": true
{ }
"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"
}
},
{
"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 = [ export const linki = [
{ {
"id": "1", "id": "1",
@@ -209,12 +169,12 @@ export const linki = [
}, },
{ {
"id": "2", "id": "2",
"name": "work/postings", "name": "work/joboffers",
"title": "Ogłoszenia", "title": "Ogłoszenia",
}, },
{ {
"id": "3", "id": "3",
"name": "work/jobposting", "name": "work/addjoboffer",
"title": "Dodaj ogłoszenie", "title": "Dodaj ogłoszenie",
}, },
{ {
@@ -251,3 +211,150 @@ export const linki_home = [
"title": "Kontakt", "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. ",
]

View File

@@ -22,9 +22,38 @@
} }
} }
/* @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 components;
@tailwind utilities; @tailwind utilities;
#root {
margin: 0;
padding: 0;
height: 100vh;
overflow: auto;
}
:root { :root {
--black-gradient: linear-gradient( --black-gradient: linear-gradient(
@@ -38,6 +67,61 @@
* { * {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.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 { .editor-container {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@@ -70,6 +154,10 @@
list-style: inside; list-style: inside;
} }
.lista-outside {
list-style: outside;
}
.sidebar-show { .sidebar-show {
-webkit-animation: slide-top 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; -webkit-animation: slide-top 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
animation: slide-top 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation: slide-top 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
@@ -80,57 +168,146 @@
animation: slide-down 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation: slide-down 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
} }
@-webkit-keyframes slide-top { .slider-wrapper {
0% { display: flex;
-webkit-transform: translateX(600px); justify-content: center;
transform: translateX(600px); align-items: center;
} width: 100%;
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
} }
@keyframes slide-top { .slider {
0% { position: relative;
-webkit-transform: translateX(600px); width: 100%;
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);
}
} }
@media (max-width: 768px) { .slider__track,
.details-section { .slider__range,
display: none; .slider__left-value,
} .slider__right-value {
.list-section { position: absolute;
display: block; }
}
.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;
} }

View File

@@ -7,7 +7,7 @@ module.exports = {
colors: { colors: {
primary: "#00040f", primary: "#00040f",
secondary: "#00f6ff", secondary: "#00f6ff",
dimWhite: "rgba(255, 255, 255, 0.7)", dimWhite: "rgba(255, 255, 255, 0.8)",
dimBlue: "rgba(9, 151, 124, 0.1)", dimBlue: "rgba(9, 151, 124, 0.1)",
}, },
fontFamily: { fontFamily: {

View File

@@ -1,7 +1,32 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react' 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({ export default defineConfig({
plugins: [react()], 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==');
});
},
},
},
},
});