A webapp implementation of thanosSnap

Wrap client in Suspense component

modamo-gh 0553be62 8273113c

Changed files
+228 -216
app
components
+6 -216
app/page.tsx
··· 1 - "use client"; 2 - 3 - import { AtSign } from "lucide-react"; 4 - import { Manrope } from "next/font/google"; 5 - import { useSearchParams } from "next/navigation"; 6 - import { useEffect, useState } from "react"; 7 - 8 - const manrope = Manrope({ 9 - preload: true, 10 - subsets: ["latin"], 11 - weight: ["300"] 12 - }); 1 + import HomeContent from "@/components/HomeContent"; 2 + import { Suspense } from "react"; 13 3 14 4 const Home = () => { 15 - const searchParams = useSearchParams(); 16 - 17 - const [atProtoIdentifier, setAtProtoIdentifier] = useState(""); 18 - const [followCounts, setFollowCounts] = useState<number>(0); 19 - const [isCounting, setIsCounting] = useState(false); 20 - const [nonMutualCount, setNonMutualCount] = useState<null | number>(null); 21 - const [step, setStep] = useState< 22 - "last" | "login" | "nonMutuals" | "permission" | "stats" 23 - >("login"); 24 - const [unfollowCount, setUnfollowCount] = useState(0); 25 - 26 - useEffect(() => { 27 - const authStatus = searchParams.get("auth_status"); 28 - 29 - if (authStatus === "success") { 30 - const follows = searchParams.get("follows"); 31 - 32 - setFollowCounts(Number(follows)); 33 - setStep("stats"); 34 - 35 - window.history.replaceState(null, "", window.location.pathname); 36 - } 37 - }, [searchParams]); 38 - 39 - const ebbAndFollow = async () => { 40 - try { 41 - const response = await fetch("/api/ebbAndFollow/", { 42 - credentials: "include", 43 - headers: { "Content-Type": "application/json" }, 44 - method: "POST" 45 - }); 46 - const data = await response.json(); 47 - 48 - if (response.ok && data.success) { 49 - setUnfollowCount(data.unfollowCount); 50 - setStep("last"); 51 - } 52 - } catch (error) {} 53 - }; 54 - 55 - const fetchNonMutuals = async () => { 56 - setNonMutualCount(null); 57 - setIsCounting(true); 58 - 59 - try { 60 - const response = await fetch("/api/ebbAndFollow/count", { 61 - credentials: "include" 62 - }); 63 - const data = await response.json(); 64 - 65 - if (response.ok && data.success) { 66 - setNonMutualCount(data.nonMutualCount); 67 - setStep("nonMutuals"); 68 - } else { 69 - alert(`Failed to fetch non-mutual count: ${data.error}`); 70 - } 71 - } catch (error) { 72 - alert("A network error occurred while calculating the ebb."); 73 - } finally { 74 - setIsCounting(false); 75 - } 76 - }; 77 - 78 - const loginToATProto = async () => { 79 - console.log(atProtoIdentifier); 80 - 81 - try { 82 - const response = await fetch("/api/auth/atproto/login", { 83 - body: JSON.stringify({ 84 - atProtoIdentifier 85 - }), 86 - headers: { "Content-Type": "application/json" }, 87 - method: "POST" 88 - }); 89 - const data = await response.json(); 90 - 91 - if (response.ok && data.url) { 92 - window.location.href = data.url; 93 - } else { 94 - console.error("Login initiation failed:", data.error); 95 - } 96 - } catch (error) { 97 - console.error("Error initiating AT Protocol OAuth:", error); 98 - } 99 - }; 100 - 101 - const getStepRender = () => { 102 - switch (step) { 103 - case "last": 104 - return ( 105 - <> 106 - <h2 className={`${manrope.className} text-4xl`}> 107 - Congrats! 108 - </h2> 109 - <h2 className={`${manrope.className} text-4xl`}> 110 - Of those {nonMutualCount} non mutuals, 111 - </h2> 112 - <h2 className={`${manrope.className} text-4xl`}> 113 - {unfollowCount} were unfollowed! 114 - </h2> 115 - </> 116 - ); 117 - case "login": 118 - return ( 119 - <> 120 - <h1 className={`${manrope.className} text-4xl`}> 121 - ebb&follow 122 - </h1> 123 - <div className="border flex gap-2 h-12 items-center p-2 rounded-lg w-1/4"> 124 - <AtSign /> 125 - <input 126 - className="flex-1 outline-none" 127 - onChange={(e) => 128 - setAtProtoIdentifier(e.target.value) 129 - } 130 - placeholder="Enter your ATProto handle or DID" 131 - value={atProtoIdentifier} 132 - /> 133 - </div> 134 - <button 135 - className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 136 - onClick={loginToATProto} 137 - > 138 - Log In 139 - </button> 140 - </> 141 - ); 142 - case "nonMutuals": 143 - return ( 144 - <> 145 - <h2 className={`${manrope.className} text-4xl`}> 146 - Of those {followCounts} accounts you follow, 147 - </h2> 148 - <h2 className={`${manrope.className} text-4xl`}> 149 - {nonMutualCount} are nonMutuals* 150 - </h2> 151 - <h3> 152 - *There may be a discrepancy in count between the 153 - follower counts provided by the PDS and by the 154 - AppView 155 - </h3> 156 - <button 157 - className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 158 - onClick={() => setStep("permission")} 159 - > 160 - Next 161 - </button> 162 - </> 163 - ); 164 - case "permission": 165 - return ( 166 - <> 167 - <h2 className={`${manrope.className} text-4xl`}> 168 - For each of those {nonMutualCount} nonMutuals, 169 - </h2> 170 - <h2 className={`${manrope.className} text-4xl`}> 171 - ebb&follow will randomly choose to either continue 172 - following or unfollow 173 - </h2> 174 - <h2 className={`${manrope.className} text-4xl`}> 175 - No algorithm, just a coin flip 176 - </h2> 177 - <h2 className={`${manrope.className} text-4xl`}> 178 - Do you wish to continue? 179 - </h2> 180 - <div className="flex justify-around w-1/2"> 181 - <button 182 - className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/4" 183 - onClick={() => setStep("login")} 184 - > 185 - No 186 - </button> 187 - <button 188 - className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/4" 189 - onClick={ebbAndFollow} 190 - > 191 - Yes 192 - </button> 193 - </div> 194 - </> 195 - ); 196 - case "stats": 197 - return ( 198 - <> 199 - <h2 className={`${manrope.className} text-4xl`}> 200 - You follow {followCounts} accounts 201 - </h2> 202 - <button 203 - className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 204 - onClick={fetchNonMutuals} 205 - > 206 - {isCounting 207 - ? "Counting non-mutuals..." 208 - : "Count non-mutuals"} 209 - </button> 210 - </> 211 - ); 212 - } 213 - }; 214 - 215 5 return ( 216 - <div className="bg-linear-to-br flex flex-col from-[#0081A7] gap-4 items-center justify-center to-[#FDFCDC] via-[#00AFB9] h-screen w-screen"> 217 - {getStepRender()} 218 - </div> 6 + <Suspense fallback={<div>Loading...</div>}> 7 + <HomeContent /> 8 + </Suspense> 219 9 ); 220 10 }; 221 11 222 - export default Home; 12 + export default Home;
+222
components/HomeContent.tsx
··· 1 + "use client"; 2 + 3 + import { AtSign } from "lucide-react"; 4 + import { Manrope } from "next/font/google"; 5 + import { useSearchParams } from "next/navigation"; 6 + import { useEffect, useState } from "react"; 7 + 8 + const manrope = Manrope({ 9 + preload: true, 10 + subsets: ["latin"], 11 + weight: ["300"] 12 + }); 13 + 14 + const HomeContent = () => { 15 + const searchParams = useSearchParams(); 16 + 17 + const [atProtoIdentifier, setAtProtoIdentifier] = useState(""); 18 + const [followCounts, setFollowCounts] = useState<number>(0); 19 + const [isCounting, setIsCounting] = useState(false); 20 + const [nonMutualCount, setNonMutualCount] = useState<null | number>(null); 21 + const [step, setStep] = useState< 22 + "last" | "login" | "nonMutuals" | "permission" | "stats" 23 + >("login"); 24 + const [unfollowCount, setUnfollowCount] = useState(0); 25 + 26 + useEffect(() => { 27 + const authStatus = searchParams.get("auth_status"); 28 + 29 + if (authStatus === "success") { 30 + const follows = searchParams.get("follows"); 31 + 32 + setFollowCounts(Number(follows)); 33 + setStep("stats"); 34 + 35 + window.history.replaceState(null, "", window.location.pathname); 36 + } 37 + }, [searchParams]); 38 + 39 + const ebbAndFollow = async () => { 40 + try { 41 + const response = await fetch("/api/ebbAndFollow/", { 42 + credentials: "include", 43 + headers: { "Content-Type": "application/json" }, 44 + method: "POST" 45 + }); 46 + const data = await response.json(); 47 + 48 + if (response.ok && data.success) { 49 + setUnfollowCount(data.unfollowCount); 50 + setStep("last"); 51 + } 52 + } catch (error) {} 53 + }; 54 + 55 + const fetchNonMutuals = async () => { 56 + setNonMutualCount(null); 57 + setIsCounting(true); 58 + 59 + try { 60 + const response = await fetch("/api/ebbAndFollow/count", { 61 + credentials: "include" 62 + }); 63 + const data = await response.json(); 64 + 65 + if (response.ok && data.success) { 66 + setNonMutualCount(data.nonMutualCount); 67 + setStep("nonMutuals"); 68 + } else { 69 + alert(`Failed to fetch non-mutual count: ${data.error}`); 70 + } 71 + } catch (error) { 72 + alert("A network error occurred while calculating the ebb."); 73 + } finally { 74 + setIsCounting(false); 75 + } 76 + }; 77 + 78 + const loginToATProto = async () => { 79 + console.log(atProtoIdentifier); 80 + 81 + try { 82 + const response = await fetch("/api/auth/atproto/login", { 83 + body: JSON.stringify({ 84 + atProtoIdentifier 85 + }), 86 + headers: { "Content-Type": "application/json" }, 87 + method: "POST" 88 + }); 89 + const data = await response.json(); 90 + 91 + if (response.ok && data.url) { 92 + window.location.href = data.url; 93 + } else { 94 + console.error("Login initiation failed:", data.error); 95 + } 96 + } catch (error) { 97 + console.error("Error initiating AT Protocol OAuth:", error); 98 + } 99 + }; 100 + 101 + const getStepRender = () => { 102 + switch (step) { 103 + case "last": 104 + return ( 105 + <> 106 + <h2 className={`${manrope.className} text-4xl`}> 107 + Congrats! 108 + </h2> 109 + <h2 className={`${manrope.className} text-4xl`}> 110 + Of those {nonMutualCount} non mutuals, 111 + </h2> 112 + <h2 className={`${manrope.className} text-4xl`}> 113 + {unfollowCount} were unfollowed! 114 + </h2> 115 + </> 116 + ); 117 + case "login": 118 + return ( 119 + <> 120 + <h1 className={`${manrope.className} text-4xl`}> 121 + ebb&follow 122 + </h1> 123 + <div className="border flex gap-2 h-12 items-center p-2 rounded-lg w-1/4"> 124 + <AtSign /> 125 + <input 126 + className="flex-1 outline-none" 127 + onChange={(e) => 128 + setAtProtoIdentifier(e.target.value) 129 + } 130 + placeholder="Enter your ATProto handle or DID" 131 + value={atProtoIdentifier} 132 + /> 133 + </div> 134 + <button 135 + className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 136 + onClick={loginToATProto} 137 + > 138 + Log In 139 + </button> 140 + </> 141 + ); 142 + case "nonMutuals": 143 + return ( 144 + <> 145 + <h2 className={`${manrope.className} text-4xl`}> 146 + Of those {followCounts} accounts you follow, 147 + </h2> 148 + <h2 className={`${manrope.className} text-4xl`}> 149 + {nonMutualCount} are nonMutuals* 150 + </h2> 151 + <h3> 152 + *There may be a discrepancy in count between the 153 + follower counts provided by the PDS and by the 154 + AppView 155 + </h3> 156 + <button 157 + className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 158 + onClick={() => setStep("permission")} 159 + > 160 + Next 161 + </button> 162 + </> 163 + ); 164 + case "permission": 165 + return ( 166 + <> 167 + <h2 className={`${manrope.className} text-4xl`}> 168 + For each of those {nonMutualCount} nonMutuals, 169 + </h2> 170 + <h2 className={`${manrope.className} text-4xl`}> 171 + ebb&follow will randomly choose to either continue 172 + following or unfollow 173 + </h2> 174 + <h2 className={`${manrope.className} text-4xl`}> 175 + No algorithm, just a coin flip 176 + </h2> 177 + <h2 className={`${manrope.className} text-4xl`}> 178 + Do you wish to continue? 179 + </h2> 180 + <div className="flex justify-around w-1/2"> 181 + <button 182 + className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/4" 183 + onClick={() => setStep("login")} 184 + > 185 + No 186 + </button> 187 + <button 188 + className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/4" 189 + onClick={ebbAndFollow} 190 + > 191 + Yes 192 + </button> 193 + </div> 194 + </> 195 + ); 196 + case "stats": 197 + return ( 198 + <> 199 + <h2 className={`${manrope.className} text-4xl`}> 200 + You follow {followCounts} accounts 201 + </h2> 202 + <button 203 + className="bg-white/90 active:bg-white hover:cursor-pointer h-12 rounded-lg text-xl text-[#00AFB9] w-1/8" 204 + onClick={fetchNonMutuals} 205 + > 206 + {isCounting 207 + ? "Counting non-mutuals..." 208 + : "Count non-mutuals"} 209 + </button> 210 + </> 211 + ); 212 + } 213 + }; 214 + 215 + return ( 216 + <div className="bg-linear-to-br flex flex-col from-[#0081A7] gap-4 items-center justify-center to-[#FDFCDC] via-[#00AFB9] h-screen w-screen"> 217 + {getStepRender()} 218 + </div> 219 + ); 220 + }; 221 + 222 + export default HomeContent;