diff --git a/app/components/icons/Lock.tsx b/app/components/icons/Lock.tsx
new file mode 100644
index 000000000..5f03d3fa6
--- /dev/null
+++ b/app/components/icons/Lock.tsx
@@ -0,0 +1,18 @@
+export function LockIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/app/db/models/builds/buildsByUserId.sql b/app/db/models/builds/buildsByUserId.sql
index b3dc802c5..d0ca7a36c 100644
--- a/app/db/models/builds/buildsByUserId.sql
+++ b/app/db/models/builds/buildsByUserId.sql
@@ -22,6 +22,7 @@ with "Top500Weapon" as (
"clothesGearSplId",
"shoesGearSplId",
"updatedAt",
+ "private",
json_group_array(
json_object(
'weaponSplId',
@@ -37,6 +38,10 @@ with "Top500Weapon" as (
left join "Top500Weapon" on "Top500Weapon"."buildId" = "Build"."id"
where
"Build"."ownerId" = @userId
+ and (
+ "Build"."private" = 0
+ or "Build"."ownerId" = @loggedInUserId
+ )
group by
"Build"."id"
)
diff --git a/app/db/models/builds/buildsByWeaponId.sql b/app/db/models/builds/buildsByWeaponId.sql
index 81c3cf2bd..e8850207f 100644
--- a/app/db/models/builds/buildsByWeaponId.sql
+++ b/app/db/models/builds/buildsByWeaponId.sql
@@ -36,8 +36,11 @@ with "Top500Weapon" as (
"Build"
left join "Top500Weapon" on "Top500Weapon"."buildId" = "Build"."id"
where
- "Top500Weapon"."weaponSplId" = @weaponId
- or "Top500Weapon"."weaponSplId" = @altWeaponId
+ (
+ "Top500Weapon"."weaponSplId" = @weaponId
+ or "Top500Weapon"."weaponSplId" = @altWeaponId
+ )
+ and "Build"."private" = 0
group by
"Build"."id"
),
diff --git a/app/db/models/builds/createBuild.sql b/app/db/models/builds/createBuild.sql
index 2d9ec99fd..b71db65f3 100644
--- a/app/db/models/builds/createBuild.sql
+++ b/app/db/models/builds/createBuild.sql
@@ -6,7 +6,8 @@ insert into
"modes",
"headGearSplId",
"clothesGearSplId",
- "shoesGearSplId"
+ "shoesGearSplId",
+ "private"
)
values
(
@@ -16,5 +17,6 @@ values
@modes,
@headGearSplId,
@clothesGearSplId,
- @shoesGearSplId
- ) returning *
\ No newline at end of file
+ @shoesGearSplId,
+ @private
+ ) returning *
diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts
index 330fe958c..70dcec72c 100644
--- a/app/db/models/builds/queries.server.ts
+++ b/app/db/models/builds/queries.server.ts
@@ -40,6 +40,7 @@ interface CreateArgs {
shoesGearSplId: Build["shoesGearSplId"];
weaponSplIds: Array
;
abilities: BuildAbilitiesTuple;
+ private: Build["private"];
}
export const create = sql.transaction((build: CreateArgs) => {
const createdBuild = createBuildStm.get({
@@ -57,6 +58,7 @@ export const create = sql.transaction((build: CreateArgs) => {
headGearSplId: build.headGearSplId,
clothesGearSplId: build.clothesGearSplId,
shoesGearSplId: build.shoesGearSplId,
+ private: build.private,
}) as Build;
for (const weaponSplId of build.weaponSplIds) {
@@ -108,12 +110,22 @@ type BuildsByUserRow = Pick<
| "clothesGearSplId"
| "shoesGearSplId"
| "updatedAt"
+ | "private"
> & {
weapons: string;
abilities: string;
};
-export function buildsByUserId(userId: Build["ownerId"]) {
- const rows = buildsByUserIdStm.all({ userId }) as Array;
+export function buildsByUserId({
+ userId,
+ loggedInUserId,
+}: {
+ userId: Build["ownerId"];
+ loggedInUserId?: UserWithPlusTier["id"];
+}) {
+ const rows = buildsByUserIdStm.all({
+ userId,
+ loggedInUserId,
+ }) as Array;
return rows.map(augmentBuild);
}
diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts
index 77bff4f7c..c8ac0531d 100644
--- a/app/db/seed/index.ts
+++ b/app/db/seed/index.ts
@@ -834,6 +834,7 @@ function adminBuilds() {
faker.word.noun()
)}`,
ownerId: 1,
+ private: 0,
description: Math.random() < 0.75 ? faker.lorem.paragraph() : null,
headGearSplId: randomOrderHeadGear[0]!,
clothesGearSplId: randomOrderClothesGear[0]!,
@@ -889,6 +890,7 @@ function manySplattershotBuilds() {
);
db.builds.create({
+ private: 0,
title: `${capitalize(faker.word.adjective())} ${capitalize(
faker.word.noun()
)}`,
diff --git a/app/db/types.ts b/app/db/types.ts
index 2981a2ffa..90a491a94 100644
--- a/app/db/types.ts
+++ b/app/db/types.ts
@@ -151,6 +151,8 @@ export interface Build {
id: number;
ownerId: number;
title: string;
+ /** Private builds are only visible on the user builds page to the owner only */
+ private: number;
description: string | null;
modes: string | null;
headGearSplId: number;
diff --git a/app/features/build-stats/queries/abilitiesByWeaponId.server.ts b/app/features/build-stats/queries/abilitiesByWeaponId.server.ts
index 118e61d51..4bd207bf3 100644
--- a/app/features/build-stats/queries/abilitiesByWeaponId.server.ts
+++ b/app/features/build-stats/queries/abilitiesByWeaponId.server.ts
@@ -1,6 +1,7 @@
import { sql } from "~/db/sql";
import type { Ability, MainWeaponId } from "~/modules/in-game-lists";
+// TODO: could consider removing private builds from this
const stm = sql.prepare(/* sql */ `
with "GroupedAbilities" as (
select
diff --git a/app/features/build-stats/queries/averageAbilityPoints.server.ts b/app/features/build-stats/queries/averageAbilityPoints.server.ts
index e428a7462..a3acd7471 100644
--- a/app/features/build-stats/queries/averageAbilityPoints.server.ts
+++ b/app/features/build-stats/queries/averageAbilityPoints.server.ts
@@ -1,6 +1,7 @@
import { sql } from "~/db/sql";
import type { Ability, MainWeaponId } from "~/modules/in-game-lists";
+// TODO: could consider removing private builds from this
const query = (includeWeaponId: boolean) => /* sql */ `
select "BuildAbility"."ability", sum("BuildAbility"."abilityPoints") as "abilityPointsSum"
from "BuildAbility"
diff --git a/app/routes/u.$identifier/builds/index.tsx b/app/routes/u.$identifier/builds/index.tsx
index fcedd73d5..b9f9c07a7 100644
--- a/app/routes/u.$identifier/builds/index.tsx
+++ b/app/routes/u.$identifier/builds/index.tsx
@@ -32,7 +32,7 @@ export const action: ActionFunction = async ({ request }) => {
if (
!db.builds
- .buildsByUserId(user.id)
+ .buildsByUserId({ userId: user.id, loggedInUserId: user?.id })
.some((build) => build.id === data.buildToDeleteId)
) {
throw new Response(null, { status: 400 });
@@ -52,7 +52,10 @@ export const loader = async ({ params, request }: LoaderArgs) => {
const { identifier } = userParamsSchema.parse(params);
const user = notFoundIfFalsy(db.users.findByIdentifier(identifier));
- const builds = db.builds.buildsByUserId(user.id);
+ const builds = db.builds.buildsByUserId({
+ userId: user.id,
+ loggedInUserId: loggedInUser?.id,
+ });
if (builds.length === 0 && loggedInUser?.id !== user.id) {
throw new Response(null, { status: 404 });
diff --git a/app/routes/u.$identifier/builds/new.tsx b/app/routes/u.$identifier/builds/new.tsx
index 1ea8940e5..b52bddee5 100644
--- a/app/routes/u.$identifier/builds/new.tsx
+++ b/app/routes/u.$identifier/builds/new.tsx
@@ -10,6 +10,7 @@ import { z } from "zod";
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
import { Button } from "~/components/Button";
import { GearCombobox, WeaponCombobox } from "~/components/Combobox";
+import { FormMessage } from "~/components/FormMessage";
import { CrossIcon } from "~/components/icons/Cross";
import { PlusIcon } from "~/components/icons/Plus";
import { Image } from "~/components/Image";
@@ -43,7 +44,9 @@ import { modeImageUrl, userBuildsPage } from "~/utils/urls";
import {
actualNumber,
checkboxValueToBoolean,
+ checkboxValueToDbBoolean,
clothesMainSlotAbility,
+ dbBoolean,
falsyToNull,
headMainSlotAbility,
id,
@@ -68,6 +71,7 @@ const newBuildActionSchema = z.object({
TC: z.preprocess(checkboxValueToBoolean, z.boolean()),
RM: z.preprocess(checkboxValueToBoolean, z.boolean()),
CB: z.preprocess(checkboxValueToBoolean, z.boolean()),
+ private: z.preprocess(checkboxValueToDbBoolean, dbBoolean),
"weapon[value]": z.preprocess(
processMany(toArray, removeDuplicates),
z.array(weaponSplId).min(1).max(BUILD.MAX_WEAPONS_COUNT)
@@ -128,7 +132,10 @@ export const action: ActionFunction = async ({ request }) => {
schema: newBuildActionSchema,
});
- const usersBuilds = db.builds.buildsByUserId(user.id);
+ const usersBuilds = db.builds.buildsByUserId({
+ userId: user.id,
+ loggedInUserId: user.id,
+ });
if (usersBuilds.length >= BUILD.MAX_COUNT) {
throw new Response(null, { status: 400 });
}
@@ -143,6 +150,7 @@ export const action: ActionFunction = async ({ request }) => {
modes: modesShort.filter((mode) => data[mode]),
weaponSplIds: data["weapon[value]"] as Array,
ownerId: user.id,
+ private: data.private,
};
if (data.buildToEditId) {
db.builds.updateByReplacing({ id: data.buildToEditId, ...commonArgs });
@@ -174,7 +182,10 @@ export const loader = async ({ request }: LoaderArgs) => {
return json({ buildToEdit: null });
}
- const usersBuilds = db.builds.buildsByUserId(params.data.userId);
+ const usersBuilds = db.builds.buildsByUserId({
+ userId: params.data.userId,
+ loggedInUserId: user.id,
+ });
const buildToEdit = usersBuilds.find((b) => b.id === params.data.buildId);
return json({
@@ -200,6 +211,7 @@ export default function NewBuildPage() {
+
{t("actions.submit")}
@@ -282,6 +294,26 @@ function ModeCheckboxes() {
);
}
+function PrivateCheckbox() {
+ const { buildToEdit } = useLoaderData