Merge branch 'develop' into fix/article-interaction-buttons

This commit is contained in:
maximus 2022-10-18 11:07:40 +03:00
commit b5443aa033
19 changed files with 513 additions and 53 deletions

View File

@ -1,7 +1,7 @@
{
"serv": {
"goHome": "Home",
"noSuchPath": "We don't have this page"
"goHome": "Home page",
"noSuchPath": "We don't have such a page"
},
"sidemenu": {
"dashboard": "Dashboard",
@ -19,45 +19,110 @@
"hellousr": "Hello, {{username}}",
"edit": "Edit",
"language": "Language",
"selectLanguage": "Select language",
"selectLanguage": "Select a language",
"save": "Save",
"cancel": "Cancel",
"account": {
"info": "Personal information",
"info": "Personal Information",
"mail": "Mail",
"connect": "Add account",
"connectedAccounts_one": "Connected account",
"connectedAccounts_other": "Connected accounts",
"settings": "Account settings"
"connect": "Add Account",
"connectedAccounts_one": "Linked Account",
"connectedAccounts_other": "Linked Accounts",
"settings": "Account Settings"
},
"security": {
"password": {
"caption": "Password",
"twoFactor": "Two factor authentication (2FA)",
"description": "Keep your account secure by enabling 2FA via SMS or using a temporary one-time passcode (TOTP) from an authenticator app."
"twoFactor": "Two-factor authentication (2FA)",
"description": "Protect your account by enabling 2FA via SMS or using a temporary one-time password (OTP) from the authentication app."
},
"activity": {
"caption": "Device activity"
}
},
"search": {
"label": "Search for something.."
"label": "We will find something.."
},
"subscriptions": {
"subscribed": "Service have been connected"
"subscribed": "The service is attached to the account"
},
"viewHistory": "View history",
"logOutEverywhere": "log out from all devices",
"logOutEverywhere": "Log out from all devices",
"back": "Back",
"logOut": "Log out",
"logOut": "Exit",
"failures": {
"subscriptions": {
"failure": "Failed to connect service",
"exists": "Service have already been connected",
"confirmation": "Invalid confirmation information provided"
"failure": "Failed to attach the service to your account",
"exists": "The service was already attached to your account earlier",
"confirmation": "Invalid password"
},
"services": {
"fork": "Failed to authenticate in service"
"fork": "Failed to perform authorization in the service"
}
},
"articlePage": {
"abstract": "Abstract",
"keywords": "Keywords"
},
"navbar": {
"createNew": "Create an article",
"about": {
"navTitle": "About the project",
"aboutProject": "About Scipaper",
"contacts": "Contacts",
"help": "Help"
},
"library": {
"navTitle": "My library",
"publications": "Publications",
"favorites": "Favorites",
"collections": "Collections",
"recentViewed": "History"
},
"auth": {
"signIn": "Sign In",
"signUp": "Sign Up"
}
},
"footer": {
"accountSettings": "Account Settings",
"about": "About Scipaper",
"help": "Help",
"contactUs": "Contacts",
"allRightsReserved": "All rights reserved",
"termsOfUse": "Terms of Use",
"privacyPolicy": "Privacy Policy",
"coockiesPolicy": "coockies Usage Policy",
"supportedBy": "Created"
},
"mainPage": {
"title": "Scientific Library with free access",
"search": "Search",
"article_one": "Articles",
"article_few": "Articles",
"article_many": "Articles",
"advancedSearch": "Advanced search",
"featuredArticles": {
"title": "Popular articles",
"descriptionPart1": "Choose the one you are interested in",
"descriptionPart2": "Scientific category",
"categories": {
"Medical": "Medical",
"TechnicsAndTechlonogies": "Technics and Technology",
"Fundamental": "Fundamental",
"Humanitarian": "Humanitarian",
"Agricultural": "Agricultural",
"Social": "Social"
}
},
"featuredAuthors": "Popular authors",
"more": "More",
"showAll": "Show all"
},
"searchResults": {
"title": "Search results",
"totalResults":"Total results",
"nothingFound": "Nothing found"
}
}
}

View File

@ -5,6 +5,7 @@
import classNames from "classnames";
import { StyleType } from "core/_variants";
import React from "react";
import {ReactComponent as DeleteIcon} from "./Filters/svg/chest.svg"
/* -------------------------------------------------------------------------- */
/* Component props */
@ -15,7 +16,8 @@ type Props = {
children: React.ReactNode;
className?: string;
iconed?: boolean;
onClick?: () => void;
onClick?: (element: any) => void;
closeOption?: Boolean;
} & Omit<React.MouseEventHandler<HTMLSpanElement>, "">;
/* -------------------------------------------------------------------------- */
@ -27,17 +29,18 @@ function Badge({
children,
onClick,
emphasis = "low",
closeOption = false,
...props
}: Props): JSX.Element {
return (
<>
<span
onClick={onClick}
className={classNames(
"border p-2 rounded-sm text-xs text-center",
"border-none p-2 rounded-md text-xs text-center",
{
"cursor-pointer": onClick,
"border-transparent": emphasis == "low",
"bg-blue-400 text-white": emphasis == "high",
"bg-blue-400 text-white rounded-xs": emphasis == "high",
"border-gray-400 background-gray-200": emphasis == "medium",
},
className
@ -45,7 +48,16 @@ function Badge({
{...props}
>
{children}
{closeOption && (
<button
onClick={onClick}
className=" text-white pr-1 pl-3 py-1"
>
<div><DeleteIcon className="h-[9px] w-[9px]" /></div>
</button>
)}
</span>
</>
);
}
export default Badge;

View File

@ -1,12 +1,12 @@
import React from "react";
import classNames from "classnames";
import { ReactComponent as Checkmark } from "assets/svg/check.svg";
import { ReactComponent as Checkmark } from "../assets/svg/arrow-down.svg";
export type Props = {
/**
* Control the state of checkbox
*/
isChecked: boolean;
isChecked?: boolean;
/**
* An Element next to the checkbox
*/
@ -33,12 +33,8 @@ const Checkbox = ({ children, className, isChecked, ...props }: Props) => {
checked={isChecked}
{...props}
/>
<div //
className=" w-4 h-3 leading-[0] absolute top-1.5 left-1 opacity-0
pointer-events-none focus:pointer-events-auto flex items-center justify-center
fill-white peer-disabled:fill-gray-500 "
>
<Checkmark className="fill-inherit" />
<div className="h-2 w-2 absolute top-0 left-0">
<Checkmark className="h-6 w-6 fill-white hover:fill-white stroke-white" />
</div>
</div>
{children}

View File

@ -0,0 +1,55 @@
/* -------------------------------------------------------------------------- */
/* Imports */
/* -------------------------------------------------------------------------- */
import React from "react";
import { Disclosure as Disclose } from "@headlessui/react";
import { ReactComponent as SelectIcon } from "../assets/svg/arrow-down.svg";
import classNames from "classnames";
/* -------------------------------------------------------------------------- */
/* Component props */
/* -------------------------------------------------------------------------- */
type Props = {
children?: React.ReactNode;
className?: string;
caption?: string;
};
/* -------------------------------------------------------------------------- */
/* styles */
/* -------------------------------------------------------------------------- */
const ButtonStyle = `
flex w-full
justify-between
items-center
py-2 text-left
text-base
font-medium
text-black-400
`;
/* -------------------------------------------------------------------------- */
/* Component implementation */
/* -------------------------------------------------------------------------- */
export default function Disclosure({ children, className, caption }: Props) {
return (
<div className={classNames("top-16 ", className)}>
<div className="mx-auto w-full bg-white py-2">
<Disclose as="div">
{({ open }) => (
<>
<Disclose.Button className={`${ButtonStyle}`}>
<span>{caption}</span>
<SelectIcon
className={`${
open ? "rotate-180 transform" : ""
} h-4 w-5 fill-black hover:fill-black stroke-black`}
/>
</Disclose.Button>
<Disclose.Panel className="py-2 text-sm text-gray-500 ">
{children}
</Disclose.Panel>
</>
)}
</Disclose>
</div>
</div>
);
}

View File

@ -0,0 +1,64 @@
/* -------------------------------------------------------------------------- */
/* Imports */
/* -------------------------------------------------------------------------- */
import React from "react";
import Badge from "components/Badge";
import { IProduct } from "./IProdutct";
import classNames from "classnames";
/* -------------------------------------------------------------------------- */
/* Component props */
/* -------------------------------------------------------------------------- */
type Props = {
hints?: IProduct[];
delFilter: (element: IProduct) => void;
clearAll: () => void;
className?: string;
};
/* -------------------------------------------------------------------------- */
/* Component implementation */
/* -------------------------------------------------------------------------- */
export default function AppiledFilters({
hints,
delFilter,
clearAll,
className,
}: Props) {
return (
<>
<div
className={classNames(
"w-full py-3 h-36 max-h-36 overflow-hidden ",
className
)}
>
<div className="flex flex-row items-center justify-between space-x-3 ">
<div className="font-medium">Фильтры</div>
<div>
<button
onClick={clearAll}
className="font-normal text-sm text-gray-400"
>
Очистить всё
</button>
</div>
</div>
<ul className="list-none mt-2 h-24 max-h-32 overflow-auto">
{hints &&
hints.length > 0 &&
hints.map((item: IProduct) => (
<li className="mt-1" key={item.id}>
<Badge
className="py-1 cursor-default"
emphasis="high"
closeOption={true}
onClick={() => delFilter(item)}
>
{item.brand}
</Badge>
</li>
))}
</ul>
</div>
</>
);
}

View File

@ -0,0 +1,109 @@
import React from "react";
import Disclosure from "components/Disclosure";
import AppiledFilters from "./AppiledFilters";
import SearchFilterBar from "./SearchFilterBar";
import axios from "axios";
import { useDebounce } from "./functions/debounce";
import { IProduct } from "./IProdutct";
type Props = {
className?: string;
};
export default function Fiter({ className }: Props) {
const [checkedId, setCheckedId] = React.useState<Array<number>>([]);
const [activeFilter, setActiveFilter] = React.useState<Array<IProduct>>([]);
/* -------------------------------------------------------------------------- */
/* Test request to demonstrate the basic functionality of the filter */
/* -------------------------------------------------------------------------- */
const [query, setQuery] = React.useState("");
const [hints, setHints] = React.useState<Array<IProduct>>([]);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};
const debounced = useDebounce(query);
async function fetchProducts() {
const response = await axios.get(
`https://dummyjson.com/products/search?q=${debounced}`
);
setHints(response.data.products);
}
React.useEffect(() => {
fetchProducts();
}, [debounced, query]);
const isChecked = (item: number) => (checkedId.includes(item) ? true : false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedId = parseInt(e.target.value);
/* ------------- Check if "checkedId" contains "selectedIds" ------------- */
/* -------------- If true, this checkbox is already checked -------------- */
if (checkedId.includes(selectedId)) {
setCheckedId(checkedId.filter((id) => id !== selectedId));
setActiveFilter(activeFilter.filter((item) => item.id !== selectedId));
} else {
const newId = [...checkedId];
newId.push(selectedId);
setCheckedId(newId);
/* --------------- Adding checked filters to AppliedFilters component -------------- */
hints.forEach((hint) => {
if (hint.id === selectedId)
activeFilter.push({ id: selectedId, brand: hint.brand });
setActiveFilter(activeFilter);
});
}
};
/* ------------- Delete filter from AppliedFilters component ------------- */
const DelFilter = (e: IProduct) => {
setActiveFilter(activeFilter.filter((item) => item.id !== e.id));
setCheckedId(checkedId.filter((id) => id !== e.id));
};
const clearAll = () => {
setActiveFilter([]);
setCheckedId([]);
setQuery("");
};
//console.log(activeFilter)
return (
<>
<div className="pl-2">
<div className="max-w-[268px] mx-auto divide-y divide-solid">
<AppiledFilters
hints={activeFilter}
delFilter={DelFilter}
clearAll={clearAll}
></AppiledFilters>
<Disclosure caption="Авторы">
<SearchFilterBar
hints={hints}
isChecked={isChecked}
handleChange={handleChange}
onChange={onChange}
query={query}
placeHolder={"Введите имя автора"}
/>
</Disclosure>
<Disclosure caption="Публикации">
<p>контент...</p>
</Disclosure>
<Disclosure caption="Тип издания">
<p>контент...</p>
</Disclosure>
<Disclosure caption="Тематика публикаций">
<p>контент...</p>
</Disclosure>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,23 @@
export interface IProduct {
id?: number;
title?: string;
description?: string;
price?: number;
discountPercentage?: number;
rating?: number;
stock?: number;
brand?: string;
category?: string;
thumbnail?: string;
images?: string[];
}
export interface serverResponse<T> {
products: T[];
total: number;
skip: number;
limit: number;
}

View File

@ -0,0 +1,117 @@
import { useState, useCallback, Fragment } from "react";
import { Combobox, Transition } from "@headlessui/react";
import { IProduct } from "./IProdutct";
import Checkbox from "components/Checkbox";
import { ReactComponent as SearchIcon } from "./svg/searchIcon.svg";
import classNames from "classnames";
type Props = {
hints: IProduct[];
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isChecked: (item: number) => boolean;
className?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeHolder: string;
query: string;
};
export default function SearchFilterBar({
hints,
isChecked,
handleChange,
className,
onChange,
placeHolder,
query,
}: Props): JSX.Element {
const [checkList, setCheckList] = useState(hints);
const [showFilters, SetShowFilters] = useState(false);
const ShowAllFilters = useCallback(() => {
SetShowFilters((prev) => !prev);
}, [SetShowFilters]);
return (
<>
<Combobox value={checkList} onChange={setCheckList}>
{({ open }) => (
<div>
<div
className={classNames(
"flex flex-row w-full items-center border border-solid rounded border-gray-200",
className
)}
>
<div className="basis-6 ">
<Combobox.Button>
<div className="pl-1 pt-1">
<SearchIcon />
</div>
</Combobox.Button>
</div>
<div className="grow">
<Combobox.Input
onChange={onChange}
placeholder={placeHolder}
className="w-full text-sm py-1 focus:outline-none"
/>
</div>
</div>
<div className="mt-4">
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options static={true}>
{open && (
<ul
className={classNames([
"w-[276px] relative h-40 top-0 right-2",
[
{
"max-h-60 h-60 overflow-auto": showFilters === true,
"max-h-40 h-40 overflow-hidden":
showFilters === false,
},
],
])}
>
<li className="">
{hints.length >= 5 && query !== "" && (
<button
onClick={ShowAllFilters}
className="text-right text-blue-500 pl-2 text-sm font-medium"
>
Показать всё({hints.length})
</button>
)}
</li>
{hints.length > 0 ? (
hints.map((item) => (
<li key={item.id} className="mt-1 pl-2">
<Checkbox
className="z-10 mt-1 w-full max-h-56"
value={item.id}
isChecked={item.id ? isChecked(item.id) : false}
onChange={handleChange}
>
<p>{item.brand}</p>
</Checkbox>
</li>
))
) : (
<p className="text-blue-300 pl-2">Nothing Found</p>
)}
</ul>
)}
</Combobox.Options>
</Transition>
</div>
</div>
)}
</Combobox>
</>
);
}

View File

@ -0,0 +1,11 @@
import React from "react";
export function useDebounce(value: string, delay: number = 300) {
const [debounced, setDebouced] = React.useState(value);
React.useEffect(() => {
const handler = setTimeout(() => setDebouced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
}

View File

@ -0,0 +1,4 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9497 11.4498L1.05026 1.55027" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M10.9497 1.55029L1.05025 11.4498" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.1003 14.1184L10.4628 9.48086C11.1824 8.55051 11.5717 7.41301 11.5717 6.21658C11.5717 4.78444 11.0128 3.44158 10.0021 2.42908C8.99135 1.41658 7.64492 0.859436 6.21456 0.859436C4.78421 0.859436 3.43778 1.41836 2.42706 2.42908C1.41456 3.43979 0.857422 4.78444 0.857422 6.21658C0.857422 7.64694 1.41635 8.99336 2.42706 10.0041C3.43778 11.0166 4.78242 11.5737 6.21456 11.5737C7.41099 11.5737 8.54671 11.1844 9.47707 10.4666L14.1146 15.1023C14.1282 15.1159 14.1443 15.1267 14.1621 15.1341C14.1799 15.1414 14.1989 15.1452 14.2181 15.1452C14.2374 15.1452 14.2564 15.1414 14.2742 15.1341C14.292 15.1267 14.3081 15.1159 14.3217 15.1023L15.1003 14.3255C15.1139 14.3119 15.1247 14.2958 15.132 14.278C15.1394 14.2602 15.1432 14.2412 15.1432 14.2219C15.1432 14.2027 15.1394 14.1837 15.132 14.1659C15.1247 14.1481 15.1139 14.132 15.1003 14.1184ZM9.04314 9.04515C8.28599 9.80051 7.28242 10.2166 6.21456 10.2166C5.14671 10.2166 4.14314 9.80051 3.38599 9.04515C2.63064 8.28801 2.21456 7.28444 2.21456 6.21658C2.21456 5.14872 2.63064 4.14336 3.38599 3.38801C4.14314 2.63265 5.14671 2.21658 6.21456 2.21658C7.28242 2.21658 8.28778 2.63086 9.04314 3.38801C9.79849 4.14515 10.2146 5.14872 10.2146 6.21658C10.2146 7.28444 9.79849 8.28979 9.04314 9.04515Z" fill="#8C8C8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -98,10 +98,10 @@ const FeaturedArticlesCards = () => {
const navigationPrevRef = useRef(null);
const navigationNextRef = useRef(null);
const paginationRef = useRef(null);
const [t, i18next] = useTranslation()
const [t, i18next] = useTranslation();
return (
<div className="slider-wrapper articles">
<div className="slider-wrapper articles px-8">
<div className="flex justify-end gap-2 my-2">
<div
className="prev inline-flex justify-center items-center
@ -169,7 +169,10 @@ const FeaturedArticlesCards = () => {
<Card.CardAction href={Articale.Link}>
<Link to="*">
<Typography className="text-blue-500 font-bold">
<Typography
className="text-blue-500 font-bold"
fontWeightVariant="bold"
>
{t("mainPage.more")}
</Typography>
</Link>

View File

@ -75,10 +75,12 @@ export default function FeaturedAuthorsCards(): JSX.Element {
return (
<div>
{/* The Title of Featured Authors section */}
<Heading className="text-center my-8 text-3xl font-semibold">{t("mainPage.featuredAuthors")}</Heading>
<Heading className="text-center my-8 text-3xl font-semibold">
{t("mainPage.featuredAuthors")}
</Heading>
{/* Featured Authors section */}
<div className="slider-wrapper Authors">
<div className="slider-wrapper Authors px-8">
<Swiper
slidesPerView={1.25}
slidesPerGroup={1}
@ -133,7 +135,7 @@ export default function FeaturedAuthorsCards(): JSX.Element {
<Card.CardAction href={card.Link}>
<Link className="text-blue-500 font-bold" to="*">
{t('mainPage.more')}
{t("mainPage.more")}
</Link>
<SVGCaretRight className="fill-blue-500 w-4 h-4" />
</Card.CardAction>

View File

@ -33,7 +33,7 @@ const AnArticle = () => {
<AskeletonArticle />
) : (
<>
<ArticlePart.Article.Breadcumbs>
<ArticlePart.Article.Breadcumbs className="py-4">
{article?.topic}
</ArticlePart.Article.Breadcumbs>
<div className="flex flex-col gap-4 pb-4">
@ -52,25 +52,18 @@ const AnArticle = () => {
/>
{article?.tags && (
<div className="keywords my-10 flex flex-col gap-2">
<Typography
className="text-2xl"
fontWeightVariant="semibold"
>
{t('articlePage.keywords')}
<Typography className="text-2xl" fontWeightVariant="semibold">
{t("articlePage.keywords")}
</Typography>
<ArticlePart.Article.Keywords className="transition ease-in-out delay-50">
{article?.tags}
</ArticlePart.Article.Keywords>
</div>
)}
<div className="abstract my-10 flex flex-col gap-2">
<Typography
className="text-2xl"
fontWeightVariant="semibold"
>
{t('articlePage.abstract')}
<Typography className="text-2xl" fontWeightVariant="semibold">
{t("articlePage.abstract")}
</Typography>
<ArticlePart.Article.Description>
{article?.summary !== undefined ? (

View File

@ -27,7 +27,7 @@ const AnArticleBody = () => {
<Container variant="straight">
{shouldShowLoading ? (
<>
<Skeleton count={1} />
<Skeleton count={1} className="my-4" />
<div className="gap-4 py-12 px-20">
<Skeleton
count={1}
@ -39,7 +39,7 @@ const AnArticleBody = () => {
</>
) : (
<>
<ArticlePart.Article.Breadcumbs>
<ArticlePart.Article.Breadcumbs className="py-4">
{article?.topic}
</ArticlePart.Article.Breadcumbs>
<div className="gap-4 py-12 px-20">

View File

@ -3,7 +3,7 @@ import "react-loading-skeleton/dist/skeleton.css";
const AskeletonArticle = () => {
return (
<>
<Skeleton count={1} />
<Skeleton count={1} className="my-4" />
<div className="flex flex-col gap-4 pb-4">
<Skeleton count={1} containerClassName="title w-3/4 text-2xl" />
<Skeleton count={1} containerClassName="authors w-1/4" />

View File

@ -51,6 +51,7 @@ export default function Link({
: style
}
aria-disabled={disabled}
className="flex items-center"
{...props}
>
{children}

View File

@ -42,7 +42,8 @@ root.render(
<Route path="/account">
<Route path="settings" element={<AccountSettings />} />
</Route>
<Route path="/search-results" element={<SearchResultsPage />}></Route>
<Route path="/search-results" element={<SearchResultsPage />} />
<Route path="/*" element={<NotFound />}></Route>
</Routes>
</BrowserRouter>
</React.StrictMode>

View File

@ -6,6 +6,7 @@ import { SearchResultSection } from "components/SearchResultsSection";
import { useSearchStoreImplementation } from "searchResults/data/searchStoreImplementation";
import { useSearchViewModel } from "searchResults/controller/searchResultsViewModel";
import { Loader } from "components/Loader/Loader";
import Fiter from "components/Filters/Filter";
export const SearchResultsPage = () => {
return (