336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
import {
|
|
DislikeOutlined,
|
|
LikeOutlined,
|
|
RightOutlined,
|
|
} from "@ant-design/icons";
|
|
import "./App.css";
|
|
import { Badge, Flex, Skeleton } from "antd";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { motion } from "framer-motion";
|
|
|
|
function SkeletonPlaceholder() {
|
|
return (
|
|
<>
|
|
<Skeleton.Input active={true} size="large" block={true} />
|
|
<br />
|
|
<Skeleton.Input active={true} size="large" block={true} />
|
|
<br />
|
|
<Skeleton.Input active={true} size="large" block={true} />
|
|
<br />
|
|
<Skeleton.Input active={true} size="large" block={true} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UseVoteLocalStorage() {
|
|
const getVotes = () => {
|
|
const votes = localStorage.getItem("shx-product-pipeline-votes");
|
|
return votes === null ? [] : JSON.parse(votes);
|
|
};
|
|
|
|
const [storageVotes, setStorageVotes] = useState(getVotes());
|
|
|
|
const saveVotes = (votes) => {
|
|
setStorageVotes(votes);
|
|
localStorage.setItem("shx-product-pipeline-votes", JSON.stringify(votes));
|
|
};
|
|
|
|
const vote = (name, up) => {
|
|
const newVotes = [...storageVotes];
|
|
|
|
const existingVoteIndex = newVotes.findIndex((vote) => vote.n === name);
|
|
|
|
if (existingVoteIndex !== -1) {
|
|
newVotes[existingVoteIndex].t = up === true ? 1 : 0;
|
|
} else {
|
|
newVotes.push({
|
|
n: name,
|
|
t: up === true ? 1 : 0,
|
|
});
|
|
}
|
|
|
|
saveVotes(newVotes);
|
|
};
|
|
|
|
return {
|
|
votes: storageVotes,
|
|
vote: vote,
|
|
setVotes: saveVotes,
|
|
};
|
|
}
|
|
|
|
function Card({
|
|
rankingPosition,
|
|
name,
|
|
productVariant,
|
|
productCharacteristics,
|
|
rightComponent,
|
|
}) {
|
|
const MiddleComponent = () => {
|
|
return (
|
|
<Flex vertical style={{ textAlign: "left" }}>
|
|
<span>{name}</span>
|
|
{productVariant !== undefined &&
|
|
productCharacteristics !== undefined && (
|
|
<span style={{ fontSize: 16, color: "#535c68" }}>
|
|
{productVariant}: {productCharacteristics}
|
|
</span>
|
|
)}
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="card">
|
|
<Flex justify="space-between">
|
|
{rankingPosition === undefined ? (
|
|
<div style={{ paddingLeft: 10 }}>
|
|
<MiddleComponent />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Flex gap={4} style={{ textAlign: "left" }} align="center">
|
|
<span style={{ color: "#787878", fontSize: 18 }}>
|
|
#{rankingPosition}
|
|
</span>
|
|
</Flex>
|
|
<div style={{ width: "100%", textAlign: "left", marginLeft: 10 }}>
|
|
<MiddleComponent />
|
|
</div>
|
|
</>
|
|
)}
|
|
<Flex vertical justify="center">
|
|
{rightComponent}
|
|
</Flex>
|
|
</Flex>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function VoteRequest(name, up) {
|
|
fetch(
|
|
`https://devdash.ex.umbach.dev/api/v1/productpipeline/vote?t=${
|
|
up === true ? "u" : "d"
|
|
}`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: name,
|
|
}),
|
|
}
|
|
).catch(() => {});
|
|
}
|
|
|
|
function App() {
|
|
const { votes, vote, setVotes } = UseVoteLocalStorage();
|
|
const votesRef = useRef(votes);
|
|
|
|
useEffect(() => {
|
|
votesRef.current = votes;
|
|
}, [votes]);
|
|
|
|
const [products, setProducts] = useState({
|
|
NewProducts: [],
|
|
InWorkProducts: [],
|
|
FutureProducts: [],
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fetchProducts = () =>
|
|
fetch("https://devdash.ex.umbach.dev/api/v1/productpipeline")
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
setProducts(data);
|
|
|
|
// remove votes for products which no longer in future products
|
|
setVotes(
|
|
votesRef.current.filter((v) =>
|
|
data.FutureProducts.some((item) => item.Name === v.n)
|
|
)
|
|
);
|
|
})
|
|
.catch(() => {});
|
|
|
|
fetchProducts();
|
|
|
|
setInterval(() => fetchProducts(), 3000);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="app">
|
|
<div className="container" style={{ paddingTop: 20 }}>
|
|
<span className="subtitle">Immer auf dem aktuellen Stand sein</span>
|
|
<h1 className="header">Neue Produkte</h1>
|
|
|
|
<div className="cards-container" style={{ paddingBottom: 20 }}>
|
|
<div className="cards">
|
|
{products.NewProducts.length > 0 ? (
|
|
<>
|
|
{products.NewProducts.map((product, index) => (
|
|
<Card
|
|
key={index}
|
|
name={product.Name}
|
|
productVariant={product.Variant}
|
|
productCharacteristics={product.Characteristics}
|
|
rightComponent={
|
|
<RightOutlined
|
|
style={{ fontSize: 18 }}
|
|
onClick={() => window.open(product.Url)}
|
|
/>
|
|
}
|
|
/>
|
|
))}
|
|
</>
|
|
) : (
|
|
<SkeletonPlaceholder />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<span className="subtitle">Was als nächstes kommt</span>
|
|
<h1 className="header">Aktuell in Arbeit</h1>
|
|
|
|
<div className="cards-container" style={{ paddingBottom: 20 }}>
|
|
<div className="cards">
|
|
{products.InWorkProducts.length > 0 ? (
|
|
<>
|
|
{products.InWorkProducts.map((product, index) => (
|
|
<Card
|
|
key={index}
|
|
name={product.Name}
|
|
productVariant={product.Variant}
|
|
productCharacteristics={product.Characteristics}
|
|
rightComponent={<Badge status="processing" />}
|
|
/>
|
|
))}
|
|
</>
|
|
) : (
|
|
<SkeletonPlaceholder />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<span className="subtitle">Jede Stimme zählt</span>
|
|
<h1 className="header">Zukünftige Produkte</h1>
|
|
|
|
<div className="cards-container">
|
|
<div className="cards">
|
|
{products.FutureProducts.length > 0 ? (
|
|
<>
|
|
{products.FutureProducts
|
|
/*.sort((a, b) =>
|
|
a.Name.localeCompare(b.Name)
|
|
)*/
|
|
.sort((a, b) => b.Votes - a.Votes)
|
|
.map((product, index) => (
|
|
<motion.div
|
|
key={product.Name}
|
|
layout
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Card
|
|
name={product.Name}
|
|
rankingPosition={index + 1}
|
|
productVariant={product.Variant}
|
|
productCharacteristics={product.Characteristics}
|
|
rightComponent={
|
|
<div style={{ display: "flex", gap: 12 }}>
|
|
<DislikeOutlined
|
|
style={{
|
|
color:
|
|
votes.findIndex(
|
|
(vote) =>
|
|
vote.n === product.Name && vote.t === 0
|
|
) > -1
|
|
? "red"
|
|
: "#000",
|
|
}}
|
|
onClick={() => {
|
|
if (
|
|
votes.findIndex(
|
|
(vote) =>
|
|
vote.n === product.Name && vote.t === 0
|
|
) === -1
|
|
) {
|
|
VoteRequest(product.Name, false);
|
|
vote(product.Name, false);
|
|
|
|
// simulate vote before request is made
|
|
setProducts((products) => {
|
|
const newArr = products.FutureProducts.map(
|
|
(p) =>
|
|
p.Name === product.Name
|
|
? { ...p, Votes: p.Votes - 1 }
|
|
: p
|
|
);
|
|
|
|
return {
|
|
...products,
|
|
FutureProducts: newArr,
|
|
};
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
<span>{product.Votes}</span>
|
|
<LikeOutlined
|
|
style={{
|
|
color:
|
|
votes.findIndex(
|
|
(vote) =>
|
|
vote.n === product.Name && vote.t === 1
|
|
) > -1
|
|
? "green"
|
|
: "#000",
|
|
}}
|
|
onClick={() => {
|
|
if (
|
|
votes.findIndex(
|
|
(vote) =>
|
|
vote.n === product.Name && vote.t === 1
|
|
) === -1
|
|
) {
|
|
VoteRequest(product.Name, true);
|
|
vote(product.Name, true);
|
|
|
|
// simulate vote before request is made
|
|
setProducts((products) => {
|
|
const newArr = products.FutureProducts.map(
|
|
(p) =>
|
|
p.Name === product.Name
|
|
? { ...p, Votes: p.Votes + 1 }
|
|
: p
|
|
);
|
|
|
|
return {
|
|
...products,
|
|
FutureProducts: newArr,
|
|
};
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</>
|
|
) : (
|
|
<SkeletonPlaceholder />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|