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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/deployment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/event-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/server-status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/sign-in.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/submit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/desktop-chromium/world-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/deployment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/event-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/person-profile.png
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/search.png
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/server-status.png
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/sign-in.png
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/submit.png
Binary file modified apps/web/e2e/__screenshots__/mobile-chromium/world-profile.png
16 changes: 16 additions & 0 deletions apps/web/e2e/public-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export async function expectSearchPage(page: Page) {
await expect(page.getByRole("heading", { name: /Results for aurora/i })).toBeVisible();
await expect(page.getByRole("button", { name: /Search VRDex/i })).toBeVisible();
await expect(page.getByRole("link", { name: /DJ Aurora/i }).first()).toBeVisible();
await expect(page.locator('[title="Logo"]').first()).toBeVisible();
await expect(page.getByRole("heading", { name: /Events worth checking first/i })).toHaveCount(0);
}

Expand Down Expand Up @@ -215,6 +216,13 @@ export async function expectAccountPage(page: Page) {
await expect(page.getByRole("link", { name: "Sign in" }).last()).toBeVisible();
}

export async function expectAppearancePage(page: Page) {
await expect(page.getByRole("heading", { name: /Shape the way your profile image shows up/i })).toBeVisible();
await expect(page.getByRole("heading", { name: "Profile picture shape and border" })).toBeVisible();
await expect(page.getByLabel("Avatar roundedness")).toBeVisible();
await expect(page.getByText("Demo mode is live-only", { exact: false })).toBeVisible();
}

export async function expectSuppressionPage(page: Page) {
await expect(page.getByRole("heading", { name: /Request review of a public listing/i })).toBeVisible();
await expect(page.getByLabel("Request type")).toBeVisible();
Expand Down Expand Up @@ -253,6 +261,9 @@ export async function expectPersonProfilePage(page: Page) {
await expect(page.getByText(/Creator links/i)).toBeVisible();
await expect(page.getByText("VRChat profile", { exact: true })).toBeVisible();
await expect(page.getByText("DJ Aurora SoundCloud", { exact: true })).toBeVisible();
await expect(page.getByRole("heading", { name: "Media kit" })).toBeVisible();
await expect(page.getByRole("link", { name: /Primary logo/i })).toBeVisible();
await expect(page.getByRole("link", { name: /Download logos zip/i })).toBeVisible();
await expect(page.getByRole("heading", { name: "Worlds" })).toBeVisible();
await expect(page.getByRole("link", { name: /Neon Harbor/i })).toBeVisible();
}
Expand Down Expand Up @@ -348,6 +359,11 @@ export const capturedRoutes: CapturedRoute[] = [
path: "/account",
expectPage: expectAccountPage,
},
{
name: "appearance-demo",
path: "/account/appearance",
expectPage: expectAppearancePage,
},
{
name: "search",
path: "/search?q=aurora",
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"typecheck": "next typegen && node ./scripts/ensure-next-type-stubs.mjs && tsc --noEmit --incremental false"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1068.0",
"@convex-dev/auth": "^0.0.92",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
75 changes: 53 additions & 22 deletions apps/web/src/app/_components/discovery-public-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type PublicSearchResult = {
subtitle?: string;
summary?: string;
imageUrl?: string;
profileImageUrl?: string;
logoImageUrl?: string;
startsAt?: number;
source?: {
sourceType?: string;
Expand Down Expand Up @@ -93,11 +95,46 @@ function resultMatchesFilter(result: PublicSearchResult, filter: SearchResultFil
return result.entityType === filter;
}

function resultMeta(result: PublicSearchResult): string {
const subtitle = resultSubtitle(result);
const parts = [entityLabel(result), subtitle].filter(Boolean);
function ResultImage({ result }: { result: PublicSearchResult }) {
const imageStyle = safeImageBackground(result.imageUrl, discoveryThumbOverlay);

if (
result.entityType !== "profile" ||
!result.logoImageUrl ||
!result.profileImageUrl ||
result.logoImageUrl === result.profileImageUrl
) {
return (
<span
className="flex size-14 shrink-0 items-center justify-center rounded-card bg-[linear-gradient(135deg,#2f211b,#d66a4d)] bg-cover bg-center text-lg font-semibold text-white"
style={imageStyle}
>
{!imageStyle ? initialsFor(result.title) : null}
</span>
);
}

return parts.join(" / ");
const profileImageStyle = safeImageBackground(result.profileImageUrl, discoveryThumbOverlay);
const logoStyle = safeImageBackground(result.logoImageUrl);

return (
<span className="grid shrink-0 grid-cols-2 gap-1">
<span
className="flex size-14 items-center justify-center rounded-card bg-[linear-gradient(135deg,#2f211b,#d66a4d)] bg-cover bg-center text-lg font-semibold text-white"
style={profileImageStyle}
title="Profile image"
>
{!profileImageStyle ? initialsFor(result.title) : null}
</span>
<span
className="flex size-14 items-center justify-center rounded-card border border-border bg-surface-strong bg-contain bg-center bg-no-repeat text-xs font-semibold text-muted"
style={logoStyle}
title="Logo"
>
{!logoStyle ? "Logo" : null}
</span>
</span>
);
}

function TopNav() {
Expand All @@ -120,7 +157,6 @@ function TopNav() {
}

function DiscoveryCard({ result, surface }: { result: PublicSearchResult; surface: string }) {
const imageStyle = safeImageBackground(result.imageUrl, discoveryThumbOverlay);
const subtitle = resultSubtitle(result);

return (
Expand All @@ -135,12 +171,7 @@ function DiscoveryCard({ result, surface }: { result: PublicSearchResult; surfac
surface,
}}
>
<span
className="flex size-14 shrink-0 items-center justify-center rounded-card bg-[linear-gradient(135deg,#2f211b,#d66a4d)] bg-cover bg-center text-lg font-semibold text-white"
style={imageStyle}
>
{!imageStyle ? initialsFor(result.title) : null}
</span>
<ResultImage result={result} />
<span className="flex min-w-0 flex-col gap-2">
{result.startsAt === undefined ? null : <ViewerLocalEventDateTime className="text-sm font-medium text-accent-strong" timestamp={result.startsAt} />}
<span className="text-xl font-semibold tracking-[-0.03em] group-hover:text-accent-strong">
Expand All @@ -154,7 +185,7 @@ function DiscoveryCard({ result, surface }: { result: PublicSearchResult; surfac
}

function SearchResultCard({ result }: { result: PublicSearchResult }) {
const imageStyle = safeImageBackground(result.imageUrl, discoveryThumbOverlay);
const subtitle = resultSubtitle(result);

return (
<TrackedDiscoveryLink
Expand All @@ -168,17 +199,17 @@ function SearchResultCard({ result }: { result: PublicSearchResult }) {
surface: "search_results",
}}
>
<span
className="flex size-14 shrink-0 items-center justify-center rounded-card bg-[linear-gradient(135deg,#2f211b,#d66a4d)] bg-cover bg-center text-lg font-semibold text-white"
style={imageStyle}
>
{!imageStyle ? initialsFor(result.title) : null}
</span>
<span className="flex min-w-0 flex-col gap-2">
<span className="text-xs font-medium text-muted">{resultMeta(result)}</span>
<span className="text-xl font-semibold tracking-[-0.03em] group-hover:text-accent-strong">
{result.title}
<ResultImage result={result} />
<span className="flex min-w-0 flex-1 flex-col gap-2">
<span className="flex items-start justify-between gap-4">
<span className="min-w-0 text-xl font-semibold tracking-[-0.03em] group-hover:text-accent-strong">
{result.title}
</span>
<span className="shrink-0 rounded-control border border-border bg-surface-strong px-3 py-1 text-xs font-medium text-muted">
{entityLabel(result)}
</span>
</span>
{subtitle ? <span className="text-sm text-muted">{subtitle}</span> : null}
{result.startsAt === undefined ? null : <ViewerLocalEventDateTime className="text-sm text-accent-strong" timestamp={result.startsAt} />}
{result.summary ? <span className="line-clamp-2 text-sm leading-6 text-muted">{result.summary}</span> : null}
{result.source ? <span className="text-xs text-muted">{result.source.label}</span> : null}
Expand Down
118 changes: 114 additions & 4 deletions apps/web/src/app/_components/profile-public-page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Link from "next/link";
import type { CSSProperties } from "react";

import { EventPreviewCard, type PublicEventPreview } from "./event-public-page";
import { buttonVariants } from "@/components/ui/button";
import { Card, Eyebrow, SectionHeading, SectionTitle } from "@/components/ui/card";
import { BrandLink, PageContainer, PageNav, PageShell } from "@/components/ui/page-shell";
import { avatarFrameStyle, defaultAvatarAppearance, type AvatarAppearance } from "@/lib/avatar-appearance";
import { cn } from "@/lib/cn";
import { safeImageBackground } from "@/lib/safe-image";

Expand Down Expand Up @@ -51,6 +53,30 @@ type PublicProfileGenre = {
featured?: boolean;
};

type PublicProfileAsset = {
assetId: string;
label?: string;
caption?: string;
mimeType: string;
byteSize: number;
imageUrl: string;
downloadUrl: string;
};

type PublicProfileMediaKit = {
profileImage?: PublicProfileAsset;
banner?: PublicProfileAsset;
primaryLogo?: PublicProfileAsset;
additionalLogos: PublicProfileAsset[];
logos: PublicProfileAsset[];
assets: PublicProfileAsset[];
logoZipUrl?: string;
compactDisplay: "profile_image" | "logo";
avatarAppearance?: PublicProfileAvatarAppearance;
};

type PublicProfileAvatarAppearance = AvatarAppearance;

type PublicProfileBase = {
profileType: "person" | "community";
slug: string;
Expand Down Expand Up @@ -89,6 +115,7 @@ type PublicProfileBase = {
}>;
upcomingEvents: PublicEventPreview[];
hostedEvents: PublicEventPreview[];
mediaKit?: PublicProfileMediaKit;
};

type PublicPersonProfile = PublicProfileBase & {
Expand Down Expand Up @@ -137,7 +164,6 @@ function initialsFor(name: string): string {
}

const profileBannerOverlay = "linear-gradient(135deg, rgba(22, 17, 15, 0.58), rgba(214, 106, 77, 0.2))";

function safeHttpsUrl(url: string): string | null {
try {
const parsed = new URL(url);
Expand Down Expand Up @@ -172,6 +198,44 @@ function roleLabel(role: WorldCreatorRole): string {
.join(" ");
}

function formatByteSize(value: number): string {
if (value >= 1024 * 1024) {
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
}

return `${Math.max(1, Math.round(value / 1024))} KB`;
}

function mimeLabel(value: string): string {
return value.replace(/^image\//, "").replace("svg+xml", "svg").toUpperCase();
}

function MediaAssetCard({ asset, label }: { asset: PublicProfileAsset; label: string }) {
const imageStyle = safeImageBackground(asset.imageUrl);

return (
<a
className="group grid gap-3 rounded-card border border-border bg-surface-strong p-3 text-sm transition hover:-translate-y-0.5 hover:shadow-panel"
download
href={asset.downloadUrl}
>
<span
className="flex aspect-[4/3] items-center justify-center rounded-control border border-border bg-[linear-gradient(135deg,#2f211b,#d66a4d)] bg-contain bg-center bg-no-repeat text-lg font-semibold text-white"
style={imageStyle}
>
{!imageStyle ? label.slice(0, 2).toUpperCase() : null}
</span>
<span className="grid gap-1">
<span className="font-medium group-hover:text-accent-strong">{asset.label ?? label}</span>
{asset.caption ? <span className="line-clamp-2 leading-5 text-muted">{asset.caption}</span> : null}
<span className="text-xs text-muted">
{mimeLabel(asset.mimeType)} / {formatByteSize(asset.byteSize)}
</span>
</span>
</a>
);
}

function PillList({ items }: { items: string[] }) {
if (items.length === 0) {
return <p className="text-sm leading-6 text-muted">No public entries yet.</p>;
Expand Down Expand Up @@ -217,7 +281,16 @@ export function ProfilePublicPage({ profile }: { profile: PublicProfile }) {
(item): item is string => Boolean(item),
);
const bannerStyle = safeImageBackground(profile.bannerImageUrl, profileBannerOverlay);
const avatarStyle = safeImageBackground(profile.avatarImageUrl);
const avatarImageStyle = safeImageBackground(profile.avatarImageUrl);
const hasAvatarImage = avatarImageStyle !== undefined;
const mediaKit = profile.mediaKit ?? {
additionalLogos: [],
logos: [],
assets: [],
compactDisplay: "profile_image" as const,
};
const avatarAppearance = mediaKit.avatarAppearance ?? defaultAvatarAppearance;
const avatarStyle: CSSProperties = avatarFrameStyle(avatarImageStyle, avatarAppearance);
const sourceSubmittedAt = formatSubmittedAt(profile.source?.submittedAt);
const sourceDetails = [
profile.source && profile.source.label !== trust ? profile.source.label : null,
Expand Down Expand Up @@ -246,12 +319,12 @@ export function ProfilePublicPage({ profile }: { profile: PublicProfile }) {
<div className="grid gap-6 lg:items-end">
<div className="flex flex-col gap-4">
<div
className="flex size-24 items-center justify-center rounded-panel border border-white/35 bg-white/20 bg-cover bg-center text-3xl font-semibold shadow-panel"
className="flex size-24 items-center justify-center bg-white/20 bg-cover bg-center text-3xl font-semibold text-white shadow-panel"
style={avatarStyle}
role="img"
aria-label={`${profile.displayName} display image`}
>
{!avatarStyle ? initialsFor(profile.displayName) : null}
{!hasAvatarImage ? initialsFor(profile.displayName) : null}
</div>

<div className="max-w-3xl">
Expand Down Expand Up @@ -370,6 +443,43 @@ export function ProfilePublicPage({ profile }: { profile: PublicProfile }) {
</div>
</Card>

<Card>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<SectionHeading>
Media kit
</SectionHeading>
{mediaKit.logoZipUrl ? (
<a className={buttonVariants({ size: "sm", variant: "secondary" })} download href={mediaKit.logoZipUrl}>
Download logos zip
</a>
) : null}
</div>
<div className="mt-5 grid gap-4 lg:grid-cols-[0.9fr_1.1fr]">
<div className="rounded-card border border-border bg-surface-strong p-4">
<p className="text-sm font-medium text-muted">Primary logo</p>
{mediaKit.primaryLogo ? (
<div className="mt-3">
<MediaAssetCard asset={mediaKit.primaryLogo} label="Primary logo" />
</div>
) : (
<p className="mt-3 text-sm leading-6 text-muted">No public logo yet.</p>
)}
</div>
<div className="rounded-card border border-border bg-surface-strong p-4">
<p className="text-sm font-medium text-muted">Additional logos</p>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
{mediaKit.additionalLogos.length === 0 ? (
<p className="text-sm leading-6 text-muted">No additional public logos yet.</p>
) : (
mediaKit.additionalLogos.map((asset, index) => (
<MediaAssetCard asset={asset} key={asset.assetId} label={`Logo ${index + 2}`} />
))
)}
</div>
</div>
</div>
</Card>

<Card>
<SectionHeading>
Worlds
Expand Down
11 changes: 8 additions & 3 deletions apps/web/src/app/account/account-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,14 @@ function ConnectedAccountPanel() {
<dd className="mt-1">{viewer.user.emailVerified ? "Verified" : "Not verified"}</dd>
</div>
</dl>
<Button className="mt-5" size="lg" type="button" onClick={() => void signOut()}>
Sign out
</Button>
<div className="mt-5 flex flex-wrap gap-3">
<Link className={buttonVariants({ size: "lg", variant: "primary" })} href="/account/appearance">
Customize appearance
</Link>
<Button size="lg" type="button" onClick={() => void signOut()}>
Sign out
</Button>
</div>
</Card>

<Card surface="glass">
Expand Down
Loading