diff --git a/src/AppwriteService.ts b/src/AppwriteService.ts index d39a8b5..e049a9d 100644 --- a/src/AppwriteService.ts +++ b/src/AppwriteService.ts @@ -187,13 +187,20 @@ export const AppwriteService = { ) ).documents; }, - listUserUpvotes: async (userId: string) => { + listUserUpvotes: async (userId: string, queries: string[] = []) => { + const defaultQueries = [ + Query.equal("userId", userId), + Query.orderDesc("$createdAt"), + ]; + + queries = [...queries, ...defaultQueries]; + return ( - await databases.listDocuments("main", "projectUpvotes", [ - Query.limit(100), - Query.equal("userId", userId), - Query.orderDesc("$createdAt"), - ]) + await databases.listDocuments( + "main", + "projectUpvotes", + queries + ) ).documents; }, uploadThumbnail: async (file: File) => { diff --git a/src/components/blocks/Upvote.tsx b/src/components/blocks/Upvote.tsx index 1728a7f..120c530 100644 --- a/src/components/blocks/Upvote.tsx +++ b/src/components/blocks/Upvote.tsx @@ -6,62 +6,55 @@ import { useContext, useSignal, } from "@builder.io/qwik"; -import { AppwriteException } from "appwrite"; import { AppwriteService } from "~/AppwriteService"; import { AccountContext, UpvotesContext } from "~/routes/layout"; type Props = { projectId: string; votes: number; - inline?: boolean; }; export default component$((props: Props) => { - const upvoteContext = useContext(UpvotesContext); - const accountContext = useContext(AccountContext); + const upvotes = useContext(UpvotesContext); + const account = useContext(AccountContext); const isLoading = useSignal(false); const isUpvotedServer = useComputed$(() => { - return !!upvoteContext.value.find( - (upvote) => upvote.projectId === props.projectId - ); + return upvotes[props.projectId]; }); const isUpvotedClient = useSignal(isUpvotedServer.value); const isUpvoted = useComputed$(() => { return isLoading.value ? isUpvotedClient.value : isUpvotedServer.value; }); - const votesServer = useSignal(props.votes); - const votes = useComputed$(() => { + const countServer = useSignal(props.votes); + const count = useComputed$(() => { if (isLoading.value) { return isUpvotedClient.value - ? votesServer.value + 1 - : votesServer.value - 1; + ? countServer.value + 1 + : countServer.value - 1; } - return votesServer.value; + return countServer.value; }); const upvoteProject = $(async (e: QwikMouseEvent) => { e.stopPropagation(); + if (!account.value) { + alert("Please sign in first."); + return; + } + isUpvotedClient.value = !isUpvotedServer.value; isLoading.value = true; try { - const res = await AppwriteService.upvoteProject(props.projectId); - if (accountContext.value) { - upvoteContext.value = await AppwriteService.listUserUpvotes( - accountContext.value.$id - ); - } - votesServer.value = res.votes; + const response = await AppwriteService.upvoteProject(props.projectId); + upvotes[props.projectId] = response.isUpvoted; + countServer.value = response.votes; } catch (error: unknown) { - if (error instanceof AppwriteException && error.code === 401) { - alert("Please sign in first."); - } else { - alert("An unexpected error occurred."); - } + alert("An unexpected error occurred."); } finally { isUpvotedClient.value = isUpvotedServer.value; isLoading.value = false; @@ -72,14 +65,14 @@ export default component$((props: Props) => { ); }); diff --git a/src/components/hooks/useUpvotes.ts b/src/components/hooks/useUpvotes.ts new file mode 100644 index 0000000..61fbf5c --- /dev/null +++ b/src/components/hooks/useUpvotes.ts @@ -0,0 +1,34 @@ +import type { Signal } from "@builder.io/qwik"; +import { useContext, useVisibleTask$ } from "@builder.io/qwik"; +import { Query } from "appwrite"; +import { AppwriteService } from "~/AppwriteService"; +import { AccountContext, UpvotesContext } from "~/routes/layout"; + +export function useUpvotes(projectIds: Signal) { + const upvotes = useContext(UpvotesContext); + const account = useContext(AccountContext); + + useVisibleTask$(async ({ track }) => { + track(() => [account.value, projectIds.value]); + if (!account.value) { + return; + } + + const newProjectIds = projectIds.value.filter( + (projectId) => upvotes[projectId] === undefined + ); + + if (newProjectIds.length === 0) { + return; + } + + const projectUpvotes = await AppwriteService.listUserUpvotes( + account.value.$id, + [Query.equal("projectId", newProjectIds)] + ); + + projectUpvotes.forEach((upvote) => { + upvotes[upvote.projectId] = true; + }); + }); +} diff --git a/src/components/layout/Project.tsx b/src/components/layout/Project.tsx index ecfe49f..d0c2ac1 100644 --- a/src/components/layout/Project.tsx +++ b/src/components/layout/Project.tsx @@ -49,7 +49,7 @@ export default component$((props: { project: Project | null }) => { {project.name}

- +

{ > {project.name} - +

diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 7bb989f..b3859bf 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,6 @@ import { component$, + useComputed$, useContext, useSignal, useVisibleTask$, @@ -15,7 +16,7 @@ import type { Project } from "~/AppwriteService"; import { AppwriteService } from "~/AppwriteService"; import { Query } from "appwrite"; import { AccountContext } from "./layout"; -import { UpvotesContext } from "./layout"; +import { useUpvotes } from "~/components/hooks/useUpvotes"; export const useHomeData = routeLoader$(async () => { const [ @@ -91,17 +92,40 @@ export const head: DocumentHead = () => ({ export default component$(() => { const account = useContext(AccountContext); - const upvotes = useContext(UpvotesContext); const homeDataSignal = useHomeData(); const homeData = homeDataSignal.value; + const allProjects = [ + ...(homeData.featured ? [homeData.featured] : []), + ...(homeData.newAndShiny ?? []), + ...(homeData.trendZone ?? []), + ...(homeData.madeWithTailwind ?? []), + ]; + + const projectIds = useComputed$(() => + allProjects.map((project) => project.$id) + ); + useUpvotes(projectIds); + + useVisibleTask$(async () => { + account.value = await AppwriteService.getAccount(); + }); + const yourPicks = useSignal([]); useVisibleTask$(async () => { - if (!account.value || upvotes.value.length === 0) return; + if (!account.value) return; + + const lastUpvotes = await AppwriteService.listUserUpvotes( + account.value.$id, + [Query.limit(3)] + ); + + if (lastUpvotes.length === 0) return; + yourPicks.value = await AppwriteService.listProjects([ Query.equal( "$id", - upvotes.value.slice(0, 3).map((upvote) => upvote.projectId) + lastUpvotes.map((upvote) => upvote.projectId) ), ]); }); diff --git a/src/routes/layout.tsx b/src/routes/layout.tsx index 879f68f..dfe24a0 100644 --- a/src/routes/layout.tsx +++ b/src/routes/layout.tsx @@ -13,22 +13,21 @@ import { useTask$, } from "@builder.io/qwik"; import { routeLoader$, useLocation, useNavigate } from "@builder.io/qwik-city"; -import type { Models } from "appwrite"; -import type { ProjectUpvote } from "~/AppwriteService"; +import { type Models } from "appwrite"; import { AppwriteService } from "~/AppwriteService"; import { Config } from "~/Config"; import Search from "~/components/blocks/Search"; import Footer from "~/components/layout/Footer"; import Header from "~/components/layout/Header"; -export const UpvotesContext = createContextId>( - "app.upvotes-context" -); - export const AccountContext = createContextId>>( "app.account-context" ); +export const UpvotesContext = createContextId>( + "app.upvotes-context" +); + export const ThemeContext = createContextId>("app.theme-context"); @@ -51,6 +50,9 @@ export default component$(() => { const account = useSignal>(null); useContextProvider(AccountContext, account); + const upvotes = useStore>({}, { deep: true }); + useContextProvider(UpvotesContext, upvotes); + const themeData = useThemeLoader(); const theme = useSignal(themeData.value ?? "dark"); useContextProvider(ThemeContext, theme); @@ -71,12 +73,8 @@ export default component$(() => { document.documentElement.style.overflow = "auto"; }), }); - useContextProvider(SearchModalContext, searchModal); - const upvotes = useSignal([]); - useContextProvider(UpvotesContext, upvotes); - const searchModalRef = useSignal(); const onKeyDown = $((e: QwikKeyboardEvent) => { if (e.key === "Escape") { @@ -94,10 +92,6 @@ export default component$(() => { useVisibleTask$(async () => { account.value = await AppwriteService.getAccount(); - - if (account.value) { - upvotes.value = await AppwriteService.listUserUpvotes(account.value.$id); - } }); const openedFilter = useSignal(null); diff --git a/src/routes/projects/[projectId]/index.tsx b/src/routes/projects/[projectId]/index.tsx index 564b5bb..cae7020 100644 --- a/src/routes/projects/[projectId]/index.tsx +++ b/src/routes/projects/[projectId]/index.tsx @@ -1,4 +1,4 @@ -import { component$, useVisibleTask$ } from "@builder.io/qwik"; +import { component$, useComputed$, useVisibleTask$ } from "@builder.io/qwik"; import { AppwriteService } from "~/AppwriteService"; import type { DocumentHead } from "@builder.io/qwik-city"; import { routeLoader$, Link } from "@builder.io/qwik-city"; @@ -7,6 +7,7 @@ import ProjectTags from "~/components/layout/ProjectTags"; import Upvote from "~/components/blocks/Upvote"; import Socials from "~/components/blocks/Socials"; import { AppwriteException } from "appwrite"; +import { useUpvotes } from "~/components/hooks/useUpvotes"; export const useProjectData = routeLoader$(async ({ params, status }) => { try { @@ -91,6 +92,9 @@ export default component$(() => { headerIds: false, }); + const projectIds = useComputed$(() => [project.$id]); + useUpvotes(projectIds); + useVisibleTask$(async () => { const visitedProjects = JSON.parse( localStorage.getItem("visitedProjects") ?? "[]" @@ -117,7 +121,7 @@ export default component$(() => {

{project.name}

- +

{project.tagline}

diff --git a/src/routes/search/index.tsx b/src/routes/search/index.tsx index 0ee4443..da9c94c 100644 --- a/src/routes/search/index.tsx +++ b/src/routes/search/index.tsx @@ -1,9 +1,16 @@ -import { $, component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"; +import { + $, + component$, + useComputed$, + useSignal, + useVisibleTask$, +} from "@builder.io/qwik"; import type { DocumentHead } from "@builder.io/qwik-city"; import { Link } from "@builder.io/qwik-city"; import { routeLoader$, useLocation } from "@builder.io/qwik-city"; import { Query } from "appwrite"; import { AppwriteService } from "~/AppwriteService"; +import { useUpvotes } from "~/components/hooks/useUpvotes"; import Group from "~/components/layout/Group"; import ProjectFeatured from "~/components/layout/ProjectFeatured"; @@ -126,6 +133,13 @@ export default component$(() => { const searchData = useSearchData(); const projects = useSignal(searchData.value.projects); + + const projectIds = useComputed$(() => + projects.value.map((project) => project.$id) + ); + + useUpvotes(projectIds); + const location = useLocation(); const lastId = useSignal(