dodanie nowych komponentow oraz dodanie walidacji propTypes
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
10103fb1a1
commit
256bd8a0ee
@ -7,7 +7,7 @@
|
||||
<link rel="icon" href="../icon/favicon.ico"/>
|
||||
<!-- todo ICONs -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>izaac frontend</title>
|
||||
<title>Izaac - praca dla inżynierów</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
27
src/App.jsx
27
src/App.jsx
@ -7,38 +7,29 @@ import { Route, Routes } from "react-router-dom";
|
||||
import Contact from "./components/Contact";
|
||||
import Mininav from "./components/Mininav";
|
||||
import AddJobListing from "./components/AddJobListing";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import EmployerPanel from "./components/EmployerPanel";
|
||||
|
||||
function App() {
|
||||
|
||||
const loc = () => {
|
||||
if (location.pathname.startsWith("work/jobposting")) {
|
||||
return "";
|
||||
} else {
|
||||
return "overflow-hidden";
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Router>
|
||||
<div className={`bg-white w-full ${loc}`}>
|
||||
<div className={`${styles.paddingX} ${styles.flexCenter} border-b-2`}>
|
||||
<Mininav />
|
||||
<div className={`${styles.boxWidth}`}>
|
||||
<div className={`bg-white w-full`}>
|
||||
<div className={`${styles.paddingX} ${styles.flexCenter} border-b-2 relative`}>
|
||||
{/* <Mininav /> */}
|
||||
<div className={`${styles.boxWidth} mx-auto`}>
|
||||
<NavBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.flexStart}`}>
|
||||
<div className={`${styles.flexStart} mt-18 sm:mt-0`}>
|
||||
<div className={`${styles.boxWidth2} `}>
|
||||
<Routes>
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/work" element={<WorkApp />} />
|
||||
<Route path="/work/postings" element={<WorkApp />} />
|
||||
<Route path="/work/joboffers" element={<WorkApp />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/work/jobposting" element={<AddJobListing />} />
|
||||
<Route path="/employerpanel" element={<EmployerPanel />} />
|
||||
<Route path="/work/addjoboffer" element={<AddJobListing />} />
|
||||
<Route path="/work/joboffers/:id" element={<WorkApp/>} />
|
||||
{/* <Route path="/employerpanel" element={<EmployerPanel />} /> */}
|
||||
{/* Add more routes as needed */}
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import StepOneJoblisting from './StepOneJoblisting';
|
||||
import StepTwoJoblisting from './StepTwoJoblisting';
|
||||
import StepThreeJoblisting from './StepThreeJoblisting';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const Category = (props) => {
|
||||
return (
|
||||
@ -15,4 +15,11 @@ const Category = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
Category.propTypes = {
|
||||
name: propTypes.string,
|
||||
func: propTypes.func,
|
||||
small: propTypes.bool,
|
||||
last: propTypes.bool
|
||||
}
|
||||
|
||||
export default Category
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
import { ogloszenia } from "../consts";
|
||||
import ListingSmall from "./ListingSmall";
|
||||
import { useState } from "react";
|
||||
|
||||
@ -1,30 +1,128 @@
|
||||
import React from 'react'
|
||||
|
||||
import Search from './Search'
|
||||
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { categories, work_from_home, employment_types } from '../consts';
|
||||
import propTypes from 'prop-types';
|
||||
import Selector from './Selector';
|
||||
const Filter = (props) => {
|
||||
if (!props.isOpen) return null;
|
||||
return (
|
||||
<div className={`sticky top-24 z-20 bg-gray-100 flex flex-wrap place-items-center items-center slide-container py-2 ${!props.isOpen ? '' : 'expanded'}`}>
|
||||
<Search label='Wyszukaj ogłoszenie'
|
||||
placeholder='Wpisz nazwę stanowiska...' />
|
||||
<Search label='Lokalizacja'
|
||||
placeholder='Wpisz lokalizację...' />
|
||||
<Search label='Min. wynagrodzenie'
|
||||
placeholder='Wpisz kwotę minimalną...' />
|
||||
<Search label='Max. wynagrodzenie'
|
||||
placeholder='Wpisz kwotę maksymalną...' />
|
||||
useEffect(() => {
|
||||
console.log(props.searchQuery)
|
||||
console.log(selectedCategories)
|
||||
console.log(selectedWorkFromHome)
|
||||
console.log(selectedEmploymentTypes)
|
||||
console.log(valueminSalary)
|
||||
}, [props.searchQuery]);
|
||||
|
||||
<button className='bg-gray-600 text-white py-3 px-12 col-span-2
|
||||
mx-auto rounded-md font-semibold text-xl hover:bg-gray-500 duration-150
|
||||
mt-3
|
||||
'
|
||||
onClick={props.onClicked}
|
||||
const [selectedCategories, setSelectedCategories] = useState([]);
|
||||
const [selectedWorkFromHome, setSelectedWorkFromHome] = useState([]);
|
||||
const [selectedEmploymentTypes, setSelectedEmploymentTypes] = useState([]);
|
||||
const [valueminSalary, setValueminSalary] = useState('');
|
||||
const [valuemaxSalary, setValuemaxSalary] = useState('');
|
||||
const [valueName, setValueName] = useState('');
|
||||
const [valueLocalization, setValueLocalization] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCategories([])
|
||||
setSelectedWorkFromHome([])
|
||||
setSelectedEmploymentTypes([])
|
||||
setValueminSalary('')
|
||||
setValuemaxSalary('')
|
||||
setValueName('')
|
||||
setValueLocalization('')
|
||||
}, [props.clearSearchQuery])
|
||||
|
||||
const handle_checked_change = (e) => {
|
||||
switch (e.target.name) {
|
||||
case 'categories':
|
||||
if (e.target.checked) {
|
||||
setSelectedCategories([...selectedCategories, e.target.value]);
|
||||
} else {
|
||||
setSelectedCategories(selectedCategories.filter(category => category !== e.target.value));
|
||||
}
|
||||
props.setSearchQuery(prevState =>
|
||||
({...prevState,
|
||||
categories: e.target.checked ? [...(prevState.categories || []), e.target.value] : (prevState.categories || []).filter(category => category !== e.target.value)}));
|
||||
break;
|
||||
case 'work_from_home':
|
||||
if (e.target.checked) {
|
||||
setSelectedWorkFromHome([...selectedWorkFromHome, e.target.value]);
|
||||
} else {
|
||||
setSelectedWorkFromHome(selectedCategories.filter(work_from_home => work_from_home !== e.target.value));
|
||||
}
|
||||
props.setSearchQuery(prevState =>
|
||||
({...prevState,
|
||||
work_from_home: e.target.checked ? [...(prevState.work_from_home || []), e.target.value] : (prevState.work_from_home || []).filter(work_from_home => work_from_home !== e.target.value)}));
|
||||
break;
|
||||
case 'employment':
|
||||
if (e.target.checked) {
|
||||
setSelectedEmploymentTypes([...selectedEmploymentTypes, e.target.value]);
|
||||
} else {
|
||||
setSelectedEmploymentTypes(selectedCategories.filter(employment => employment !== e.target.value));
|
||||
}
|
||||
props.setSearchQuery(prevState =>
|
||||
({...prevState,
|
||||
employment: e.target.checked ? [...(prevState.employment || []), e.target.value] : (prevState.employment || []).filter(employment => employment !== e.target.value)}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
// idea - make the page whole page grayed out and only the filter is visible
|
||||
<div className={`z-10 bg-dimWhite collapsible px-3 sm:px-4 ${!props.isOpen ? '' : 'expanded border-4'}`}>
|
||||
<div className='grid grid-cols-2 sm:grid-cols-3 mb-2'>
|
||||
<div className='hidden sm:block'></div>
|
||||
<p className='mx-auto mt-3 text-center font-poppins font-semibold text-xl text-gray-600'>Filtry</p>
|
||||
|
||||
<button className='rounded-xl mt-4 ph-6 w-32 bg-slate-400 hover:bg-slate-600 duration-100 ' onClick={props.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'
|
||||
setSearchQuery={props.setSearchQuery}
|
||||
_type='text'
|
||||
value={props.searchQuery.name || ''}/>
|
||||
<Search label='Lokalizacja'
|
||||
placeholder='Wpisz lokalizację...'
|
||||
name='localization'
|
||||
_type='text'
|
||||
setSearchQuery={props.setSearchQuery}
|
||||
value={props.searchQuery.localization || ''} />
|
||||
<Search label='Min. wynagrodzenie'
|
||||
placeholder='Wpisz kwotę minimalną...'
|
||||
name='min_salary'
|
||||
_type='text'
|
||||
setSearchQuery={props.setSearchQuery}
|
||||
value={props.searchQuery.min_salary || '' } />
|
||||
<Search label='Max. wynagrodzenie'
|
||||
placeholder='Wpisz kwotę maksymalną...'
|
||||
name='max_salary'
|
||||
setSearchQuery={props.setSearchQuery}
|
||||
_type='text'
|
||||
value={props.searchQuery.max_salary || ''}/>
|
||||
<Selector value_to_map_from={categories} name='Kategorie' inputname='categories' onChange={handle_checked_change} state={selectedCategories}/>
|
||||
<Selector value_to_map_from={work_from_home} s name='Praca zdalna' inputname='work_from_home' state={selectedWorkFromHome} onChange={handle_checked_change}/>
|
||||
<Selector value_to_map_from={employment_types} name='Typ kontraktu' inputname='employment' state={selectedEmploymentTypes} onChange={handle_checked_change} />
|
||||
|
||||
<button className='bg-gray-600 text-white py-1 sm:py-3 px-12 col-span-2 mx-auto rounded-md font-semibold text-xl hover:bg-gray-500 duration-150 my-3 sm:my-5 sm:min-h-full'
|
||||
onClick={props.onClick}
|
||||
>
|
||||
Filtruj
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export default Filter
|
||||
Filter.propTypes = {
|
||||
isOpen: propTypes.bool,
|
||||
searchQuery: propTypes.object,
|
||||
setSearchQuery: propTypes.func,
|
||||
categories: propTypes.array,
|
||||
clearSearchQuery: propTypes.func,
|
||||
onClick: propTypes.func
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from '../style'
|
||||
const Home = () => {
|
||||
return (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { placeholderImage } from '../assets';
|
||||
import axios from 'axios';
|
||||
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const ImageUpload = ({setFormData, data}) => {
|
||||
const [imageSrc, setImageSrc] = useState(placeholderImage);
|
||||
@ -83,4 +83,9 @@ const ImageUpload = ({setFormData, data}) => {
|
||||
);
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
setFormData: propTypes.func,
|
||||
data: propTypes.object
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
|
||||
@ -9,7 +9,7 @@ const JobOfferContent = ({ id, skills }) => {
|
||||
|
||||
const fetchJobOfferById = async (jobId) => {
|
||||
try {
|
||||
const response = await axios.get(`https://izaac.knck.pl/api/jobposting/joboffers/${jobId}`);
|
||||
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);
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import { IzaacLOGO } from '../assets'
|
||||
|
||||
const ListingSmall = ({ id, name, company_name, min_salary, max_salary, require_salary, index, onClick, _class, image }) => {
|
||||
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 items-center gap-4 drop-shadow cursor-pointer mb-2 sm:mr-4 mr-0 text-xl font-bold border rounded-[10px] p-2 sm: px-4 hover:border-l-8 hover:border-zinc-500 duration-300 ${index % 2 === 0 ? 'bg-gray-200' : 'bg-gray-300'} ${_class}`}
|
||||
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'>
|
||||
@ -14,7 +15,7 @@ const ListingSmall = ({ id, name, company_name, min_salary, max_salary, require_
|
||||
</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-sm text-left text-gray-600'>{company_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>
|
||||
@ -24,4 +25,19 @@ const ListingSmall = ({ id, name, company_name, min_salary, max_salary, require_
|
||||
);
|
||||
};
|
||||
|
||||
export default ListingSmall;
|
||||
export default ListingSmall;
|
||||
|
||||
ListingSmall.propTypes = {
|
||||
id: propTypes.number,
|
||||
name: propTypes.string,
|
||||
company_name: propTypes.string,
|
||||
min_salary: propTypes.number,
|
||||
max_salary: propTypes.number,
|
||||
require_salary: propTypes.bool,
|
||||
index: propTypes.number,
|
||||
onClick: propTypes.func,
|
||||
_class: propTypes.string,
|
||||
image: propTypes.string,
|
||||
selected: propTypes.bool,
|
||||
blur: propTypes.string
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import propTypes from 'prop-types';
|
||||
import styles from '../style';
|
||||
|
||||
const Login = ({isOpen, onClose}) => {
|
||||
@ -31,4 +31,9 @@ const Login = ({isOpen, onClose}) => {
|
||||
)
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
isOpen: propTypes.bool,
|
||||
onClose: propTypes.func
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Register from './Register';
|
||||
import Login from './Login';
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
|
||||
|
||||
const Mininav = () => {
|
||||
const [isPopupOpen, setPopupOpen] = useState(false)
|
||||
@ -17,7 +16,7 @@ const Mininav = () => {
|
||||
<>
|
||||
<div className='bg-gray-500 h-5 absolute w-full top-0 flex justify-end items-center z-50'>
|
||||
{/* Link otwierający modal */}
|
||||
<a href="/employerpanel" className='text-white font-poppins font-semibold text-xs'>Panel pracodawcy</a>
|
||||
{/* <a href="/employerpanel" className='text-white font-poppins font-semibold text-xs'>Panel pracodawcy</a> */}
|
||||
<button onClick={openPopup} className='text-white font-poppins font-semibold text-xs mx-12'>
|
||||
Rejestracja
|
||||
</button>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import { linki, linki_home } from '../consts';
|
||||
import { close, search, menu, IzaacLOGO} from '../assets';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Mininav from './Mininav';
|
||||
|
||||
const NavBar = () => {
|
||||
const[toggle, setToggle] = useState(false)
|
||||
const[toggleSearch, setToggleSearch] = useState(false)
|
||||
@ -20,7 +20,7 @@ const NavBar = () => {
|
||||
const currentLinks = getLinks()
|
||||
|
||||
return (
|
||||
<nav className='w-full flex pt-7 justify-between items-center navbar '>
|
||||
<nav className='fixed top-0 left-0 sm:relative z-30 w-full flex pt-4 justify-between items-center navbar bg-white px-8'>
|
||||
|
||||
<img src={IzaacLOGO} alt="izaac" className='md:w-[200px] w-[120px]'/>
|
||||
|
||||
@ -76,5 +76,4 @@ const NavBar = () => {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default NavBar
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import styles from '../style';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const Register = ({isOpen, onClose}) => {
|
||||
if (!isOpen) return null;
|
||||
@ -44,5 +44,8 @@ const Register = ({isOpen, onClose}) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Register.propTypes = {
|
||||
isOpen: propTypes.bool,
|
||||
onClose: propTypes.func
|
||||
}
|
||||
export default Register
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styles from '../style'
|
||||
import { useState, useEffect } from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
import {employment_types, work_from_home } from '../consts'
|
||||
|
||||
@ -112,5 +112,18 @@ const Salary = ({handleChange, formData, removeFields, setFormData}) => {
|
||||
</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
|
||||
|
||||
|
||||
@ -1,19 +1,61 @@
|
||||
import React from 'react'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const Search = (props) => {
|
||||
return (
|
||||
<div className='grid py-2'>
|
||||
<label className="mx-2 text-l font- text-center font-poppins"
|
||||
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 font- text-center font-poppins no-spin-buttons"
|
||||
htmlFor="search">
|
||||
{props.label}
|
||||
{props.label}
|
||||
<span className='text-sm text-red-700'>
|
||||
{check_errors(props.value, props.name) ? '' : ' *'}
|
||||
</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
placeholder={props.placeholder}
|
||||
className="border-2 border-gray-300
|
||||
<input
|
||||
type={props._type}
|
||||
value={props.value || ''}
|
||||
placeholder={props.placeholder}
|
||||
name={props.name}
|
||||
id={props.name}
|
||||
onChange={(e) => props.setSearchQuery(prevState => ({...prevState, [props.name]: e.target.value}))}
|
||||
className={`border-2
|
||||
bg-white h-8 px-5 pr-16 mx-3 rounded-lg
|
||||
text-sm focus:outline-none" />
|
||||
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
|
||||
}
|
||||
|
||||
export default Search
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const levelMappings = {
|
||||
'N': 'Nice to have',
|
||||
@ -53,4 +53,12 @@ const SelectedSkill = ({ skill_name, skillId ,letter, onLevelChange, removeSkill
|
||||
);
|
||||
};
|
||||
|
||||
SelectedSkill.propTypes = {
|
||||
skill_name: propTypes.string,
|
||||
skillId: propTypes.number,
|
||||
letter: propTypes.string,
|
||||
onLevelChange: propTypes.func,
|
||||
removeSkillFromList: propTypes.func,
|
||||
};
|
||||
|
||||
export default SelectedSkill;
|
||||
|
||||
35
src/components/Selector.jsx
Normal file
35
src/components/Selector.jsx
Normal file
@ -0,0 +1,35 @@
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const Selector = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-2 sm:gap-[1px]'>
|
||||
<span className='col-span-2 mx-auto text-l font- text-center font-poppins'>{props.name}</span>
|
||||
{props.value_to_map_from.map((value_to_map_from) => (
|
||||
<label key={value_to_map_from.id} className
|
||||
='flex items-center justify-start text-sm font-poppins font-semibold text-gray-600'>
|
||||
<input type='checkbox' name={props.inputname} value={value_to_map_from.id}
|
||||
onChange={props.onChange}
|
||||
className='mr-2'
|
||||
checked={props.state.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.array
|
||||
}
|
||||
|
||||
export default Selector
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import SkillRender from './SkillRender';
|
||||
|
||||
|
||||
@ -15,5 +15,9 @@ const SkillsList = ({skillData, skill_names}) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SkillsList.propTypes = {
|
||||
skillData: propTypes.array,
|
||||
skill_names: propTypes.array,
|
||||
};
|
||||
|
||||
export default SkillsList;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
const levelMappings = {
|
||||
'N': 'Nice to have',
|
||||
@ -42,4 +41,10 @@ const SkillRender = ({key, skill, level,}) => {
|
||||
)
|
||||
};
|
||||
|
||||
SkillRender.propTypes = {
|
||||
key: propTypes.number,
|
||||
skill: propTypes.string,
|
||||
level: propTypes.string,
|
||||
};
|
||||
|
||||
export default SkillRender;
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import SelectedSkill from './SelectedSkill';
|
||||
import axios from 'axios';
|
||||
|
||||
const SkillsSelector = ({ formData, setFormData, skills }) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
@ -101,4 +101,10 @@ const SkillsSelector = ({ formData, setFormData, skills }) => {
|
||||
);
|
||||
};
|
||||
|
||||
SkillsSelector.propTypes = {
|
||||
formData: propTypes.object.isRequired,
|
||||
setFormData: propTypes.func.isRequired,
|
||||
skills: propTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default SkillsSelector;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import propTypes from "prop-types";
|
||||
|
||||
import styles from "../style";
|
||||
import TextDivider from "./TextDivider";
|
||||
|
||||
|
||||
const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
|
||||
return (
|
||||
<div className={`${styles.flexCenter}`}>
|
||||
@ -120,4 +123,11 @@ const StepFourJoblisting = ({ handleChange, formData, nextStep, prevStep }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StepFourJoblisting.propTypes = {
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
nextStep: propTypes.func,
|
||||
prevStep: propTypes.func,
|
||||
};
|
||||
|
||||
export default StepFourJoblisting;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "../style";
|
||||
import propTypes from "prop-types";
|
||||
|
||||
const StepOneJoblisting = ({ nextStep, handleChange, formData }) => {
|
||||
// Funkcja do obsługi kliknięcia na div i aktualizacji stanu
|
||||
const handleDivClick = (value) => () => {
|
||||
@ -76,4 +78,10 @@ const StepOneJoblisting = ({ nextStep, handleChange, formData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StepOneJoblisting.propTypes = {
|
||||
nextStep: propTypes.func,
|
||||
handleChange: propTypes.func,
|
||||
formData: propTypes.object,
|
||||
};
|
||||
|
||||
export default StepOneJoblisting;
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import styles from '../style';
|
||||
import TextDivider from './TextDivider';
|
||||
import ListingSmall from './ListingSmall';
|
||||
import { IzaacLOGO } from '../assets'
|
||||
|
||||
import SkillsList from './SkillList';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const StepThreeJoblisting = ({ formData, prevStep, skills, handleSubmit }) => {
|
||||
// Funkcja do przetwarzania HTML i dodawania klas
|
||||
const processHTML = (htmlString) => {
|
||||
@ -103,4 +104,11 @@ const StepThreeJoblisting = ({ formData, prevStep, skills, handleSubmit }) => {
|
||||
);
|
||||
}
|
||||
|
||||
StepThreeJoblisting.propTypes = {
|
||||
formData: PropTypes.object,
|
||||
prevStep: PropTypes.func,
|
||||
skills: PropTypes.array,
|
||||
handleSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default StepThreeJoblisting;
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
import { categories, experience_levels } from '../consts';
|
||||
import styles from '../style'
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
@ -7,8 +10,6 @@ import SkillsSelector from './SkillsSelector';
|
||||
import Salary from './Salary';
|
||||
import { placeholderImage } from '../assets';
|
||||
|
||||
import { categories, experience_levels } from '../consts';
|
||||
|
||||
const StepTwoJoblisting = ({ nextStep, prevStep, handleChange, formData, setFormData, removeFields, skills }) => {
|
||||
const [editorData, setEditorData] = useState('');
|
||||
const [imageSrc, setImageSrc] = useState(placeholderImage);
|
||||
@ -230,4 +231,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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
|
||||
import TextDivider from './TextDivider'
|
||||
|
||||
const Success = () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
const TextDivider = ({text }) => {
|
||||
import propTypes from 'prop-types'
|
||||
|
||||
const TextDivider = ({ text }) => {
|
||||
return (
|
||||
<div className='flex items-center justify-center mt-4 mb-6'>
|
||||
<div className='h-0.5 bg-black w-[14rem] opacity-30'></div>
|
||||
@ -13,4 +14,7 @@ const TextDivider = ({text }) => {
|
||||
)
|
||||
}
|
||||
|
||||
TextDivider.propTypes = {
|
||||
text: propTypes.string
|
||||
}
|
||||
export default TextDivider
|
||||
|
||||
@ -2,13 +2,15 @@ import React from "react";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import ListingSmall from "./ListingSmall";
|
||||
import SkillRender from "./SkillRender";
|
||||
import Search from "./Search";
|
||||
// import SkillRender from "./SkillRender";
|
||||
// import Search from "./Search";
|
||||
import Filter from "./Filter";
|
||||
import Category from "./Category";
|
||||
// import Category from "./Category";
|
||||
import JobOfferContent from "./JobOfferContent";
|
||||
import { categories } from "../consts";
|
||||
import axios from "axios";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
|
||||
function renderCircles(level) {
|
||||
let numberOfFilledCircles;
|
||||
@ -44,11 +46,17 @@ function renderCircles(level) {
|
||||
}
|
||||
|
||||
const WorkApp = () => {
|
||||
let { id } = useParams();
|
||||
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||
// Czy szczegóły są widoczne
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Funkcja pobierająca dane z API
|
||||
// useEffect(() => {
|
||||
// if (!isOpen) {
|
||||
// setSearchQuery({ name: "", localization: "", min_salary: "", max_salary: "" });
|
||||
// }}, [isOpen]);
|
||||
|
||||
const [ogloszenia, setOgloszenia] = useState([]);
|
||||
|
||||
const [skills, setSkills] = useState([]);
|
||||
@ -64,12 +72,15 @@ const WorkApp = () => {
|
||||
// console.log(data);
|
||||
if (data.length > 0) {
|
||||
setSelectedOgloszenie(data[0]);
|
||||
// console.log(data[0])
|
||||
console.log(data[0])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
if (id === undefined) {
|
||||
id = ogloszenia[0]?.id;
|
||||
}
|
||||
|
||||
const getSkills = async () => {
|
||||
try {
|
||||
@ -86,26 +97,27 @@ const WorkApp = () => {
|
||||
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState({});
|
||||
const clearSearchQuery = () => {
|
||||
setSearchQuery({});
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
const prevSlide = () => {
|
||||
setCurrentSlide(
|
||||
(prevSlide) => (prevSlide - 1 + categories.length) % categories.length
|
||||
);
|
||||
};
|
||||
const nextSlide = () => {
|
||||
setCurrentSlide((prevSlide) => (prevSlide + 1) % categories.length);
|
||||
};
|
||||
const [currentSlide, setCurrentSlide] = React.useState(0);
|
||||
|
||||
// Funkcja do powrotu do listy ogłoszeń
|
||||
const handleBackToList = () => {
|
||||
setIsDetailsVisible(false);
|
||||
@ -125,53 +137,50 @@ const WorkApp = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className=" mx-auto bg-white w-full">
|
||||
<div className="flex-grow h-5/6 grid grid-cols-10">
|
||||
<div className="px-3 flex col-span-11 sm:flex-wrap flex-shrink-0 items-center place-content-center">
|
||||
<button onClick={prevSlide} className="lg:hidden mx-6 font-semibold place-self-start self-center leading-5">
|
||||
Poprzednia <br/> kategoria
|
||||
</button>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`lg:block ${
|
||||
index === currentSlide ? "block" : "hidden"
|
||||
} lg:flex flex-grow lg:flex-none flex-basis-0"`}
|
||||
>
|
||||
<Category
|
||||
name={category.name}
|
||||
last={index === categories.length - 1}
|
||||
small={isSmallViewport}
|
||||
func={() => { getOgloszenia({ category: category.id }); }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={nextSlide} className="lg:hidden font-semibold leading-5 place-self-end self-center mr-6">
|
||||
Następna <br/> kategoria
|
||||
</button>
|
||||
</div>
|
||||
<section className={`bg-white w-full`}>
|
||||
<div className="h-[88vh] flex flex-row sm:px-12">
|
||||
|
||||
|
||||
<div
|
||||
className={`static rounded-xl col-span-11 sm:col-span-4 bg-gray-100 overflow-y-auto px-2 my-4 mx-2 sm:h-[84vh] h-[84vh] ${
|
||||
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-0 col-span-11 sm:cols-span-4 h-24 bg-slate-300 flex items-center place-content-center mb-2">
|
||||
<h1 className="text-center text-3xl font-poppins font-semibold text-slate-800 py-4 mx-6">
|
||||
<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-2xl font-poppins font-semibold text-slate-800 py-2 mx-6">
|
||||
Oferty pracy
|
||||
</h1>
|
||||
<button
|
||||
className="block font-poppins text-white font-semibold
|
||||
bg-gray-500 px-4 py-2 mx-6 rounded-md"
|
||||
bg-gray-500 px-4 py-2 mx-6 rounded-md hover:bg-gray-600 duration-100"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Pokaż filtry
|
||||
|
||||
{!isOpen ? 'Pokaż filtry' : 'Ukryj filtry'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div className="z-10 sticky top-24 col-span-11 sm:cols-span-4 bg-slate-300 mb-2">
|
||||
{isOpen && <Filter isOpen={isOpen} />}
|
||||
<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}
|
||||
@ -181,14 +190,18 @@ const WorkApp = () => {
|
||||
require_salary={ogloszenie.require_salary}
|
||||
image={ogloszenie.image}
|
||||
index={index}
|
||||
onClick={() => handleOgloszenieClick(ogloszenie)}
|
||||
onClick={() => {handleOgloszenieClick(ogloszenie)}}
|
||||
selected={selectedOgloszenie?.id === ogloszenie.id}
|
||||
blur={isOpen ? 'blur' : ''}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</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 ${
|
||||
className={`absolute sm:relative w-full sm:w-[60%] bg-gray-100 sm:h-[89vh] h-[100vh] top-16 sm:top-0 -left-4 right-0 overflow-y-auto p-4 mx-4 blur-transition ${
|
||||
isDetailsVisible ? "block" : "hidden sm:block"
|
||||
}`}
|
||||
} noblur ${isOpen ? 'blur' : ''}`}
|
||||
>
|
||||
{isDetailsVisible && (
|
||||
<div className="grid grid-cols-6">
|
||||
@ -202,19 +215,8 @@ const WorkApp = () => {
|
||||
)}
|
||||
|
||||
{selectedOgloszenie && (
|
||||
<JobOfferContent id={selectedOgloszenie.id} skills={skills} />
|
||||
<JobOfferContent id={id} skills={skills} />
|
||||
)}
|
||||
{/* <div className='grid sm:grid-cols-2 md:grid-cols-5 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>
|
||||
</section>
|
||||
|
||||
@ -169,12 +169,12 @@ export const linki = [
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "work/postings",
|
||||
"name": "work/joboffers",
|
||||
"title": "Ogłoszenia",
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "work/jobposting",
|
||||
"name": "work/addjoboffer",
|
||||
"title": "Dodaj ogłoszenie",
|
||||
},
|
||||
{
|
||||
|
||||
116
src/index.css
116
src/index.css
@ -22,6 +22,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* @layer components {
|
||||
.no-spin-buttons::-webkit-inner-spin-button,
|
||||
.no-spin-buttons::-webkit-outer-spin-button {
|
||||
@apply -webkit-appearance-none margin-0;
|
||||
}
|
||||
.no-spin-buttons {
|
||||
@apply -moz-appearance-textfield;
|
||||
}
|
||||
} */
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dla Firefox */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@ -60,11 +81,43 @@
|
||||
}
|
||||
|
||||
/* Expanded state styles */
|
||||
.expanded {
|
||||
max-height: fit-content; /* Adjust as needed */
|
||||
opacity: 1;
|
||||
.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;
|
||||
}
|
||||
@ -109,59 +162,4 @@
|
||||
.sidebar-hide {
|
||||
-webkit-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 {
|
||||
0% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-top {
|
||||
0% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-down {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(600px);
|
||||
transform: translateX(600px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.details-section {
|
||||
display: none;
|
||||
}
|
||||
.list-section {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
colors: {
|
||||
primary: "#00040f",
|
||||
secondary: "#00f6ff",
|
||||
dimWhite: "rgba(255, 255, 255, 0.7)",
|
||||
dimWhite: "rgba(255, 255, 255, 0.8)",
|
||||
dimBlue: "rgba(9, 151, 124, 0.1)",
|
||||
},
|
||||
fontFamily: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user