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
6 changes: 3 additions & 3 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const UserLinkDetail = ({ username, contentSlug, initialContent }: Props) => {

if (status === "pending") {
return (
<div className="mx-auto max-w-prose px-4 py-8">
<div className="mx-auto max-w-prose px-0 py-4 sm:px-4 sm:py-8">
<div className="animate-pulse">
<div className="mb-4 h-6 w-24 rounded bg-elevated" />
<div className="mb-4 h-4 w-48 rounded bg-elevated" />
Expand All @@ -134,7 +134,7 @@ const UserLinkDetail = ({ username, contentSlug, initialContent }: Props) => {

if (status === "error" || !linkContent) {
return (
<div className="mx-auto max-w-prose px-4 py-8">
<div className="mx-auto max-w-prose px-0 py-4 sm:px-4 sm:py-8">
<Link
href="/"
className="mb-6 inline-flex items-center gap-1.5 font-mono text-sm text-muted transition-colors hover:text-fg"
Expand Down Expand Up @@ -173,7 +173,7 @@ const UserLinkDetail = ({ username, contentSlug, initialContent }: Props) => {
const isOwner = session?.user?.id === linkContent.author?.id;

return (
<article className="mx-auto max-w-prose px-4 py-8">
<article className="mx-auto max-w-prose px-0 py-4 sm:px-4 sm:py-8">
<Link
href="/"
className="mb-6 inline-flex items-center gap-1.5 font-mono text-sm text-muted transition-colors hover:text-fg"
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/admin/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const AdminDashboard = () => {
const { data: reportCounts } = api.report.getCounts.useQuery();

return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mx-auto max-w-6xl px-0 py-4 sm:px-4 sm:py-8">
<div className="mb-8">
<p className="eyebrow">
<span className="slash">{"// "}</span>admin
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const ModerationQueue = () => {
highlightedItem === id ? "ring-2 ring-accent rounded-lg" : "";

return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mx-auto max-w-6xl px-0 py-4 sm:px-4 sm:py-8">
<div className="mb-6 flex items-center gap-4">
<Link
href="/admin"
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/admin/sources/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ const AdminSourcesPage = () => {
};

return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mx-auto max-w-6xl px-0 py-4 sm:px-4 sm:py-8">
<div className="mb-8 flex items-center justify-between">
<div>
<p className="eyebrow">
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/admin/tags/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ const TagsAdmin = () => {
};

return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mx-auto max-w-6xl px-0 py-4 sm:px-4 sm:py-8">
<div className="mb-8 flex items-center justify-between">
<div>
<p className="eyebrow">
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/admin/users/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const UserManagement = () => {
: usersData?.users;

return (
<div className="mx-auto max-w-6xl px-4 py-8">
<div className="mx-auto max-w-6xl px-0 py-4 sm:px-4 sm:py-8">
<div className="mb-6 flex items-center gap-4">
<Link
href="/admin"
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/company/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default async function Page(props: Props) {
if (!company) return notFound();

return (
<div className="mx-auto w-full max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
<div className="mx-auto w-full max-w-4xl px-0 py-4 sm:px-6 sm:py-8 lg:px-8">
<div className="overflow-hidden rounded-lg bg-white shadow dark:bg-neutral-800">
<div className="border-b border-neutral-200 p-6 dark:border-neutral-700">
<div className="flex flex-col items-center gap-6 sm:flex-row">
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/draft/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const PreviewPage = async (props: Props) => {
const content = Markdoc.transform(ast, config);

return (
<div className="mx-auto max-w-3xl px-4 py-8">
<div className="mx-auto max-w-3xl px-0 py-4 sm:px-4 sm:py-8">
<nav className="mb-6 flex items-center gap-2 text-sm">
<span className="rounded-full bg-accent/15 px-3 py-1 font-medium text-accent">
Draft Preview
Expand Down
7 changes: 6 additions & 1 deletion app/(app)/feed/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ const FeedPage = ({ initialFeed }: { initialFeed?: FeedFirstPage | null }) => {
{t.label}
</button>
))}
<div className="ml-auto pl-4">{filterCluster}</div>
{/* Mobile: filters drop to their own full-width row below the tabs
instead of cramming under them. Desktop: inline, pushed right.
No overflow clip here — it would hide the FilterPill popovers. */}
<div className="order-last flex w-full justify-end pt-1 min-[640px]:order-none min-[640px]:ml-auto min-[640px]:w-auto min-[640px]:pl-4 min-[640px]:pt-0">
{filterCluster}
</div>
</div>
) : (
<div className="flex items-center border-b border-hairline pb-2">
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/s/[sourceSlug]/[slug]/_feedArticleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const FeedArticleContent = ({ sourceSlug, article }: Props) => {
const safeExternalUrl = safeExternalHref(article.externalUrl);

return (
<article className="mx-auto max-w-prose px-4 py-8">
<article className="mx-auto max-w-prose px-0 py-4 sm:px-4 sm:py-8">
<Link
href="/"
className="mb-6 inline-flex items-center gap-1.5 font-mono text-sm text-muted transition-colors hover:text-fg"
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const SourceProfileContent = ({ sourceSlug, initialProfile }: Props) => {
// first render (including SSR) is the real profile rather than a skeleton.
if (status === "error" || !pub) {
return (
<div className="mx-auto max-w-2xl px-4 py-8 text-fg">
<div className="mx-auto max-w-2xl px-0 py-4 sm:px-4 sm:py-8 text-fg">
<div className="bg-danger/12 rounded-lg border border-danger/30 p-6 text-center">
<h1 className="text-lg font-semibold text-danger">
Publication Not Found
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/tag/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export default async function TagPage(props: Props) {
const nextHref = hasNextPage ? `/tag/${slug}?page=${page + 1}` : null;

return (
<div className="mx-auto max-w-3xl px-4 py-8">
<div className="mx-auto max-w-3xl px-0 py-4 sm:px-4 sm:py-8">
<p className="eyebrow">
<span className="slash">{"// "}</span>
Tag
Expand Down
15 changes: 11 additions & 4 deletions app/api/admin/sync-feeds/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import Parser from "rss-parser";
import { customAlphabet } from "nanoid";
import { fetchOgImage } from "@/lib/og-image";
import { ensureHttps, unwrapDoubledUrl } from "@/utils/url";

// Generate Reddit-style short IDs: lowercase + numbers, 7 characters
const generateShortId = customAlphabet(
Expand Down Expand Up @@ -128,10 +129,14 @@
| { url?: string; type?: string }
| undefined;

if (mediaContent?.$?.url) return mediaContent.$.url;
if (mediaThumbnail?.$?.url) return mediaThumbnail.$.url;
// Some feeds (e.g. HackerNoon's media:thumbnail) prefix an already-absolute
// CDN URL with their own origin, producing a 404ing doubled URL — unwrap it.
if (mediaContent?.$?.url)
return ensureHttps(unwrapDoubledUrl(mediaContent.$.url));
if (mediaThumbnail?.$?.url)
return ensureHttps(unwrapDoubledUrl(mediaThumbnail.$.url));
if (enclosure?.url && enclosure.type?.startsWith("image/"))
return enclosure.url;
return ensureHttps(unwrapDoubledUrl(enclosure.url));

return null;
}
Expand Down Expand Up @@ -246,7 +251,9 @@

// Fetch OG image from the article URL
try {
const ogImageUrl = await fetchOgImage(item.link);
const ogImageUrl = ensureHttps(
unwrapDoubledUrl(await fetchOgImage(item.link)),
);
if (ogImageUrl) {
await db
.update(aggregated_article)
Expand All @@ -258,7 +265,7 @@
// Silently skip OG image fetch errors
}

newArticles++;

Check warning on line 268 in app/api/admin/sync-feeds/route.ts

View workflow job for this annotation

GitHub Actions / Run ESLint and Prettier

'newArticles' is assigned a value but never used

Check warning on line 268 in app/api/admin/sync-feeds/route.ts

View workflow job for this annotation

GitHub Actions / Run ESLint and Prettier

'newArticles' is assigned a value but never used
stats.articlesAdded++;
}

Expand Down Expand Up @@ -293,7 +300,7 @@
stats,
});
} catch (error) {
console.error("Sync error:", error);

Check warning on line 303 in app/api/admin/sync-feeds/route.ts

View workflow job for this annotation

GitHub Actions / Run ESLint and Prettier

Unexpected console statement
return NextResponse.json(
{ error: "Sync failed", details: (error as Error).message },
{ status: 500 },
Expand Down
28 changes: 25 additions & 3 deletions components/CommandPalette/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ export function CommandPalette({ onClose }: CommandPaletteProps) {
aria-label="Search Codú"
className="w-full max-w-[600px] overflow-hidden rounded-xl border border-strong bg-elevated shadow-lg"
>
<div className="flex items-center gap-3 border-b border-hairline px-5 py-4">
{/* A subtle thin accent line marks the active field — no focus ring. */}
<div className="flex items-center gap-3 border-b border-hairline px-5 py-4 transition-colors focus-within:border-accent">
<span className="text-lg leading-none text-faint">⌕</span>
<input
autoFocus
Expand All @@ -154,11 +155,32 @@ export function CommandPalette({ onClose }: CommandPaletteProps) {
role="combobox"
aria-expanded="true"
aria-controls="cmdk-list"
className="flex-1 bg-transparent text-lg text-fg outline-none placeholder:text-faint"
// The global focus-visible ring resolves to blue here; opt out and
// let the parent's focus-within accent underline mark the field.
className="flex-1 bg-transparent text-lg text-fg outline-none ring-0 ring-offset-0 placeholder:text-faint focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<kbd className="rounded-sm border border-hairline px-1.5 py-0.5 font-mono text-[11px] text-faint">
{/* Desktop closes with the Esc key; mobile gets a tap target. */}
<kbd className="hidden rounded-sm border border-hairline px-1.5 py-0.5 font-mono text-[11px] text-faint sm:inline-flex">
Esc
</kbd>
<button
type="button"
onClick={onClose}
aria-label="Close search"
className="-mr-1 flex h-8 w-8 items-center justify-center rounded-md text-faint transition-colors hover:bg-surface hover:text-fg sm:hidden"
>
<svg
className="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>

<div
Expand Down
2 changes: 1 addition & 1 deletion components/ContentDetail/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ContentDetailLayout = ({
sideInfo,
}: ContentDetailLayoutProps) => {
return (
<div className="mx-auto max-w-3xl px-4 py-8">
<div className="mx-auto max-w-3xl px-0 py-4 sm:px-4 sm:py-8">
{/* Breadcrumb navigation */}
{breadcrumbs.length > 0 && (
<nav
Expand Down
2 changes: 1 addition & 1 deletion components/ContentDetail/PostReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const PostReader = async ({
{articleSchema && <JsonLd data={articleSchema} />}
{breadcrumbSchema && <JsonLd data={breadcrumbSchema} />}

<div className="mx-auto max-w-3xl px-4 py-8">
<div className="mx-auto max-w-3xl px-0 py-4 sm:px-4 sm:py-8">
<nav className="mb-6 flex items-center gap-2 text-sm text-muted">
<Link href="/" className="hover:text-fg">
Feed
Expand Down
5 changes: 4 additions & 1 deletion components/Feed/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ const FeedFilters = ({
typeValue !== "all" || sort !== "recent" || tagValue !== ALL_TOPICS;

return (
<div className="flex items-center gap-2" data-testid="feed-filters">
<div
className="flex flex-wrap items-center justify-end gap-x-2 gap-y-1"
data-testid="feed-filters"
>
{isDirty && (
<button
type="button"
Expand Down
28 changes: 28 additions & 0 deletions components/Layout/NavDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { signIn } from "next-auth/react";
import { type Session } from "next-auth";
import { FEATURE_FLAGS, isFlagEnabled } from "@/utils/flags";

Expand Down Expand Up @@ -106,6 +107,33 @@ export function NavDrawer({
</button>
</div>

{/* Logged-out auth CTAs live here on mobile — the top bar drops them to
make room for the search icon. */}
{!session && (
<div className="mb-3 flex flex-col gap-2 px-1">
<button
type="button"
onClick={() => {
onClose();
signIn();
}}
className="primary-button w-full justify-center"
>
Join free
</button>
<button
type="button"
onClick={() => {
onClose();
signIn();
}}
className="w-full rounded-md border border-strong px-3 py-2 text-center text-sm font-semibold text-fg transition-colors hover:bg-surface"
>
Log in
</button>
</div>
)}

<nav className="flex flex-col gap-0.5">
{nav.map((item) => {
const active = isActive(item.href);
Expand Down
70 changes: 56 additions & 14 deletions components/Layout/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ export function TopBar({
</Link>
</div>

{/* Desktop: a full search field that opens the ⌘K palette. Hidden on
mobile, where a compact icon button in the right cluster takes over. */}
<button
type="button"
onClick={onOpenPalette}
className="flex w-full min-w-0 max-w-[440px] items-center gap-2 justify-self-center rounded-full border border-hairline bg-surface py-1.5 pl-3.5 pr-2.5 text-left transition-colors duration-base ease-out hover:border-strong"
className="hidden w-full min-w-0 max-w-[440px] items-center gap-2 justify-self-center rounded-full border border-hairline bg-surface py-1.5 pl-3.5 pr-2.5 text-left transition-colors duration-base ease-out hover:border-strong focus:outline-none focus-visible:border-accent focus-visible:ring-0 focus-visible:ring-offset-0 min-[721px]:flex"
>
<span className="text-sm leading-none text-faint">⌕</span>
<SearchIcon className="h-[18px] w-[18px] shrink-0 text-faint" />
<span className="min-w-0 flex-1 truncate text-sm text-faint">
Search…
</span>
Expand All @@ -89,6 +91,7 @@ export function TopBar({

{session ? (
<div className="flex items-center gap-3 justify-self-end">
<SearchIconButton onClick={onOpenPalette} />
<Link
href="/notifications"
aria-label="Notifications"
Expand Down Expand Up @@ -160,24 +163,63 @@ export function TopBar({
</div>
) : (
<div className="flex items-center gap-3 justify-self-end">
<button
onClick={() => signIn()}
className="whitespace-nowrap text-sm font-semibold text-fg"
>
Log in
</button>
<button
onClick={() => signIn()}
className="primary-button whitespace-nowrap"
>
Join free
</button>
<SearchIconButton onClick={onOpenPalette} />
{/* Log in / Join free move into the hamburger drawer on mobile — hide
the whole cluster (wrapping the parent beats primary-button's own
display, which would otherwise survive a bare `hidden`). */}
<div className="hidden items-center gap-3 min-[721px]:flex">
<button
onClick={() => signIn()}
className="whitespace-nowrap text-sm font-semibold text-fg"
>
Log in
</button>
<button
onClick={() => signIn()}
className="primary-button whitespace-nowrap"
>
Join free
</button>
</div>
</div>
)}
</header>
);
}

function SearchIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.4-3.4" />
</svg>
);
}

/** Mobile-only search affordance: the full search field collapses to this icon
* button (≤sm), which opens the same ⌘K palette. */
function SearchIconButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
aria-label="Search"
className="flex h-9 w-9 items-center justify-center rounded-full text-fg hover:bg-elevated min-[721px]:hidden"
>
<SearchIcon className="h-[22px] w-[22px]" />
</button>
);
}

function Avatar({
name,
image,
Expand Down
Loading
Loading