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


); } 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 ( {name} {productVariant !== undefined && productCharacteristics !== undefined && ( {productVariant}: {productCharacteristics} )} ); }; return (
{rankingPosition === undefined ? (
) : ( <> #{rankingPosition}
)} {rightComponent}
); } 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 (
Immer auf dem aktuellen Stand sein

Neue Produkte

{products.NewProducts.length > 0 ? ( <> {products.NewProducts.map((product, index) => ( window.open(product.Url)} /> } /> ))} ) : ( )}
Was als nächstes kommt

Aktuell in Arbeit

{products.InWorkProducts.length > 0 ? ( <> {products.InWorkProducts.map((product, index) => ( } /> ))} ) : ( )}
Jede Stimme zählt

Zukünftige Produkte

{products.FutureProducts.length > 0 ? ( <> {products.FutureProducts /*.sort((a, b) => a.Name.localeCompare(b.Name) )*/ .sort((a, b) => b.Votes - a.Votes) .map((product, index) => ( 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, }; }); } }} /> {product.Votes} 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, }; }); } }} />
} /> ))} ) : ( )}
); } export default App;