wip 3-col layout
This commit is contained in:
parent
60af57059b
commit
dd941ae70e
@ -1,202 +0,0 @@
|
|||||||
import "./Layout.css";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
import { base64 } from "@scure/base";
|
|
||||||
import { unwrap } from "@snort/shared";
|
|
||||||
|
|
||||||
import Icon from "@/Icons/Icon";
|
|
||||||
import useLoginFeed from "@/Feed/LoginFeed";
|
|
||||||
import { mapPlanName } from "./subscribe";
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
|
||||||
import Avatar from "@/Element/User/Avatar";
|
|
||||||
import { isHalloween, isFormElement, isStPatricksDay, isChristmas } from "@/SnortUtils";
|
|
||||||
import { getCurrentSubscription } from "@/Subscription";
|
|
||||||
import Toaster from "@/Toaster";
|
|
||||||
import { useTheme } from "@/Hooks/useTheme";
|
|
||||||
import { useLoginRelays } from "@/Hooks/useLoginRelays";
|
|
||||||
import { LoginUnlock } from "@/Element/PinPrompt";
|
|
||||||
import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
|
|
||||||
import { LoginStore } from "@/Login";
|
|
||||||
import { NoteCreatorButton } from "@/Element/Event/NoteCreatorButton";
|
|
||||||
import { ProfileLink } from "@/Element/User/ProfileLink";
|
|
||||||
import SearchBox from "../Element/SearchBox";
|
|
||||||
import SnortApi from "@/External/SnortApi";
|
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
|
||||||
|
|
||||||
export default function Layout() {
|
|
||||||
const location = useLocation();
|
|
||||||
const [pageClass, setPageClass] = useState("page");
|
|
||||||
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
|
|
||||||
|
|
||||||
useLoginFeed();
|
|
||||||
useTheme();
|
|
||||||
useLoginRelays();
|
|
||||||
useKeyboardShortcut(".", event => {
|
|
||||||
// if event happened in a form element, do nothing, otherwise focus on search input
|
|
||||||
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
|
||||||
event.preventDefault();
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldHideHeader = useMemo(() => {
|
|
||||||
const hideOn = ["/login", "/new"];
|
|
||||||
return hideOn.some(a => location.pathname.startsWith(a));
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const widePage = ["/login", "/messages"];
|
|
||||||
const noScroll = ["/messages"];
|
|
||||||
if (widePage.some(a => location.pathname.startsWith(a))) {
|
|
||||||
setPageClass(noScroll.some(a => location.pathname.startsWith(a)) ? "scroll-lock" : "");
|
|
||||||
} else {
|
|
||||||
setPageClass("page");
|
|
||||||
}
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={pageClass}>
|
|
||||||
{!shouldHideHeader && (
|
|
||||||
<header className="main-content">
|
|
||||||
<LogoHeader />
|
|
||||||
<AccountHeader />
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<Outlet />
|
|
||||||
<NoteCreatorButton className="note-create-button" />
|
|
||||||
<Toaster />
|
|
||||||
</div>
|
|
||||||
<LoginUnlock />
|
|
||||||
{stalker && (
|
|
||||||
<div
|
|
||||||
className="stalker"
|
|
||||||
onClick={() => {
|
|
||||||
LoginStore.removeSession(id);
|
|
||||||
}}>
|
|
||||||
<button type="button" className="circle flex items-center">
|
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountHeader = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useKeyboardShortcut("/", event => {
|
|
||||||
// if event happened in a form element, do nothing, otherwise focus on search input
|
|
||||||
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
|
||||||
event.preventDefault();
|
|
||||||
document.querySelector<HTMLInputElement>(".search input")?.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
|
|
||||||
publicKey: s.publicKey,
|
|
||||||
latestNotification: s.latestNotification,
|
|
||||||
readNotifications: s.readNotifications,
|
|
||||||
readonly: s.readonly,
|
|
||||||
}));
|
|
||||||
const profile = useUserProfile(publicKey);
|
|
||||||
const { publisher } = useEventPublisher();
|
|
||||||
|
|
||||||
const hasNotifications = useMemo(
|
|
||||||
() => latestNotification > readNotifications,
|
|
||||||
[latestNotification, readNotifications],
|
|
||||||
);
|
|
||||||
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
|
|
||||||
|
|
||||||
async function goToNotifications() {
|
|
||||||
// request permissions to send notifications
|
|
||||||
if ("Notification" in window) {
|
|
||||||
try {
|
|
||||||
if (Notification.permission !== "granted") {
|
|
||||||
const res = await Notification.requestPermission();
|
|
||||||
console.debug(res);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
const reg = await navigator.serviceWorker.ready;
|
|
||||||
if (reg && publisher) {
|
|
||||||
const api = new SnortApi(undefined, publisher);
|
|
||||||
const sub = await reg.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: (await api.getPushNotificationInfo()).publicKey,
|
|
||||||
});
|
|
||||||
await api.registerPushNotifications({
|
|
||||||
endpoint: sub.endpoint,
|
|
||||||
p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
|
|
||||||
auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
|
|
||||||
scope: `${location.protocol}//${location.hostname}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!publicKey) {
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={() => navigate("/login/sign-up")}>
|
|
||||||
<FormattedMessage defaultMessage="Sign Up" id="39AHJm" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="header-actions">
|
|
||||||
{!location.pathname.startsWith("/search") ? <SearchBox /> : <div className="grow"></div>}
|
|
||||||
{!readonly && (
|
|
||||||
<Link className="btn" to="/messages">
|
|
||||||
<Icon name="mail" size={24} />
|
|
||||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Link className="btn" to="/notifications" onClick={goToNotifications}>
|
|
||||||
<Icon name="bell-02" size={24} />
|
|
||||||
{hasNotifications && <span className="has-unread"></span>}
|
|
||||||
</Link>
|
|
||||||
<ProfileLink pubkey={publicKey} user={profile}>
|
|
||||||
<Avatar pubkey={publicKey} user={profile} />
|
|
||||||
</ProfileLink>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function LogoHeader() {
|
|
||||||
const { subscriptions } = useLogin();
|
|
||||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
|
||||||
|
|
||||||
const extra = () => {
|
|
||||||
if (isHalloween()) return "🎃";
|
|
||||||
if (isStPatricksDay()) return "🍀";
|
|
||||||
if (isChristmas()) return "🎄";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to="/" className="logo">
|
|
||||||
<h1>
|
|
||||||
{extra()}
|
|
||||||
{CONFIG.appName}
|
|
||||||
</h1>
|
|
||||||
{currentSubscription && (
|
|
||||||
<div className="flex items-center g4 text-sm font-semibold tracking-wider">
|
|
||||||
<Icon name="diamond" size={16} className="text-pro" />
|
|
||||||
{mapPlanName(currentSubscription.type)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
104
packages/app/src/Pages/Layout/AccountHeader.tsx
Normal file
104
packages/app/src/Pages/Layout/AccountHeader.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { base64 } from "@scure/base";
|
||||||
|
import { unwrap } from "@snort/shared";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import SearchBox from "../../Element/SearchBox";
|
||||||
|
import { ProfileLink } from "../../Element/User/ProfileLink";
|
||||||
|
import Avatar from "../../Element/User/Avatar";
|
||||||
|
import Icon from "../../Icons/Icon";
|
||||||
|
import useKeyboardShortcut from "../../Hooks/useKeyboardShortcut";
|
||||||
|
import { isFormElement } from "../../SnortUtils";
|
||||||
|
import useLogin from "../../Hooks/useLogin";
|
||||||
|
import useEventPublisher from "../../Hooks/useEventPublisher";
|
||||||
|
import SnortApi from "../../External/SnortApi";
|
||||||
|
|
||||||
|
const AccountHeader = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useKeyboardShortcut("/", event => {
|
||||||
|
// if event happened in a form element, do nothing, otherwise focus on search input
|
||||||
|
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.querySelector<HTMLInputElement>(".search input")?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
|
||||||
|
publicKey: s.publicKey,
|
||||||
|
latestNotification: s.latestNotification,
|
||||||
|
readNotifications: s.readNotifications,
|
||||||
|
readonly: s.readonly,
|
||||||
|
}));
|
||||||
|
const profile = useUserProfile(publicKey);
|
||||||
|
const { publisher } = useEventPublisher();
|
||||||
|
|
||||||
|
const hasNotifications = useMemo(
|
||||||
|
() => latestNotification > readNotifications,
|
||||||
|
[latestNotification, readNotifications],
|
||||||
|
);
|
||||||
|
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
|
||||||
|
|
||||||
|
async function goToNotifications() {
|
||||||
|
// request permissions to send notifications
|
||||||
|
if ("Notification" in window) {
|
||||||
|
try {
|
||||||
|
if (Notification.permission !== "granted") {
|
||||||
|
const res = await Notification.requestPermission();
|
||||||
|
console.debug(res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
const reg = await navigator.serviceWorker.ready;
|
||||||
|
if (reg && publisher) {
|
||||||
|
const api = new SnortApi(undefined, publisher);
|
||||||
|
const sub = await reg.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: (await api.getPushNotificationInfo()).publicKey,
|
||||||
|
});
|
||||||
|
await api.registerPushNotifications({
|
||||||
|
endpoint: sub.endpoint,
|
||||||
|
p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
|
||||||
|
auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
|
||||||
|
scope: `${location.protocol}//${location.hostname}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publicKey) {
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={() => navigate("/login/sign-up")}>
|
||||||
|
<FormattedMessage defaultMessage="Sign Up" id="39AHJm" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="header-actions">
|
||||||
|
{!location.pathname.startsWith("/search") ? <SearchBox /> : <div className="grow"></div>}
|
||||||
|
{!readonly && (
|
||||||
|
<Link className="btn" to="/messages">
|
||||||
|
<Icon name="mail" size={24} />
|
||||||
|
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link className="btn" to="/notifications" onClick={goToNotifications}>
|
||||||
|
<Icon name="bell-02" size={24} />
|
||||||
|
{hasNotifications && <span className="has-unread"></span>}
|
||||||
|
</Link>
|
||||||
|
<ProfileLink pubkey={publicKey} user={profile}>
|
||||||
|
<Avatar pubkey={publicKey} user={profile} />
|
||||||
|
</ProfileLink>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountHeader;
|
30
packages/app/src/Pages/Layout/LogoHeader.tsx
Normal file
30
packages/app/src/Pages/Layout/LogoHeader.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import useLogin from "../../Hooks/useLogin";
|
||||||
|
import { getCurrentSubscription } from "../../Subscription";
|
||||||
|
import { isChristmas, isHalloween, isStPatricksDay } from "../../SnortUtils";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { mapPlanName } from "../subscribe";
|
||||||
|
export function LogoHeader() {
|
||||||
|
const { subscriptions } = useLogin();
|
||||||
|
const currentSubscription = getCurrentSubscription(subscriptions);
|
||||||
|
|
||||||
|
const extra = () => {
|
||||||
|
if (isHalloween()) return "🎃";
|
||||||
|
if (isStPatricksDay()) return "🍀";
|
||||||
|
if (isChristmas()) return "🎄";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to="/" className="logo">
|
||||||
|
<h1>
|
||||||
|
{extra()}
|
||||||
|
{CONFIG.appName}
|
||||||
|
</h1>
|
||||||
|
{currentSubscription && (
|
||||||
|
<div className="flex items-center g4 text-sm font-semibold tracking-wider">
|
||||||
|
<Icon name="diamond" size={16} className="text-pro" />
|
||||||
|
{mapPlanName(currentSubscription.type)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
56
packages/app/src/Pages/Layout/NavSidebar.tsx
Normal file
56
packages/app/src/Pages/Layout/NavSidebar.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { LogoHeader } from "./LogoHeader";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Icon from "@/Icons/Icon";
|
||||||
|
|
||||||
|
const MENU_ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Home",
|
||||||
|
icon: "home",
|
||||||
|
link: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Messages",
|
||||||
|
icon: "mail",
|
||||||
|
link: "/messages",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Notifications",
|
||||||
|
icon: "bell-02",
|
||||||
|
link: "/notifications",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Settings",
|
||||||
|
icon: "cog",
|
||||||
|
link: "/settings",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function NavSidebar() {
|
||||||
|
return (
|
||||||
|
<div className="sticky border-r border-neutral-900 top-0 z-20 h-screen max-h-screen hidden md:flex xl:w-56 flex-col px-2 py-4 flex-shrink-0 gap-2">
|
||||||
|
<LogoHeader />
|
||||||
|
<div className="flex-grow flex flex-col justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{MENU_ITEMS.map(item => (
|
||||||
|
<NavLink
|
||||||
|
key={item.link}
|
||||||
|
to={item.link}
|
||||||
|
className="flex items-center py-2 px-4 rounded-full text-neutral-100 hover:bg-neutral-800 hover:text-neutral-200 hover:no-underline"
|
||||||
|
activeClassName="bg-neutral-800 text-neutral-200">
|
||||||
|
<Icon name={item.icon} size={24} className="mr-2" />
|
||||||
|
<span className="hidden xl:inline">{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavLink({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<Link to={""} {...props}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
11
packages/app/src/Pages/Layout/RightColumn.tsx
Normal file
11
packages/app/src/Pages/Layout/RightColumn.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import SearchBox from "../../Element/SearchBox";
|
||||||
|
|
||||||
|
export default function RightColumn() {
|
||||||
|
return (
|
||||||
|
<div className="flex-col hidden lg:flex lg:w-1/3 sticky top-0 h-screen p-2">
|
||||||
|
<div>
|
||||||
|
<SearchBox />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
99
packages/app/src/Pages/Layout/index.tsx
Normal file
99
packages/app/src/Pages/Layout/index.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import "./Layout.css";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
import Icon from "@/Icons/Icon";
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import { isFormElement } from "@/SnortUtils";
|
||||||
|
import Toaster from "@/Toaster";
|
||||||
|
import { useTheme } from "@/Hooks/useTheme";
|
||||||
|
import { useLoginRelays } from "@/Hooks/useLoginRelays";
|
||||||
|
import { LoginUnlock } from "@/Element/PinPrompt";
|
||||||
|
import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
|
||||||
|
import { LoginStore } from "@/Login";
|
||||||
|
import { NoteCreatorButton } from "@/Element/Event/NoteCreatorButton";
|
||||||
|
import NavSidebar from "./NavSidebar";
|
||||||
|
import AccountHeader from "./AccountHeader";
|
||||||
|
import RightColumn from "./RightColumn";
|
||||||
|
import { LogoHeader } from "./LogoHeader";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const location = useLocation();
|
||||||
|
const [pageClass, setPageClass] = useState("page");
|
||||||
|
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
|
||||||
|
|
||||||
|
useTheme();
|
||||||
|
useLoginRelays();
|
||||||
|
|
||||||
|
const hideHeaderPaths = ["/login", "/new"];
|
||||||
|
const shouldHideHeader = hideHeaderPaths.some(path => location.pathname.startsWith(path));
|
||||||
|
|
||||||
|
const pageClassPaths = useMemo(
|
||||||
|
() => ({
|
||||||
|
widePage: ["/login", "/messages"],
|
||||||
|
noScroll: ["/messages"],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isWidePage = pageClassPaths.widePage.some(path => location.pathname.startsWith(path));
|
||||||
|
const isNoScroll = pageClassPaths.noScroll.some(path => location.pathname.startsWith(path));
|
||||||
|
setPageClass(isWidePage ? (isNoScroll ? "scroll-lock" : "") : "page");
|
||||||
|
}, [location, pageClassPaths]);
|
||||||
|
|
||||||
|
const handleKeyboardShortcut = useCallback(event => {
|
||||||
|
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useKeyboardShortcut(".", handleKeyboardShortcut);
|
||||||
|
|
||||||
|
const isStalker = !!stalker;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex justify-center">
|
||||||
|
<div className={`${pageClass} w-full max-w-screen-xl overflow-x-hidden`}>
|
||||||
|
<div className="flex flex-row w-full">
|
||||||
|
<NavSidebar className="w-1/4 flex-shrink-0" />
|
||||||
|
<MainContent shouldHideHeader={shouldHideHeader} />
|
||||||
|
<RightColumn className="w-1/4 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
<NoteCreatorButton className="note-create-button" />
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
<LoginUnlock />
|
||||||
|
{isStalker && <StalkerModal id={id} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainContent({ shouldHideHeader }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col overflow-x-hidden">
|
||||||
|
{!shouldHideHeader && <Header />}
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 md:hidden">
|
||||||
|
<LogoHeader />
|
||||||
|
<AccountHeader />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StalkerModal({ id }) {
|
||||||
|
return (
|
||||||
|
<div className="stalker" onClick={() => LoginStore.removeSession(id)}>
|
||||||
|
<button type="button" className="circle flex items-center">
|
||||||
|
<Icon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -39,8 +39,6 @@
|
|||||||
|
|
||||||
@media (min-width: 520px) {
|
@media (min-width: 520px) {
|
||||||
.profile .banner {
|
.profile .banner {
|
||||||
width: 100%;
|
|
||||||
max-width: 720px;
|
|
||||||
height: 280px;
|
height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { magnetURIDecode, getRelayName } from ".";
|
import { magnetURIDecode, getRelayName } from ".";
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
describe("magnet", () => {
|
describe("magnet", () => {
|
||||||
it("should parse magnet link", () => {
|
it("should parse magnet link", () => {
|
||||||
|
@ -136,19 +136,7 @@ a.ext {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
|
||||||
width: 100vw;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.page {
|
|
||||||
width: 640px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: "jsdom",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user