made news details page, made review section

This commit is contained in:
Alibek 2024-01-25 16:48:05 +06:00
parent 083ef42f9e
commit bb852f5119
30 changed files with 710 additions and 123 deletions

View File

@ -14,6 +14,7 @@
"react": "^18",
"react-dom": "^18",
"sass": "^1.70.0",
"swr": "^2.2.4",
"zustand": "^4.5.0"
},
"devDependencies": {

View File

@ -1,3 +1,5 @@
"use client";
import Image, { StaticImageData } from "next/image";
import "./NewsCard.scss";
import Link from "next/link";

View File

@ -0,0 +1,53 @@
.review-card {
padding: 15px 20px;
display: flex;
flex-direction: column;
gap: 15px;
border-radius: 12px;
border: 1px solid #c5c6c5;
&__header {
display: flex;
align-items: center;
gap: 10px;
&_left {
min-width: 60px;
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
&_right {
display: flex;
flex-direction: column;
gap: 8px;
h4 {
color: #3e3232;
font-size: 16px;
font-weight: 800;
}
span {
display: flex;
align-items: center;
gap: 8px;
color: rgba(62, 50, 50, 0.75);
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
}
}
p {
color: rgba(0, 0, 0, 0.75);
font-size: 18px;
font-weight: 500;
line-height: 20px;
letter-spacing: 0.25px;
}
}

View File

@ -0,0 +1,67 @@
import Image from "next/image";
import "./ReviewCard.scss";
import calendar_icon from "./icons/calendar-icon.svg";
interface IReviewCardProps {
item: IReview;
}
interface IReview {
id: number;
author: IAuthor;
review: string;
created_at: string;
}
interface IAuthor {
id: number;
first_name: string;
last_name: string;
image: string;
}
const ReviewCard: React.FC<IReviewCardProps> = ({
item,
}: IReviewCardProps) => {
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const year = item?.created_at.slice(0, 4);
const month = item?.created_at.slice(5, 7);
const day = item?.created_at.slice(8, 10);
return (
<div className="review-card">
<div className="review-card__header">
<img
className="review-card__header_left"
src={item.author.image}
alt="Author Image"
/>
<div className="review-card__header_right">
<h4>
{item.author.first_name} {item.author.last_name}
</h4>
<span>
<Image src={calendar_icon} alt="Calendar Icon" />
{month && months[month]} {day}, {year}
</span>
</div>
</div>
<p>{item.review}</p>
</div>
);
};
export default ReviewCard;

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="calendar">
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

View File

@ -0,0 +1,48 @@
.create-review {
margin-top: 70px;
display: flex;
flex-direction: column;
gap: 40px;
h3 {
display: flex;
align-items: center;
gap: 6px;
color: #3e3232;
font-size: 24px;
font-weight: 500;
span {
width: 4px;
height: 10px;
border-radius: 12px;
background: #3998e8;
}
}
div {
display: flex;
flex-direction: column;
gap: 18px;
textarea {
height: 258px;
padding: 27px 18px;
resize: none;
border-radius: 12px;
border: 1px solid #c5c6c5;
font-size: 16px;
outline: none;
}
::placeholder {
color: #c5c6c5;
font-size: 16px;
font-weight: 400;
}
button {
width: fit-content;
align-self: flex-end;
}
}
}

View File

@ -0,0 +1,46 @@
"use client";
import "./CreateReview.scss";
import { useState } from "react";
import Button from "@/Shared/UI/Button/Button";
import { createReviewAction } from "./actions";
import { StaticImageData } from "next/image";
interface ICreateReviewProps {
endpoint: string;
id: number | null | undefined;
}
const CreateReview: React.FC<ICreateReviewProps> = ({
endpoint,
id,
}: ICreateReviewProps) => {
const [review, setReview] = useState<string>("");
return (
<div className="create-review">
<h3>
<span />
Написать комментарий
</h3>
<div>
<textarea
onChange={(e) => setReview(e.target.value)}
value={review}
placeholder="Напишите комментарий"
/>
<Button
disabled={createReviewAction.isLoading}
onClick={() => {
setReview("");
createReviewAction.createReview(endpoint, id, review);
}}
>
Отправить
</Button>
</div>
</div>
);
};
export default CreateReview;

View File

@ -0,0 +1,59 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import { getTokens } from "@/Shared/helpers/getTokens";
import axios, { AxiosError } from "axios";
class CreateReview {
response: string;
isLoading: boolean;
error: string;
constructor() {
this.response = "";
this.isLoading = false;
this.error = "";
}
async createReview(
endpoint: string,
id: number | null | undefined,
review: string
) {
try {
this.isLoading = true;
const access_token = getTokens()?.access_token;
const Authorization = `Bearer ${access_token}`;
const config = {
headers: {
Authorization,
},
};
const body = {
review,
};
const response = await axios.post(
`${baseAPI}/${endpoint}/${id}/reviews/`,
body,
config
);
this.response = response.data;
console.log(response);
} catch (error: unknown) {
console.log(error);
if (error instanceof AxiosError) {
this.error = error.message;
} else {
this.error = "An error ocured";
}
} finally {
this.isLoading = false;
}
}
}
export const createReviewAction = new CreateReview();

View File

@ -43,7 +43,7 @@ export const useSignIn = create<SignInStore>((set) => ({
user
);
localStorage.setItem("user", JSON.stringify(response.data));
localStorage.setItem("tokens", JSON.stringify(response.data));
set({ emailError: "" });
set({ passwordError: "" });

View File

@ -86,7 +86,7 @@ export const useSignUp = create<SignUpStore>((set) => ({
user
);
localStorage.setItem("user", JSON.stringify(response.data));
localStorage.setItem("tokens", JSON.stringify(response.data));
set({ emailError: "" });
set({ passwordError: "" });

View File

@ -0,0 +1,65 @@
.news-details-page {
padding: 118px 90px 0 90px;
display: flex;
flex-direction: column;
gap: 40px;
&__wrapper {
max-width: 1070px;
display: flex;
align-self: center;
flex-direction: column;
gap: 40px;
}
&__image {
display: flex;
flex-direction: column;
gap: 30px;
&_main {
width: 100%;
height: 600px;
border-radius: 8px;
object-fit: cover;
}
}
&__date-and-reviews {
display: flex;
align-items: center;
gap: 80px;
span {
display: flex;
align-items: center;
gap: 4px;
color: rgba(62, 50, 50, 0.75);
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
}
&__description {
color: #3e3232;
font-size: 20px;
line-height: 34px;
}
}
@media screen and (max-width: 1024px) {
.news-details-page {
padding: 118px 30px 0 30px;
}
}
@media screen and (max-width: 768px) {
.news-details-page {
padding: 112px 30px 0 30px;
}
}
@media screen and (max-width: 550px) {
.news-details-page {
padding: 112px 16px 0 16px;
}
}

View File

@ -1,7 +1,69 @@
import { newsDetailsStore } from "./store";
import "./NewsDetailsPage.scss";
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import message_icon from "./icons/message-icon.svg";
import calendar_icon from "./icons/calendar-icon.svg";
import Image from "next/image";
import NewsReviewsSection from "@/Widgets/NewsReviewsSection/NewsReviewsSection";
const NewsDetailsPage = () => {
return <div>NewsDetailsPage</div>;
const NewsDetailsPage = async ({
params,
}: {
params: { id: string };
}) => {
const data = await newsDetailsStore.getNewsDetails(params.id);
console.log(data);
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const year = data?.created_at.slice(0, 4);
const month = data?.created_at.slice(5, 7);
const day = data?.created_at.slice(8, 10);
return (
<div className="news-details-page">
<HeaderText>{data?.title}</HeaderText>
<div className="news-details-page__wrapper">
<div className="news-details-page__image">
<img
src={data?.image}
alt="News Image"
className="news-details-page__image_main"
/>
<div className="news-details-page__date-and-reviews">
<span>
<Image src={calendar_icon} alt="Calendar Icon" />
{month && months[month]} {day}, {year}
</span>
<span>
<Image src={message_icon} alt="Message Icon" />
Комментарии: {data?.count_reviews}
</span>
</div>
</div>
<p className="news-details-page__description">
{data?.description}
</p>
<NewsReviewsSection id={data?.id} list={data?.news_review} />
</div>
</div>
);
};
export default NewsDetailsPage;

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="calendar">
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-circle">
<path id="Vector" d="M14 7.66669C14.0023 8.5466 13.7967 9.41461 13.4 10.2C12.9296 11.1412 12.2065 11.9328 11.3116 12.4862C10.4168 13.0396 9.3855 13.3329 8.33333 13.3334C7.45342 13.3356 6.58541 13.1301 5.8 12.7334L2 14L3.26667 10.2C2.86995 9.41461 2.66437 8.5466 2.66667 7.66669C2.66707 6.61452 2.96041 5.58325 3.51381 4.68839C4.06722 3.79352 4.85884 3.0704 5.8 2.60002C6.58541 2.20331 7.45342 1.99772 8.33333 2.00002H8.66667C10.0562 2.07668 11.3687 2.66319 12.3528 3.64726C13.3368 4.63132 13.9233 5.94379 14 7.33335V7.66669Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 724 B

View File

@ -0,0 +1 @@
export * from "./store";

View File

@ -0,0 +1,54 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import axios, { AxiosError } from "axios";
import { StaticImport } from "next/dist/shared/lib/get-img-props";
import { StaticImageData } from "next/image";
interface IDetails {
id: number | null;
image: string;
title: string;
description: string;
news_review: IReview[];
created_at: string;
count_reviews: number;
}
interface IReview {
id: number;
author: IAuthor;
review: string;
created_at: string;
}
interface IAuthor {
id: number;
first_name: string;
last_name: string;
image: string;
}
class NewsDetailsStore {
error: string;
constructor() {
this.error = "";
}
async getNewsDetails(id: string) {
try {
const response = await axios.get<IDetails>(
`${baseAPI}/news/${id}/`
);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) {
this.error = error.message;
} else {
console.log(error);
this.error = "An error occured";
}
}
}
}
export const newsDetailsStore = new NewsDetailsStore();

View File

@ -1,8 +1,8 @@
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import "./NewsPage.scss";
import NewsList from "@/Widgets/NewsList/NewsList";
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
const NewsPage = () => {
const NewsPage = async () => {
return (
<div className="news-page">
<HeaderText>Новости</HeaderText>

View File

@ -4,8 +4,15 @@ interface IButton extends React.AllHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode | null;
}
const Button: React.FC<IButton> = ({ children }: IButton) => {
return <button className="ui-btn">{children}</button>;
const Button: React.FC<IButton> = ({
children,
onClick,
}: IButton) => {
return (
<button className="ui-btn" onClick={onClick}>
{children}
</button>
);
};
export default Button;

View File

@ -0,0 +1,20 @@
type Tokens = {
access_token: string;
refresh_token: string;
};
export const getTokens = (): Tokens | null => {
try {
const storage: string | null = localStorage.getItem("tokens");
if (storage === null) {
throw new Error("Tokens not found");
}
const parsed: Tokens = JSON.parse(storage);
return parsed;
} catch (error: any) {
console.error("Error retrieving tokens:", error.message);
return null;
}
};

View File

@ -1,19 +1,17 @@
"use client";
import "./NewsList.scss";
import { useEffect } from "react";
import { useNews } from "./news.store";
import NewsCard from "@/Entities/NewsCard/NewsCard";
import "./NewsList.scss";
import { newsStore } from "./store";
const NewsList = () => {
const { data, getNews } = useNews();
const NewsList = async () => {
const data = await newsStore.getNews();
if (newsStore.error) {
return <div className="news-list">{newsStore.error}</div>;
}
useEffect(() => {
getNews();
}, []);
return (
<ul className="news-list">
{data.results.map((card) => (
{data?.results.map((card) => (
<li key={card.id}>
<NewsCard
id={card.id}

View File

@ -0,0 +1 @@
export * from "./store";

View File

@ -0,0 +1,39 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import { IList } from "@/Shared/types";
import axios, { AxiosError } from "axios";
import { StaticImageData } from "next/image";
interface INews extends IList {
results: IResult[];
}
interface IResult {
id: number;
image: StaticImageData;
title: string;
description: string;
created_at: string;
}
class NewsStore {
error: string;
constructor() {
this.error = "";
}
async getNews() {
try {
const response = await axios.get<INews>(`${baseAPI}/news/`);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) {
this.error = error.message;
} else {
this.error = "An error occurred.";
}
}
}
}
export const newsStore = new NewsStore();

View File

@ -0,0 +1,34 @@
.news-reviews-section {
display: flex;
flex-direction: column;
gap: 70px;
h3 {
display: flex;
align-items: center;
gap: 6px;
color: #3e3232;
font-size: 24px;
font-weight: 500;
span {
width: 4px;
height: 10px;
border-radius: 12px;
background: #3998e8;
}
}
&__container {
display: flex;
flex-direction: column;
gap: 24px;
}
&__list {
display: flex;
flex-direction: column;
gap: 30px;
}
}

View File

@ -0,0 +1,46 @@
import ReviewCard from "@/Entities/ReviewCard/ReviewCard";
import "./NewsReviewsSection.scss";
import CreateReview from "@/Features/CreateReview/CreateReview";
interface INewsReviewsSectionProps {
id: number | null | undefined;
list: IReview[] | undefined;
}
interface IReview {
id: number;
author: IAuthor;
review: string;
created_at: string;
}
interface IAuthor {
id: number;
first_name: string;
last_name: string;
image: string;
}
const NewsReviewsSection: React.FC<INewsReviewsSectionProps> = ({
id,
list,
}: INewsReviewsSectionProps) => {
return (
<div className="news-reviews-section">
<CreateReview endpoint="news" id={id} />
<div className="news-reviews-section__container">
<h3>
<span />
Комментарии
</h3>
<div>
{list?.map((item) => (
<ReviewCard key={item.id} item={item} />
))}
</div>
</div>
</div>
);
};
export default NewsReviewsSection;

View File

@ -33,7 +33,7 @@
background-color: #f1f1f1;
}
&_active {
#statistics-table__location_active {
background-color: #f1f1f1;
}
}

View File

@ -10,18 +10,20 @@ import { useStatistics } from "./statistics.store";
import { useEffect, useState } from "react";
const StatisticsTable = () => {
const [location, setLocation] = useState("city");
const [location, setLocation] = useState("village");
const [locationMenu, setLocationMenu] = useState(false);
const {
data,
getStatisticsForCity,
getStatisticsForState,
getStatisticsForVillage,
} = useStatistics();
const { data, getStatistics } = useStatistics();
const locations = [
{ id: 1, name: "Регион", type: "state" },
{ id: 2, name: "Город", type: "city" },
{ id: 3, name: "Деревня", type: "village" },
];
useEffect(() => {
getStatisticsForState();
getStatistics();
}, []);
return (
<div className="statistics-table">
{locationMenu && (
@ -29,54 +31,26 @@ const StatisticsTable = () => {
onClick={(e) => e.stopPropagation()}
className="statistics-table__location"
>
<li
className={
location === "state"
? "statistics-table__location_active"
: ""
}
>
<button
onClick={() => {
setLocation("state");
getStatisticsForState();
}}
{locations.map((loc) => (
<li
key={loc.id}
id={
location === loc.type
? "statistics-table__location_active"
: ""
}
>
Регион
</button>
</li>
<li
className={
location === "city"
? "statistics-table__location_active"
: ""
}
>
<button
onClick={() => {
setLocation("city");
getStatisticsForCity();
}}
>
Город
</button>
</li>
<li
className={
location === "village"
? "statistics-table__location_active"
: ""
}
>
<button
onClick={() => {
setLocation("village");
getStatisticsForVillage();
}}
>
Деревня
</button>
</li>
<button
onClick={() => {
setLocation(loc.type);
getStatistics(loc.type);
setLocationMenu(false);
}}
>
{loc.name}
</button>
</li>
))}
</ul>
)}
<div className="statistics-table__wrapper">
@ -168,17 +142,25 @@ const StatisticsTable = () => {
</thead>
<tbody>
{data.map((statistic) => (
{data.length !== 0 ? (
data.map((statistic) => (
<tr>
<td>{statistic.name}</td>
<td>{statistic.broken_road_1}</td>
<td>{statistic.local_defect_3}</td>
<td>{statistic.hotbed_of_accidents_2}</td>
<td>{statistic.local_defect_fixed_6}</td>
<td>{statistic.repair_plans_4}</td>
<td>{statistic.repaired_5}</td>
</tr>
))
) : (
<tr>
<td>{statistic.name}</td>
<td>{statistic.broken_road_1}</td>
<td>{statistic.local_defect_3}</td>
<td>{statistic.hotbed_of_accidents_2}</td>
<td>{statistic.local_defect_fixed_6}</td>
<td>{statistic.repair_plans_4}</td>
<td>{statistic.repaired_5}</td>
<td style={{ textAlign: "center" }} colSpan={7}>
Looks like there is no data for {location}
</td>
</tr>
))}
)}
</tbody>
</table>
</div>

View File

@ -15,51 +15,19 @@ interface IStatistic {
interface IStatisticStore extends IFetch {
data: IStatistic[];
getStatisticsForCity: () => Promise<void>;
getStatisticsForState: () => Promise<void>;
getStatisticsForVillage: () => Promise<void>;
getStatistics: (location?: string) => Promise<void>;
}
export const useStatistics = create<IStatisticStore>((set) => ({
data: [],
loading: false,
error: "",
getStatisticsForCity: async () => {
getStatistics: async (location: string = "village") => {
try {
set({ loading: true });
const response = await axios.get<IStatistic[]>(
`${baseAPI}/report/city/stats/`
);
set({ data: response.data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ loading: false });
}
},
getStatisticsForState: async () => {
try {
set({ loading: true });
const response = await axios.get<IStatistic[]>(
`${baseAPI}/report/state/stats/`
);
set({ data: response.data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ loading: false });
}
},
getStatisticsForVillage: async () => {
try {
set({ loading: true });
const response = await axios.get<IStatistic[]>(
`${baseAPI}/report/village/stats/`
`${baseAPI}/report/${location}/stats/`
);
set({ data: response.data });

View File

@ -25,7 +25,7 @@ const Navbar = () => {
<li
key={page.id}
className={
page.path === pathname
page.path === pathname?.slice(0, page.path.length)
? "nav__pages-item_active"
: "nav__pages-item"
}

View File

@ -21,6 +21,11 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View File

@ -526,7 +526,7 @@ chalk@^4.0.0:
optionalDependencies:
fsevents "~2.3.2"
client-only@0.0.1:
client-only@0.0.1, client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
@ -2193,6 +2193,14 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -2308,7 +2316,7 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-sync-external-store@1.2.0:
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==