Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/AppwriteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectUpvote>("main", "projectUpvotes", [
Query.limit(100),
Query.equal("userId", userId),
Query.orderDesc("$createdAt"),
])
await databases.listDocuments<ProjectUpvote>(
"main",
"projectUpvotes",
queries
)
).documents;
},
uploadThumbnail: async (file: File) => {
Expand Down
45 changes: 19 additions & 26 deletions src/components/blocks/Upvote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -72,14 +65,14 @@ export default component$((props: Props) => {
<button
preventdefault:click
onClick$={upvoteProject}
class={`button upvote-button ${props.inline ? "" : "vertical-button"} ${
class={`button upvote-button ${
isUpvoted.value ? "is-primary" : "is-secondary"
}`}
aria-label="Upvote"
aria-pressed={isUpvoted.value}
>
<span class="icon-heart" aria-hidden="true"></span>
<span class="text">{votes.value}</span>
<span class="text">{count.value}</span>
</button>
);
});
34 changes: 34 additions & 0 deletions src/components/hooks/useUpvotes.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>) {
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;
});
});
}
2 changes: 1 addition & 1 deletion src/components/layout/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default component$((props: { project: Project | null }) => {
{project.name}
</p>
</Link>
<Upvote projectId={project.$id} votes={project.upvotes} inline />
<Upvote projectId={project.$id} votes={project.upvotes} />
</div>
<Link href={`/projects/${project.$id}`}>
<p
Expand Down
6 changes: 1 addition & 5 deletions src/components/layout/ProjectFeatured.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ export default component$((props: { project: Project }) => {
>
{project.name}
</Link>
<Upvote
projectId={project.$id}
inline={true}
votes={project.upvotes}
/>
<Upvote projectId={project.$id} votes={project.upvotes} />
</div>

<div class="u-stretch">
Expand Down
32 changes: 28 additions & 4 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
component$,
useComputed$,
useContext,
useSignal,
useVisibleTask$,
Expand All @@ -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 [
Expand Down Expand Up @@ -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<Project[] | undefined>([]);
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)
),
]);
});
Expand Down
22 changes: 8 additions & 14 deletions src/routes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Signal<ProjectUpvote[]>>(
"app.upvotes-context"
);

export const AccountContext = createContextId<Signal<null | Models.User<any>>>(
"app.account-context"
);

export const UpvotesContext = createContextId<Record<string, boolean>>(
"app.upvotes-context"
);

export const ThemeContext =
createContextId<Signal<"light" | "dark">>("app.theme-context");

Expand All @@ -51,6 +50,9 @@ export default component$(() => {
const account = useSignal<null | Models.User<any>>(null);
useContextProvider(AccountContext, account);

const upvotes = useStore<Record<string, boolean>>({}, { deep: true });
useContextProvider(UpvotesContext, upvotes);

const themeData = useThemeLoader();
const theme = useSignal(themeData.value ?? "dark");
useContextProvider(ThemeContext, theme);
Expand All @@ -71,12 +73,8 @@ export default component$(() => {
document.documentElement.style.overflow = "auto";
}),
});

useContextProvider(SearchModalContext, searchModal);

const upvotes = useSignal<ProjectUpvote[]>([]);
useContextProvider(UpvotesContext, upvotes);

const searchModalRef = useSignal<HTMLDialogElement>();
const onKeyDown = $((e: QwikKeyboardEvent) => {
if (e.key === "Escape") {
Expand All @@ -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<string | null>(null);
Expand Down
8 changes: 6 additions & 2 deletions src/routes/projects/[projectId]/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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") ?? "[]"
Expand All @@ -117,7 +121,7 @@ export default component$(() => {

<div class="u-flex u-gap-16 u-cross-center">
<h2 class="heading-level-2">{project.name}</h2>
<Upvote projectId={project.$id} votes={project.upvotes} inline />
<Upvote projectId={project.$id} votes={project.upvotes} />
</div>

<p style="font-size: 1.2rem; margin-top: -1rem;">{project.tagline}</p>
Expand Down
16 changes: 15 additions & 1 deletion src/routes/search/index.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<string | null>(
Expand Down