diff --git a/app/components/MobileNav.module.css b/app/components/MobileNav.module.css index dd0712b97..892bc395b 100644 --- a/app/components/MobileNav.module.css +++ b/app/components/MobileNav.module.css @@ -173,7 +173,6 @@ align-items: center; gap: var(--s-2); padding-inline: var(--s-4); - padding-block-start: env(safe-area-inset-top); background-color: var(--color-bg-high); border-bottom: 1.5px solid var(--color-border); border-top: 1.5px solid var(--color-border); @@ -181,8 +180,9 @@ color: var(--color-text-high); min-height: var(--layout-nav-height); - &:has(+ nav) { + &:first-child { border-top: none; + padding-block-start: env(safe-area-inset-top); } } diff --git a/app/components/MobileNav.tsx b/app/components/MobileNav.tsx index 7b9a377a8..dc3d94563 100644 --- a/app/components/MobileNav.tsx +++ b/app/components/MobileNav.tsx @@ -582,8 +582,7 @@ function ChatPanel({ > diff --git a/app/components/layout/ChatSidebar.module.css b/app/components/layout/ChatSidebar.module.css index b57b10a03..65f56537e 100644 --- a/app/components/layout/ChatSidebar.module.css +++ b/app/components/layout/ChatSidebar.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; height: 100%; + max-height: var(--visual-viewport-height); overflow: hidden; } diff --git a/app/components/layout/GlobalSearch.tsx b/app/components/layout/GlobalSearch.tsx index d80b9dc29..58ab61b86 100644 --- a/app/components/layout/GlobalSearch.tsx +++ b/app/components/layout/GlobalSearch.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import type { TFunction } from "i18next"; import { Search } from "lucide-react"; import * as React from "react"; import { @@ -18,6 +19,7 @@ import { Avatar } from "~/components/Avatar"; import { Image } from "~/components/Image"; import { Input } from "~/components/Input"; import type { SearchLoaderData } from "~/features/search/routes/search"; +import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { mySlugify, navIconUrl, @@ -83,6 +85,7 @@ export function GlobalSearch() { const searchParamOpen = searchParams.get("search") === "open"; const searchParamType = searchParams.get("type"); + const searchParamWeapon = searchParams.get("weapon"); const initialSearchType = searchParamType && SEARCH_TYPES.includes(searchParamType as SearchType) ? (searchParamType as SearchType) @@ -91,10 +94,10 @@ export function GlobalSearch() { const [isOpen, setIsOpen] = React.useState(searchParamOpen); React.useEffect(() => { - if (searchParamOpen && !isOpen) { + if (searchParamOpen) { setIsOpen(true); } - }, [searchParamOpen, isOpen]); + }, [searchParamOpen]); React.useEffect(() => { setIsMac(/Mac|iPhone|iPad|iPod/.test(navigator.userAgent)); @@ -115,10 +118,11 @@ export function GlobalSearch() { const handleOpenChange = (open: boolean) => { setIsOpen(open); - if (!open && (searchParamOpen || searchParamType)) { + if (!open && (searchParamOpen || searchParamType || searchParamWeapon)) { const newParams = new URLSearchParams(searchParams); newParams.delete("search"); newParams.delete("type"); + newParams.delete("weapon"); setSearchParams(newParams, { replace: true }); } }; @@ -138,8 +142,9 @@ export function GlobalSearch() { handleOpenChange(false)} + onClose={() => setIsOpen(false)} initialSearchType={initialSearchType} + initialWeaponId={searchParamWeapon} /> @@ -148,12 +153,26 @@ export function GlobalSearch() { ); } +function resolveInitialWeapon( + weaponIdStr: string | null, + t: TFunction<["common", "weapons"]>, +): SelectedWeapon | null { + if (!weaponIdStr) return null; + const id = Number(weaponIdStr) as MainWeaponId; + if (Number.isNaN(id)) return null; + const name = t(`weapons:MAIN_${id}`); + if (!name || name === `MAIN_${id}`) return null; + return { id, name, slug: mySlugify(name) }; +} + function GlobalSearchContent({ onClose, initialSearchType, + initialWeaponId, }: { onClose: () => void; initialSearchType: SearchType | null; + initialWeaponId: string | null; }) { const { t } = useTranslation(["common", "weapons"]); const navigate = useNavigate(); @@ -162,7 +181,9 @@ function GlobalSearchContent({ initialSearchType ?? getInitialSearchType(), ); const [selectedWeapon, setSelectedWeapon] = - React.useState(null); + React.useState( + resolveInitialWeapon(initialWeaponId, t), + ); const inputRef = React.useRef(null); const listBoxRef = React.useRef(null); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 4a4fda0fd..912966c21 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -14,7 +14,6 @@ import { daily, everyHourAt00, everyHourAt30, - everyTwoHours, everyTwoMinutes, } from "./routines/list.server"; import { logger } from "./utils/logger"; @@ -112,12 +111,6 @@ if (!global.appStartSignal && process.env.NODE_ENV === "production") { } }); - cron.schedule("5 */2 * * *", async () => { - for (const routine of everyTwoHours) { - await routine.run(); - } - }); - cron.schedule("*/2 * * * *", async () => { for (const routine of everyTwoMinutes) { await routine.run(); diff --git a/app/features/art/ArtRepository.server.test.ts b/app/features/art/ArtRepository.server.test.ts index e52699d6f..6d1940d39 100644 --- a/app/features/art/ArtRepository.server.test.ts +++ b/app/features/art/ArtRepository.server.test.ts @@ -303,6 +303,54 @@ describe("deleteById", () => { }); }); +describe("deleteOrphanTags", () => { + beforeEach(async () => { + imageCounter = 0; + await dbInsertUsers(1); + }); + + afterEach(() => { + dbReset(); + }); + + test("deletes tags with no associated art", async () => { + const art = await ArtRepository.insert({ + authorId: 1, + url: "https://example.com/image-1.png", + validatedAt: Date.now(), + description: null, + linkedUsers: [], + tags: [{ name: "Orphan1" }, { name: "Orphan2" }], + }); + + await ArtRepository.deleteById(art.id); + + const deletedCount = await ArtRepository.deleteOrphanTags(); + expect(deletedCount).toBe(2); + + const tags = await ArtRepository.findAllTags(); + expect(tags).toHaveLength(0); + }); + + test("does not delete tags that are still linked to art", async () => { + await ArtRepository.insert({ + authorId: 1, + url: "https://example.com/image-1.png", + validatedAt: Date.now(), + description: null, + linkedUsers: [], + tags: [{ name: "InUse" }], + }); + + const deletedCount = await ArtRepository.deleteOrphanTags(); + expect(deletedCount).toBe(0); + + const tags = await ArtRepository.findAllTags(); + expect(tags).toHaveLength(1); + expect(tags[0].name).toBe("InUse"); + }); +}); + describe("insert", () => { beforeEach(async () => { imageCounter = 0; diff --git a/app/features/art/ArtRepository.server.ts b/app/features/art/ArtRepository.server.ts index 7af0e21f8..9040f48b2 100644 --- a/app/features/art/ArtRepository.server.ts +++ b/app/features/art/ArtRepository.server.ts @@ -171,6 +171,15 @@ export async function findAllTags() { return db.selectFrom("ArtTag").select(["id", "name"]).execute(); } +export async function deleteOrphanTags() { + const result = await db + .deleteFrom("ArtTag") + .where("id", "not in", db.selectFrom("TaggedArt").select("TaggedArt.tagId")) + .executeTakeFirst(); + + return Number(result.numDeletedRows); +} + export async function findArtsByUserId( userId: number, { includeAuthored = true, includeTagged = true } = {}, diff --git a/app/features/art/loaders/art.server.ts b/app/features/art/loaders/art.server.ts index bb2bc76d3..d436b9ed2 100644 --- a/app/features/art/loaders/art.server.ts +++ b/app/features/art/loaders/art.server.ts @@ -26,7 +26,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ? cachedArts.allTags.find((t) => t.name === filteredTagName) : null; - if (!filteredTag) return cachedArts; + if (!filteredTag) { + return filteredTagName ? { ...cachedArts, showcaseArts: [] } : cachedArts; + } return { ...cachedArts, diff --git a/app/features/art/routes/art.tsx b/app/features/art/routes/art.tsx index 79a1810cf..b3a139b05 100644 --- a/app/features/art/routes/art.tsx +++ b/app/features/art/routes/art.tsx @@ -160,7 +160,13 @@ export default function ArtPage() { - + {filteredTag && showcaseArts.length === 0 ? ( +
+ {t("art:noArtForTag", { tag: filteredTag })} +
+ ) : ( + + )}
diff --git a/app/features/chat/ChatProvider.tsx b/app/features/chat/ChatProvider.tsx index 63f5993a8..90ff90066 100644 --- a/app/features/chat/ChatProvider.tsx +++ b/app/features/chat/ChatProvider.tsx @@ -556,55 +556,71 @@ function useChatRouteSync({ React.SetStateAction> >; }) { - const chatCode = useCurrentRouteChatCode(); + const rawChatCode = useCurrentRouteChatCode(); + const chatCodesKey = rawChatCode + ? Array.isArray(rawChatCode) + ? rawChatCode.join(",") + : rawChatCode + : ""; const { pathname } = useLocation(); const layoutSize = useLayoutSize(); - const subscribedRoomRef = React.useRef(null); - const previousRouteChatCodeRef = React.useRef(null); + const subscribedRoomRef = React.useRef([]); + const previousRouteChatCodeRef = React.useRef([]); const previousPathnameRef = React.useRef(null); React.useEffect(() => { if (isLoading) return; - const previousSubscribed = subscribedRoomRef.current; + const chatCodes = chatCodesKey ? chatCodesKey.split(",") : []; - // Clean up previous non-participant subscription if chatCode changed - if (previousSubscribed && previousSubscribed !== chatCode) { - unsubscribe(previousSubscribed); - setRooms((prev) => prev.filter((r) => r.chatCode !== previousSubscribed)); + // Clean up subscriptions for rooms no longer in chatCodes + const previousSubscribed = subscribedRoomRef.current; + const removedRooms = previousSubscribed.filter( + (code) => !chatCodes.includes(code), + ); + for (const code of removedRooms) { + unsubscribe(code); + setRooms((prev) => prev.filter((r) => r.chatCode !== code)); setMessagesByRoom((prev) => { - const { [previousSubscribed]: _, ...rest } = prev; + const { [code]: _, ...rest } = prev; return rest; }); - if (activeRoom === previousSubscribed) { + if (activeRoom === code) { setActiveRoom(null); setChatOpen(false); } - subscribedRoomRef.current = null; + } + if (removedRooms.length > 0) { + subscribedRoomRef.current = previousSubscribed.filter((code) => + chatCodes.includes(code), + ); } - if (chatCode) { - const room = rooms.find((r) => r.chatCode === chatCode); - const isParticipant = room?.participantUserIds.includes(userId); + if (chatCodes.length > 0) { + for (const code of chatCodes) { + const alreadyInRooms = rooms.some((r) => r.chatCode === code); - if (!isParticipant && subscribedRoomRef.current !== chatCode) { - logger.debug("Subscribing to non-participant room:", chatCode); - subscribe(chatCode); - subscribedRoomRef.current = chatCode; + if (!alreadyInRooms && !subscribedRoomRef.current.includes(code)) { + logger.debug("Subscribing to non-participant room:", code); + subscribe(code); + subscribedRoomRef.current = [...subscribedRoomRef.current, code]; + } } + const previousCodes = previousRouteChatCodeRef.current; const routeChatCodeChanged = - previousRouteChatCodeRef.current !== chatCode; - previousRouteChatCodeRef.current = chatCode; + chatCodes.length !== previousCodes.length || + chatCodes.some((code, i) => previousCodes[i] !== code); + previousRouteChatCodeRef.current = chatCodes; if (routeChatCodeChanged) { - setActiveRoom(chatCode); + setActiveRoom(chatCodes[0]); if (layoutSize === "desktop") { setChatOpen(true); } } } else { - previousRouteChatCodeRef.current = null; + previousRouteChatCodeRef.current = []; const pathnameChanged = previousPathnameRef.current !== pathname; previousPathnameRef.current = pathname; @@ -624,7 +640,7 @@ function useChatRouteSync({ } }, [ isLoading, - chatCode, + chatCodesKey, pathname, rooms, userId, @@ -639,11 +655,13 @@ function useChatRouteSync({ ]); } -function useCurrentRouteChatCode() { +function useCurrentRouteChatCode(): string | string[] | null { const matches = useMatches(); for (const match of matches) { - const matchData = match.data as { chatCode?: string } | undefined; + const matchData = match.data as + | { chatCode?: string | string[] } + | undefined; if (matchData?.chatCode) { return matchData.chatCode; } diff --git a/app/features/front-page/loaders/index.server.ts b/app/features/front-page/loaders/index.server.ts index ccd05188e..0d3fc74c3 100644 --- a/app/features/front-page/loaders/index.server.ts +++ b/app/features/front-page/loaders/index.server.ts @@ -1,9 +1,11 @@ import cachified from "@epic-web/cachified"; import type { Tables } from "~/db/tables"; +import { getUser } from "~/features/auth/core/user.server"; import * as Changelog from "~/features/front-page/core/Changelog.server"; import { cachedFullUserLeaderboard } from "~/features/leaderboards/core/leaderboards.server"; import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; import * as Seasons from "~/features/mmr/core/Seasons"; +import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server"; import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import type { SerializeFrom } from "~/utils/remix"; @@ -13,26 +15,35 @@ import * as ShowcaseTournaments from "../core/ShowcaseTournaments.server"; export type FrontPageLoaderData = SerializeFrom; export const loader = async () => { - const [tournaments, changelog, leaderboards, rotations] = await Promise.all([ - ShowcaseTournaments.categorizedTournamentsByUserId(null), - cachified({ - key: "front-changelog", - cache, - ttl: ttl(IN_MILLISECONDS.ONE_HOUR), - staleWhileRevalidate: ttl(IN_MILLISECONDS.TWO_HOURS), - async getFreshValue() { - return Changelog.get(); - }, - }), - cachedLeaderboards(), - SplatoonRotationRepository.findAll(), - ]); + const user = getUser(); + + const [tournaments, changelog, leaderboards, rotations, weaponPool] = + await Promise.all([ + ShowcaseTournaments.categorizedTournamentsByUserId(null), + cachified({ + key: "front-changelog", + cache, + ttl: ttl(IN_MILLISECONDS.ONE_HOUR), + staleWhileRevalidate: ttl(IN_MILLISECONDS.TWO_HOURS), + async getFreshValue() { + return Changelog.get(); + }, + }), + cachedLeaderboards(), + SplatoonRotationRepository.findAll(), + user + ? QSettingsRepository.settingsByUserId(user.id).then( + (s) => s.qWeaponPool ?? null, + ) + : Promise.resolve(null), + ]); return { tournaments, changelog, leaderboards, rotations, + weaponPool, }; }; diff --git a/app/features/front-page/routes/index.tsx b/app/features/front-page/routes/index.tsx index 8da82989c..a482954c6 100644 --- a/app/features/front-page/routes/index.tsx +++ b/app/features/front-page/routes/index.tsx @@ -4,12 +4,13 @@ import { useTranslation } from "react-i18next"; import { Link, useLoaderData } from "react-router"; import { Avatar } from "~/components/Avatar"; import { Divider } from "~/components/Divider"; -import { Image } from "~/components/Image"; +import { Image, WeaponImage } from "~/components/Image"; import { ArrowRightIcon } from "~/components/icons/ArrowRight"; import { BSKYLikeIcon } from "~/components/icons/BSKYLike"; import { BSKYReplyIcon } from "~/components/icons/BSKYReply"; import { BSKYRepostIcon } from "~/components/icons/BSKYRepost"; import { ExternalIcon } from "~/components/icons/External"; +import { navItems } from "~/components/layout/nav-items"; import { Main } from "~/components/Main"; import { TournamentCard } from "~/features/calendar/components/TournamentCard"; import { SplatoonRotations } from "~/features/front-page/components/SplatoonRotations"; @@ -41,6 +42,7 @@ export default function FrontPage() { + ); @@ -227,6 +229,61 @@ function Leaderboard({ ); } +const DISCOVER_EXCLUDED_ITEMS = new Set(["settings", "luti"]); + +function DiscoverFeatures() { + const { t } = useTranslation(["front", "common"]); + const data = useLoaderData(); + + const filteredNavItems = navItems.filter( + (item) => !DISCOVER_EXCLUDED_ITEMS.has(item.name), + ); + + return ( +
+ + {t("front:discover.header")} + + {data.weaponPool && data.weaponPool.length > 0 ? ( +
+ {data.weaponPool.map((weapon) => ( + + + + ))} +
+ ) : null} + +
+ ); +} + function ChangelogList() { const { t } = useTranslation(["front"]); const data = useLoaderData(); diff --git a/app/features/sendouq-match/loaders/q.match.$id.server.ts b/app/features/sendouq-match/loaders/q.match.$id.server.ts index 46f6347db..5482bce0c 100644 --- a/app/features/sendouq-match/loaders/q.match.$id.server.ts +++ b/app/features/sendouq-match/loaders/q.match.$id.server.ts @@ -45,15 +45,30 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { }) : null, rawReportedWeapons, - chatCode: - (user?.roles.includes("STAFF") || - (user && matchUsers.includes(user.id))) && - chatAccessible({ - isStaff: user?.roles.includes("STAFF") ?? false, + chatCode: (() => { + const isStaff = user?.roles.includes("STAFF") ?? false; + const isParticipant = user && matchUsers.includes(user.id); + + if (!(isStaff || isParticipant)) return null; + + const accessible = chatAccessible({ + isStaff, expiresAfterDays: 1, comparedTo: databaseTimestampToDate(matchUnmapped.createdAt), - }) - ? match.chatCode - : null, + }); + if (!accessible) return null; + + if (!isParticipant) return match.chatCode ?? null; + + const codes = [ + match.chatCode, + match.groupAlpha.chatCode, + match.groupBravo.chatCode, + ].filter((c): c is string => Boolean(c)); + + if (codes.length === 0) return null; + if (codes.length === 1) return codes[0]; + return codes; + })(), }; }; diff --git a/app/features/sendouq/actions/q.looking.server.ts b/app/features/sendouq/actions/q.looking.server.ts index fae8a0e0f..e6837b014 100644 --- a/app/features/sendouq/actions/q.looking.server.ts +++ b/app/features/sendouq/actions/q.looking.server.ts @@ -9,6 +9,7 @@ import { matchMapList, } from "~/features/sendouq-match/core/match.server"; import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server"; +import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server"; import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { navIconUrl, SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls"; @@ -177,6 +178,7 @@ export const action: ActionFunction = async ({ request }) => { }); await refreshSendouQInstance(); + refreshStreamsCache(); if (createdMatch.chatCode) { ChatSystemMessage.setMetadata({ diff --git a/app/features/sendouq/actions/q.preparing.server.ts b/app/features/sendouq/actions/q.preparing.server.ts index ec732d6e5..a7c4a1368 100644 --- a/app/features/sendouq/actions/q.preparing.server.ts +++ b/app/features/sendouq/actions/q.preparing.server.ts @@ -9,6 +9,7 @@ import { assertUnreachable } from "~/utils/types"; import { SENDOUQ_LOOKING_PAGE } from "~/utils/urls"; import { refreshSendouQInstance, SendouQ } from "../core/SendouQ.server"; import { preparingSchema } from "../q-schemas.server"; +import { setGroupChatMetadata } from "../q-utils.server"; export type SendouQPreparingAction = typeof action; @@ -58,6 +59,14 @@ export const action = async ({ request }: ActionFunctionArgs) => { await refreshSendouQInstance(); + const updatedGroup = SendouQ.findOwnGroup(user.id); + if (updatedGroup?.chatCode) { + setGroupChatMetadata({ + chatCode: updatedGroup.chatCode, + members: updatedGroup.members, + }); + } + notify({ userIds: [data.id], notification: { diff --git a/app/features/tournament/loaders/to.$id.register.server.ts b/app/features/tournament/loaders/to.$id.register.server.ts index 59c998359..378ce2472 100644 --- a/app/features/tournament/loaders/to.$id.register.server.ts +++ b/app/features/tournament/loaders/to.$id.register.server.ts @@ -21,7 +21,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { tournamentId, userId: user.id, }); - if (!ownTournamentTeam) + if (!ownTournamentTeam) { return { mapPool: null, friendPlayers: null, @@ -31,6 +31,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { tournamentId, }), }; + } return { mapPool: findMapPoolByTeamId(ownTournamentTeam.id), diff --git a/app/features/tournament/queries/findOwnTournamentTeam.server.ts b/app/features/tournament/queries/findOwnTournamentTeam.server.ts index 5dd5c801c..0acb04962 100644 --- a/app/features/tournament/queries/findOwnTournamentTeam.server.ts +++ b/app/features/tournament/queries/findOwnTournamentTeam.server.ts @@ -16,6 +16,7 @@ const stm = sql.prepare(/*sql*/ ` and "TournamentTeamMember"."isOwner" = 1 where "TournamentTeam"."tournamentId" = @tournamentId + and "TournamentTeam"."isPlaceholder" = 0 and "TournamentTeamMember"."userId" = @userId `); diff --git a/app/root.tsx b/app/root.tsx index 62c9717f3..1ea0bf88f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -368,7 +368,7 @@ export default function App() { const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - + htmlStyle.overflow = "initial"; bodyStyle.overflow = "hidden"; bodyStyle.paddingRight = `${scrollbarWidth}px`; diff --git a/app/routines/deleteOrphanArtTags.ts b/app/routines/deleteOrphanArtTags.ts new file mode 100644 index 000000000..f97963b86 --- /dev/null +++ b/app/routines/deleteOrphanArtTags.ts @@ -0,0 +1,11 @@ +import * as ArtRepository from "../features/art/ArtRepository.server"; +import { logger } from "../utils/logger"; +import { Routine } from "./routine.server"; + +export const DeleteOrphanArtTagsRoutine = new Routine({ + name: "DeleteOrphanArtTags", + func: async () => { + const deletedCount = await ArtRepository.deleteOrphanTags(); + logger.info(`Deleted ${deletedCount} orphan art tags`); + }, +}); diff --git a/app/routines/list.server.ts b/app/routines/list.server.ts index 594d21d4d..01198bfb4 100644 --- a/app/routines/list.server.ts +++ b/app/routines/list.server.ts @@ -1,5 +1,6 @@ import { CloseExpiredCommissionsRoutine } from "./closeExpiredCommissions"; import { DeleteOldNotificationsRoutine } from "./deleteOldNotifications"; +import { DeleteOrphanArtTagsRoutine } from "./deleteOrphanArtTags"; import { NotifyCheckInStartRoutine } from "./notifyCheckInStart"; import { NotifyPlusServerVotingRoutine } from "./notifyPlusServerVoting"; import { NotifyScrimStartingSoonRoutine } from "./notifyScrimStartingSoon"; @@ -15,6 +16,7 @@ export const everyHourAt00 = [ NotifyPlusServerVotingRoutine, NotifyCheckInStartRoutine, NotifyScrimStartingSoonRoutine, + SyncSplatoonRotationsRoutine, ]; /** List of Routines that should occur hourly at XX:30 */ @@ -27,10 +29,8 @@ export const everyHourAt30 = [ export const daily = [ DeleteOldNotificationsRoutine, CloseExpiredCommissionsRoutine, + DeleteOrphanArtTagsRoutine, ]; -/** List of Routines that should occur every 2 hours */ -export const everyTwoHours = [SyncSplatoonRotationsRoutine]; - /** List of Routines that should occur every 2 minutes */ export const everyTwoMinutes = [SyncLiveStreamsRoutine]; diff --git a/app/routines/syncSplatoonRotations.ts b/app/routines/syncSplatoonRotations.ts index a81fc0a77..556a6531c 100644 --- a/app/routines/syncSplatoonRotations.ts +++ b/app/routines/syncSplatoonRotations.ts @@ -8,8 +8,6 @@ export const SyncSplatoonRotationsRoutine = new Routine({ }); async function syncSplatoonRotations() { - if (import.meta.env.VITE_PROD_MODE !== "true") return; - const rotations = await fetchRotations(); await SplatoonRotationRepository.replaceAll(rotations); } diff --git a/app/styles/front.module.css b/app/styles/front.module.css index 5b52184f2..8fb6024b6 100644 --- a/app/styles/front.module.css +++ b/app/styles/front.module.css @@ -165,3 +165,62 @@ gap: var(--s-2); flex-wrap: wrap; } + +.weaponPills { + display: flex; + gap: var(--s-2); + justify-content: center; + flex-wrap: wrap; +} + +.weaponPill { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-bg-higher); + border-radius: var(--radius-full); + padding: var(--s-1); + transition: background-color 0.15s ease-out; +} + +.weaponPill:hover { + background-color: var(--color-bg-high); +} + +.discoverGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--s-3); + padding: var(--s-2); +} + +@media (min-width: 600px) { + .discoverGrid { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + } +} + +.discoverGridItem { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--s-1); + text-decoration: none; + color: var(--color-text); + font-size: var(--font-xs); + font-weight: var(--weight-semi); + text-align: center; +} + +.discoverGridItem:hover .discoverGridItemImage { + background-color: var(--color-bg-high); +} + +.discoverGridItemImage { + width: var(--field-size-lg); + aspect-ratio: 1 / 1; + border-radius: var(--radius-field); + background-color: var(--color-bg-higher); + display: grid; + place-items: center; +} diff --git a/locales/da/art.json b/locales/da/art.json index 23d7fff7a..cc15c1e11 100644 --- a/locales/da/art.json +++ b/locales/da/art.json @@ -29,5 +29,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Maks antal etiketter opnået", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/da/front.json b/locales/da/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/da/front.json +++ b/locales/da/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/de/art.json b/locales/de/art.json index 195c7f423..d75450428 100644 --- a/locales/de/art.json +++ b/locales/de/art.json @@ -29,5 +29,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/de/front.json b/locales/de/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/de/front.json +++ b/locales/de/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/en/art.json b/locales/en/art.json index 47b5b5573..785c750b9 100644 --- a/locales/en/art.json +++ b/locales/en/art.json @@ -29,5 +29,6 @@ "forms.tags.placeholder": "Select a tag", "forms.tags.maxReached": "Max tags reached", "delete.title": "Are you sure you want to delete the art?", - "unlink.title": "Are you sure you want to remove this art from your profile (only {{username}} can add it back)?" + "unlink.title": "Are you sure you want to remove this art from your profile (only {{username}} can add it back)?", + "noArtForTag": "No uploaded art found for #{{tag}}" } diff --git a/locales/en/front.json b/locales/en/front.json index a95e2f22a..70caa4922 100644 --- a/locales/en/front.json +++ b/locales/en/front.json @@ -38,5 +38,6 @@ "rotations.current": "Now", "rotations.nextLabel": "Next", "rotations.credit": "Data from splatoon3.ink", - "rotations.filter.all": "All" + "rotations.filter.all": "All", + "discover.header": "Discover all the features" } diff --git a/locales/es-ES/art.json b/locales/es-ES/art.json index e4e6028a9..d009261c2 100644 --- a/locales/es-ES/art.json +++ b/locales/es-ES/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "Selecciona una etiqueta", "forms.tags.maxReached": "Limite máximo de etiquetas", "delete.title": "¿Seguro que quieres eliminar este arte?", - "unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)" + "unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)", + "noArtForTag": "" } diff --git a/locales/es-ES/front.json b/locales/es-ES/front.json index 84e6b1c4c..f61b09feb 100644 --- a/locales/es-ES/front.json +++ b/locales/es-ES/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/es-US/art.json b/locales/es-US/art.json index 02d05bd09..0064019cd 100644 --- a/locales/es-US/art.json +++ b/locales/es-US/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Limite máximo de etiquetas", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/es-US/front.json b/locales/es-US/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/es-US/front.json +++ b/locales/es-US/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/fr-CA/art.json b/locales/fr-CA/art.json index 0c3fae8a4..8bff08a1d 100644 --- a/locales/fr-CA/art.json +++ b/locales/fr-CA/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Limite de tags atteinte", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/fr-CA/front.json b/locales/fr-CA/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/fr-CA/front.json +++ b/locales/fr-CA/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/fr-EU/art.json b/locales/fr-EU/art.json index e300bb7e6..f9da97048 100644 --- a/locales/fr-EU/art.json +++ b/locales/fr-EU/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Limite de tags atteinte", "delete.title": "Êtes vous sûre de vouloir supprimer cette œuvre?", - "unlink.title": "Êtes vous sûre de vouloir supprimer cette œuvre de votre profil? (Seulement {{username}} pourra le remettre)" + "unlink.title": "Êtes vous sûre de vouloir supprimer cette œuvre de votre profil? (Seulement {{username}} pourra le remettre)", + "noArtForTag": "" } diff --git a/locales/fr-EU/front.json b/locales/fr-EU/front.json index 2701d3bc7..0bf753112 100644 --- a/locales/fr-EU/front.json +++ b/locales/fr-EU/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/he/art.json b/locales/he/art.json index 5ff06b5d3..6488f515c 100644 --- a/locales/he/art.json +++ b/locales/he/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "הגעת למקסימום תגים", "forms.tags.maxReached": "הגעת לכמות תגים מקסימלית", "delete.title": "האם אתה בטוח שאתה רוצה למחוק את הציור?", - "unlink.title": "האם אתה בטוח שאתה רוצה להסיר את הציור הזה מהפרופיל שלך (רק {{username}} יוכל להוסיף אותו חזרה)?" + "unlink.title": "האם אתה בטוח שאתה רוצה להסיר את הציור הזה מהפרופיל שלך (רק {{username}} יוכל להוסיף אותו חזרה)?", + "noArtForTag": "" } diff --git a/locales/he/front.json b/locales/he/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/he/front.json +++ b/locales/he/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/it/art.json b/locales/it/art.json index 3b9ca6c2b..a14696355 100644 --- a/locales/it/art.json +++ b/locales/it/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Numero massimo di tag raggiunto.", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/it/front.json b/locales/it/front.json index 3c7db623b..ca25765ba 100644 --- a/locales/it/front.json +++ b/locales/it/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ja/art.json b/locales/ja/art.json index e5f72a42e..d36fd4bba 100644 --- a/locales/ja/art.json +++ b/locales/ja/art.json @@ -27,5 +27,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "タグの最大数に到達しました", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/ja/front.json b/locales/ja/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/ja/front.json +++ b/locales/ja/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ko/art.json b/locales/ko/art.json index 9830f4194..8e8b0deda 100644 --- a/locales/ko/art.json +++ b/locales/ko/art.json @@ -27,5 +27,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/ko/front.json b/locales/ko/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/ko/front.json +++ b/locales/ko/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/nl/art.json b/locales/nl/art.json index 195c7f423..d75450428 100644 --- a/locales/nl/art.json +++ b/locales/nl/art.json @@ -29,5 +29,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/nl/front.json b/locales/nl/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/nl/front.json +++ b/locales/nl/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/pl/art.json b/locales/pl/art.json index 3861e6eac..8c1aece3c 100644 --- a/locales/pl/art.json +++ b/locales/pl/art.json @@ -31,5 +31,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/pl/front.json b/locales/pl/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/pl/front.json +++ b/locales/pl/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/pt-BR/art.json b/locales/pt-BR/art.json index da2b0ec47..60d87e22e 100644 --- a/locales/pt-BR/art.json +++ b/locales/pt-BR/art.json @@ -30,5 +30,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Quantidade máxima de marcações atingida", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/pt-BR/front.json b/locales/pt-BR/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/pt-BR/front.json +++ b/locales/pt-BR/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ru/art.json b/locales/ru/art.json index 84580f919..398ceb4ba 100644 --- a/locales/ru/art.json +++ b/locales/ru/art.json @@ -31,5 +31,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "Максимальное кол-во тегов достигнуто", "delete.title": "Вы точно хотите удалить этот арт?", - "unlink.title": "Вы точно хотите удалить этот арт с вашего профиля (только {{username}} может добавить его обратно)?" + "unlink.title": "Вы точно хотите удалить этот арт с вашего профиля (только {{username}} может добавить его обратно)?", + "noArtForTag": "" } diff --git a/locales/ru/front.json b/locales/ru/front.json index 6dad8befb..4e26fabd4 100644 --- a/locales/ru/front.json +++ b/locales/ru/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/zh/art.json b/locales/zh/art.json index b33f9958b..fa63e8ead 100644 --- a/locales/zh/art.json +++ b/locales/zh/art.json @@ -27,5 +27,6 @@ "forms.tags.placeholder": "", "forms.tags.maxReached": "已达到最大标签数", "delete.title": "", - "unlink.title": "" + "unlink.title": "", + "noArtForTag": "" } diff --git a/locales/zh/front.json b/locales/zh/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/zh/front.json +++ b/locales/zh/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" }