Skip to content
Open
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
Binary file added docs/screenshots/layout-desktop-community.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 added docs/screenshots/layout-md-community.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 added docs/screenshots/layout-mobile-community.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 9 additions & 3 deletions src/app/blocking-qr/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,15 @@ export function BlockingQRProvider({children}: BlockingQRProviderProps) {
// new network right away. You can do it, we encourage you to do that if
// you want, but it's not a task for an average user.
//
const [shouldBlock, setShouldBlock] = useState(
() => localStorage.getItem('blocking-qr-completed') !== 'true',
);
const [shouldBlock, setShouldBlock] = useState(() => {
// TODO(demo): ?demo=1 в dev-режиме пропускает blocking-qr экран
const demo =
import.meta.env.DEV &&
new URLSearchParams(window.location.search).has('demo');
return (
!demo && localStorage.getItem('blocking-qr-completed') !== 'true'
);
});

const dismissBlockingQR = () => {
localStorage.setItem('blocking-qr-completed', 'true');
Expand Down
4 changes: 2 additions & 2 deletions src/app/community/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ function useListVirtualizer({items, parentRef}: ListVirtualizerProps) {
return null;
}
return JSON.parse(
sessionStorage.getItem('activity.scroll') ?? 'null',
sessionStorage.getItem('community.scroll') ?? 'null',
) as ScrollState;
}, [navigationType]);

Expand All @@ -406,7 +406,7 @@ function useListVirtualizer({items, parentRef}: ListVirtualizerProps) {
onChange: virtualizer => {
if (virtualizer.isScrolling) return;
sessionStorage.setItem(
'activity.scroll',
'community.scroll',
JSON.stringify({
initialOffset: virtualizer.scrollOffset,
initialMeasurementsCache: virtualizer.measurementsCache,
Expand Down
93 changes: 45 additions & 48 deletions src/app/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,56 +42,31 @@ const MENURAIL_ITEMS: MenuItem[] = [
},
];

const MENUBAR_ITEMS: MenuItem[] = [
{
path: '/community',
title: 'community',
icon: <Newspaper className="size-icon m-2" />,
},
{
path: '/feed',
title: 'feed',
icon: <BookUser className="size-icon m-2" />,
},
{
path: '/activity',
title: 'activity',
icon: <Inbox className="size-icon m-2" />,
},
{
path: '/chat',
title: 'chat',
icon: <MessageCircle className="size-icon m-2" />,
releaseTag: 'Q4',
},
{
path: '/profile',
title: 'profile',
icon: <User className="size-icon m-2" />,
},
];

export function MenuRail() {
const t = useTranslations('menu');
const {pathname} = useLocation();

return (
<div className="h-full p-1 flex flex-col gap-1.5 lg:p-4 lg:min-w-55">
<div className="h-full w-16 lg:w-55 shrink-0 p-1 flex flex-col gap-1.5 lg:p-4">
{MENURAIL_ITEMS.map(item => (
<Link key={item.path} to={item.path}>
<Button
variant="ghost"
className={cn(
'cursor-pointer justify-start w-full',
'cursor-pointer justify-start w-full max-lg:justify-center',
pathname === item.path &&
'bg-accent text-accent-foreground dark:bg-accent/50',
)}
>
{item.icon}{' '}
{item.icon}
<p className="hidden lg:block">
{t(item.title as Parameters<typeof t>[0])}
</p>{' '}
<Badge hidden={!item.releaseTag} variant="secondary">
</p>
<Badge
hidden={!item.releaseTag}
variant="secondary"
className="max-lg:hidden"
>
{item.releaseTag}
</Badge>
</Button>
Expand All @@ -102,24 +77,46 @@ export function MenuRail() {
}

export function MenuBar() {
const t = useTranslations('menu');
const {pathname} = useLocation();

return (
<div className="grid grid-cols-5 gap-2 p-2 w-full max-w-md mx-auto">
{MENUBAR_ITEMS.map(item => (
<Link key={item.path} to={item.path} className="min-w-0 w-full">
<Button
variant="ghost"
className={cn(
'cursor-pointer w-full h-full py-1 px-1 text-xs sm:text-sm',
pathname === item.path &&
'bg-accent text-accent-foreground dark:bg-accent/50',
)}
<div className="grid grid-cols-5 gap-1 px-2 pt-1.5 w-full pb-[max(0.5rem,env(safe-area-inset-bottom))]">
{MENURAIL_ITEMS.map(item => {
const active = pathname === item.path;
const label = t(item.title as Parameters<typeof t>[0]);
return (
<Link
key={item.path}
to={item.path}
aria-label={label}
aria-current={active ? 'page' : undefined}
className="min-w-0 w-full"
>
{item.icon}
</Button>
</Link>
))}
<Button
variant="ghost"
className={cn(
'cursor-pointer w-full h-auto flex-col gap-0.5 px-1.5 py-2 text-[10px] leading-none',
active &&
'bg-accent text-accent-foreground dark:bg-accent/50',
)}
>
<span className="relative">
{item.icon}
{item.releaseTag && (
<Badge
variant="secondary"
className="absolute -top-1 -right-3 h-4 px-1 text-[9px]"
>
{item.releaseTag}
</Badge>
)}
</span>
{label}
</Button>
</Link>
);
})}
</div>
);
}
4 changes: 2 additions & 2 deletions src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ export function ProfilePage() {
}

return (
<div className="mx-auto md:p-8 md:pt-4 max-w-5xl">
<div className="md:bg-card md:rounded-xl md:border md:border-border min-h-[calc(100vh-64px)] md:min-h-0 overflow-hidden transition-colors">
<div className="mx-auto h-full md:p-8 md:pt-4 max-w-5xl">
<div className="md:bg-card md:rounded-xl md:border md:border-border min-h-full md:min-h-0 overflow-hidden transition-colors">
{content}
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/app/scaffold.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ export function Scaffold({children}: ScaffoldProps): ReactNode {
return (
<div className="flex flex-col h-dvh w-dvw bg-background">
<TopBar {...topBar} />
<div className="flex-1 h-full flex min-h-0">
<div className="flex-1 flex min-h-0">
{showMenu && (
<>
<div
className={cn(
'h-full',
'shrink-0',
'bg-card',
'hidden md:block',
)}
Expand All @@ -61,7 +62,10 @@ export function Scaffold({children}: ScaffoldProps): ReactNode {
</>
)}
<div className="flex flex-col flex-1 min-w-0">
<div ref={scrollRef} className="overflow-y-auto flex-1">
<div
ref={scrollRef}
className="overflow-y-auto flex-1 min-h-0"
>
<ScrollRestoration scrollRef={scrollRef} />
<ScaffoldContextKey.Provider value={context}>
{children}
Expand Down
9 changes: 7 additions & 2 deletions src/components/session-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ export function SessionProvider({children}: {children: React.ReactNode}) {
const [status, setStatus] = useState<SessionStatus>('loading');

const refresh = useCallback(() => {
const ok = authService.get(app);
// TODO(demo): временно — ?demo=1 в dev-режиме показывает приложение
// с меню, даже без авторизации на бэкенде
const demo =
import.meta.env.DEV &&
new URLSearchParams(window.location.search).has('demo');
const ok = demo || authService.get(app);
setStatus(ok ? 'authed' : 'guest');
}, [backend]);
}, [app, backend]);

const setAuthed = useCallback(() => setStatus('authed'), []);

Expand Down