diff --git a/components/sidebar.tsx b/components/sidebar.tsx index ecaad143..cb148a72 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -1,16 +1,15 @@ -import { useState, useEffect, useRef } from "react" -import type { NextPage } from "next" -import { loginState, workspacestate } from "@/state" -import { themeState } from "@/state/theme" -import { useRecoilState } from "recoil" -import { Menu, Listbox, Dialog } from "@headlessui/react" -import { useRouter } from "next/router" +import { useState, useEffect, useRef } from "react"; +import type { NextPage } from "next"; +import { loginState, workspacestate } from "@/state"; +import { themeState } from "@/state/theme"; +import { useRecoilState } from "recoil"; +import { Menu, Listbox } from "@headlessui/react"; +import { useRouter } from "next/router"; import { IconHome, IconHomeFilled, IconMessage2, IconMessage2Filled, - IconServer, IconClipboardList, IconClipboardListFilled, IconBell, @@ -23,94 +22,32 @@ import { IconFileText, IconFileTextFilled, IconShield, + IconShieldFilled, IconCheck, IconRosetteDiscountCheck, IconRosetteDiscountCheckFilled, IconChevronLeft, - IconMenu2, IconSun, IconMoon, - IconX, + IconLogout, IconClock, IconClockFilled, - IconTrophy, - IconTrophyFilled, - IconShieldFilled, IconTarget, - IconCopyright, - IconBook, - IconBrandGithub, - IconHistory, - IconBug, -} from "@tabler/icons-react" -import axios from "axios" -import clsx from "clsx" -import Parser from "rss-parser" -import ReactMarkdown from "react-markdown"; -import packageJson from "../package.json"; + IconDots, +} from "@tabler/icons-react"; +import axios from "axios"; +import clsx from "clsx"; interface SidebarProps { - isCollapsed: boolean - setIsCollapsed: (value: boolean) => void + isCollapsed: boolean; + setIsCollapsed: (value: boolean) => void; } -const ChangelogContent: React.FC<{ workspaceId: number }> = ({ workspaceId }) => { - const [entries, setEntries] = useState< - { title: string; link: string; pubDate: string; content: string }[] - >([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch('/api/changelog') - .then(res => res.json()) - .then(data => { - setEntries(data); - setLoading(false); - }) - .catch(() => setLoading(false)); - }, [workspaceId]); - - if (loading) return

Loading...

; - if (!entries.length) return

No entries found.

; - - return ( -
- {entries.map((entry, idx) => ( -
- - {entry.title} - -
{entry.pubDate}
-
- {entry.content} -
-
- ))} -
- ); -}; - const Sidebar: NextPage = ({ isCollapsed, setIsCollapsed }) => { - const [login, setLogin] = useRecoilState(loginState) - const [workspace, setWorkspace] = useRecoilState(workspacestate) - const [theme, setTheme] = useRecoilState(themeState) - const [showOrbitInfo, setShowOrbitInfo] = useState(false); - const [showCopyright, setShowCopyright] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) - const [showChangelog, setShowChangelog] = useState(false); - const [changelog, setChangelog] = useState<{ title: string, link: string, pubDate: string, content: string }[]>([]); - const [changelogLoading, setChangelogLoading] = useState(false); + const [login, setLogin] = useRecoilState(loginState); + const [workspace, setWorkspace] = useRecoilState(workspacestate); + const [theme, setTheme] = useRecoilState(themeState); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [docsEnabled, setDocsEnabled] = useState(false); const [alliesEnabled, setAlliesEnabled] = useState(false); const [sessionsEnabled, setSessionsEnabled] = useState(false); @@ -118,65 +55,70 @@ const Sidebar: NextPage = ({ isCollapsed, setIsCollapsed }) => { const [policiesEnabled, setPoliciesEnabled] = useState(false); const [pendingPolicyCount, setPendingPolicyCount] = useState(0); const [pendingNoticesCount, setPendingNoticesCount] = useState(0); + const [mobileMoreOpen, setMobileMoreOpen] = useState(false); + const [mobileMoreVisible, setMobileMoreVisible] = useState(false); + const [isStandalone, setIsStandalone] = useState(false); // Added for PWA check const workspaceListboxWrapperRef = useRef(null); - const router = useRouter() + const router = useRouter(); + // Detect PWA Mode useEffect(() => { - if (isMobileMenuOpen) { - document.body.classList.add("overflow-hidden") - } else { - document.body.classList.remove("overflow-hidden") - } - return () => { - document.body.classList.remove("overflow-hidden") + if (typeof window !== "undefined" && window.matchMedia("(display-mode: standalone)").matches) { + setIsStandalone(true); } - }, [isMobileMenuOpen]) + }, []); + + const openMoreSheet = () => { + setMobileMoreOpen(true); + requestAnimationFrame(() => { + requestAnimationFrame(() => setMobileMoreVisible(true)); + }); + }; + + const closeMoreSheet = () => { + setMobileMoreVisible(false); + setTimeout(() => setMobileMoreOpen(false), 300); + }; + + useEffect(() => { + if (isMobileMenuOpen) document.body.classList.add("overflow-hidden"); + else document.body.classList.remove("overflow-hidden"); + return () => document.body.classList.remove("overflow-hidden"); + }, [isMobileMenuOpen]); const pages: { - name: string - href: string - icon: React.ElementType - filledIcon?: React.ElementType - accessible?: boolean + name: string; + href: string; + icon: React.ElementType; + filledIcon?: React.ElementType; + accessible?: boolean; }[] = [ { name: "Home", href: `/workspace/${workspace.groupId}`, icon: IconHome, filledIcon: IconHomeFilled }, { name: "Wall", href: `/workspace/${workspace.groupId}/wall`, icon: IconMessage2, filledIcon: IconMessage2Filled, accessible: workspace.yourPermission.includes("view_wall") }, { name: "Activity", href: `/workspace/${workspace.groupId}/activity`, icon: IconClipboardList, filledIcon: IconClipboardListFilled, accessible: true }, { name: "Quotas", href: `/workspace/${workspace.groupId}/quotas`, icon: IconTarget, accessible: true }, - ...(noticesEnabled ? [{ - name: "Notices", - href: `/workspace/${workspace.groupId}/notices`, - icon: IconClock, - filledIcon: IconClockFilled, - accessible: true, - }] : []), - ...(alliesEnabled ? [{ - name: "Alliances", - href: `/workspace/${workspace.groupId}/alliances`, - icon: IconRosetteDiscountCheck, - filledIcon: IconRosetteDiscountCheckFilled, - accessible: true, - }] : []), - ...(sessionsEnabled ? [{ - name: "Sessions", - href: `/workspace/${workspace.groupId}/sessions`, - icon: IconBell, - filledIcon: IconBellFilled, - accessible: true, - }] : []), + ...(noticesEnabled ? [{ name: "Notices", href: `/workspace/${workspace.groupId}/notices`, icon: IconClock, filledIcon: IconClockFilled, accessible: true }] : []), + ...(alliesEnabled ? [{ name: "Alliances", href: `/workspace/${workspace.groupId}/alliances`, icon: IconRosetteDiscountCheck, filledIcon: IconRosetteDiscountCheckFilled, accessible: true }] : []), + ...(sessionsEnabled ? [{ name: "Sessions", href: `/workspace/${workspace.groupId}/sessions`, icon: IconBell, filledIcon: IconBellFilled, accessible: true }] : []), { name: "Staff", href: `/workspace/${workspace.groupId}/views`, icon: IconUser, filledIcon: IconUserFilled, accessible: workspace.yourPermission.includes("view_members") }, ...(docsEnabled ? [{ name: "Docs", href: `/workspace/${workspace.groupId}/docs`, icon: IconFileText, filledIcon: IconFileTextFilled, accessible: true }] : []), ...(policiesEnabled ? [{ name: "Policies", href: `/workspace/${workspace.groupId}/policies`, icon: IconShield, filledIcon: IconShieldFilled, accessible: true }] : []), - { name: "Settings", href: `/workspace/${workspace.groupId}/settings`, icon: IconSettings, filledIcon: IconSettingsFilled, accessible: ["admin", "workspace_customisation", "reset_activity", "manage_features", "manage_apikeys", "view_audit_logs"].some(perm => workspace.yourPermission.includes(perm)) }, + { name: "Settings", href: `/workspace/${workspace.groupId}/settings`, icon: IconSettings, filledIcon: IconSettingsFilled, accessible: ["admin", "workspace_customisation", "reset_activity", "manage_features", "manage_apikeys", "view_audit_logs"].some((perm) => workspace.yourPermission.includes(perm)) }, ]; + const visiblePages = pages.filter((p) => p.accessible === undefined || p.accessible); + + const bottomBarPages = visiblePages.slice(0, 4); + const morePages = visiblePages.slice(4); + const gotopage = (page: string) => { - router.push(page) - setIsMobileMenuOpen(false) - } + router.push(page); + setIsMobileMenuOpen(false); + closeMoreSheet(); + }; const logout = async () => { - await axios.post("/api/auth/logout") + await axios.post("/api/auth/logout"); setLogin({ userId: 1, username: "", @@ -185,40 +127,25 @@ const Sidebar: NextPage = ({ isCollapsed, setIsCollapsed }) => { thumbnail: "", workspaces: [], isOwner: false, - }) - router.push("/login") - } + }); + router.push("/login"); + }; const toggleTheme = () => { - const newTheme = theme === "dark" ? "light" : "dark" - setTheme(newTheme) - if (typeof window !== "undefined") { - localStorage.setItem("theme", newTheme) - } - } - - useEffect(() => { - if (!showChangelog) return; - setChangelogLoading(true); - fetch('/api/changelog') - .then(res => res.json()) - .then((data) => { - const items = Array.isArray(data) ? data : (data?.items ?? []); - setChangelog(Array.isArray(items) ? items : []); - }) - .catch(() => setChangelog([])) - .finally(() => setChangelogLoading(false)); - }, [showChangelog]); + const newTheme = theme === "dark" ? "light" : "dark"; + setTheme(newTheme); + if (typeof window !== "undefined") localStorage.setItem("theme", newTheme); + }; useEffect(() => { fetch(`/api/workspace/${workspace.groupId}/settings/general/configuration`) - .then(res => res.json()) - .then(data => { - setDocsEnabled(data.value.guides?.enabled ?? false); - setAlliesEnabled(data.value.allies?.enabled ?? false); - setSessionsEnabled(data.value.sessions?.enabled ?? false); - setNoticesEnabled(data.value.notices?.enabled ?? false); - setPoliciesEnabled(data.value.policies?.enabled ?? false); + .then((res) => res.json()) + .then((data) => { + setDocsEnabled(data.value?.guides?.enabled ?? false); + setAlliesEnabled(data.value?.allies?.enabled ?? false); + setSessionsEnabled(data.value?.sessions?.enabled ?? false); + setNoticesEnabled(data.value?.notices?.enabled ?? false); + setPoliciesEnabled(data.value?.policies?.enabled ?? false); }) .catch(() => setDocsEnabled(false)); }, [workspace.groupId]); @@ -226,490 +153,413 @@ const Sidebar: NextPage = ({ isCollapsed, setIsCollapsed }) => { useEffect(() => { if (policiesEnabled) { fetch(`/api/workspace/${workspace.groupId}/policies/pending`) - .then(res => res.json()) - .then(data => { - if (data.success) { - setPendingPolicyCount(data.count); - } - }) + .then((res) => res.json()) + .then((data) => data.success && setPendingPolicyCount(data.count)) .catch(() => setPendingPolicyCount(0)); } }, [workspace.groupId, policiesEnabled]); useEffect(() => { - if (noticesEnabled) { - if (workspace.yourPermission?.includes("approve_notices") || workspace.yourPermission?.includes("manage_notices") || workspace.yourPermission?.includes("admin")) { - fetch(`/api/workspace/${workspace.groupId}/activity/notices/count`) - .then(res => res.json()) - .then(data => { - if (data.success) { - setPendingNoticesCount(data.count || 0); - } - }) - .catch(() => setPendingNoticesCount(0)); - } + if (noticesEnabled && (workspace.yourPermission?.includes("approve_notices") || workspace.yourPermission?.includes("manage_notices") || workspace.yourPermission?.includes("admin"))) { + fetch(`/api/workspace/${workspace.groupId}/activity/notices/count`) + .then((res) => res.json()) + .then((data) => data.success && setPendingNoticesCount(data.count || 0)) + .catch(() => setPendingNoticesCount(0)); } }, [workspace.groupId, noticesEnabled, workspace.yourPermission]); return ( <> - {!isMobileMenuOpen && ( - - )} - - {isMobileMenuOpen && ( -
setIsMobileMenuOpen(false)} - /> - )} -
- +
- setShowCopyright(false)} - className="relative z-50" - > - - -
- + {mobileMoreOpen && ( + <> +
- setShowOrbitInfo(false)} - className="relative z-50" +
-
- setShowChangelog(false)} - className="relative z-50" - > - - -
+ +
+ +
+ +
+ +
+
+ + )} - ) -} + ); +}; -export default Sidebar +export default Sidebar; diff --git a/next.config.js b/next.config.js index abee5674..aaeaba17 100644 --- a/next.config.js +++ b/next.config.js @@ -1,51 +1,62 @@ /** @type {import('next').NextConfig} */ +const withPWA = require('next-pwa')({ + dest: 'public', + register: true, + skipWaiting: true, + disable: process.env.NODE_ENV === 'development', +}); + const nextConfig = { - reactStrictMode: true, - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'tr.rbxcdn.com', - }, - ], - }, - env: { - NEXT_PUBLIC_DATABASE_CHECK: process.env.DATABASE_URL ? 'true' : '', - }, - async headers() { - return [ - { - // Apply these headers to all routes - source: '/:path*', - headers: [ - { - key: 'X-DNS-Prefetch-Control', - value: 'on', - }, - { - key: 'X-XSS-Protection', - value: '1; mode=block', - }, - { - key: 'X-Content-Type-Options', - value: 'nosniff', - }, - { - key: 'Referrer-Policy', - value: 'origin-when-cross-origin', - }, - { - key: 'X-Frame-Options', - value: 'SAMEORIGIN', - }, - { - key: 'Content-Security-Policy', - value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://widget.intercom.io https://js.intercomcdn.com https://cdn.posthog.com https://js.posthog.com https://cdn.intercom.com https://uploads.intercomcdn.com https://uranus.planetaryapp.cloud; script-src-elem 'self' 'unsafe-inline' https://static.cloudflareinsights.com/ https://*.posthog.com https://widget.intercom.io https://js.intercomcdn.com https://cdn.posthog.com https://js.posthog.com https://cdn.intercom.com https://uploads.intercomcdn.com https://uranus.planetaryapp.cloud; script-src-attr 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://fonts.intercomcdn.com; img-src 'self' data: https: blob:; connect-src 'self' https: https://api.intercom.io https://events.posthog.com https://app.posthog.com https://uranus.planetaryapp.cloud wss://*.intercom.io wss:; frame-src 'self' https://widget.intercom.io; frame-ancestors 'self'; base-uri 'self'; form-action 'self';", - }, - ], - }, - ]; - }, - }; - - module.exports = nextConfig; + reactStrictMode: true, + // This allows the build to proceed by acknowledging the custom webpack usage + experimental: { + turbopack: {}, + }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'tr.rbxcdn.com', + }, + ], + }, + env: { + NEXT_PUBLIC_DATABASE_CHECK: process.env.DATABASE_URL ? 'true' : '', + }, + async headers() { + return [ + { + // Apply these headers to all routes + source: '/:path*', + headers: [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'origin-when-cross-origin', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://widget.intercom.io https://js.intercomcdn.com https://cdn.posthog.com https://js.posthog.com https://cdn.intercom.com https://uploads.intercomcdn.com https://uranus.planetaryapp.cloud; script-src-elem 'self' 'unsafe-inline' https://static.cloudflareinsights.com/ https://*.posthog.com https://widget.intercom.io https://js.intercomcdn.com https://cdn.posthog.com https://js.posthog.com https://cdn.intercom.com https://uploads.intercomcdn.com https://uranus.planetaryapp.cloud; script-src-attr 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://fonts.intercomcdn.com; img-src 'self' data: https: blob:; connect-src 'self' https: https://api.intercom.io https://events.posthog.com https://app.posthog.com https://uranus.planetaryapp.cloud wss://*.intercom.io wss:; frame-src 'self' https://widget.intercom.io; frame-ancestors 'self'; base-uri 'self'; form-action 'self';", + }, + ], + }, + ]; + }, +}; + +module.exports = withPWA(nextConfig); diff --git a/pages/_document.tsx b/pages/_document.tsx index 27d2e0f7..20b7367d 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -4,6 +4,16 @@ export default function Document() { return ( + {/* PWA / iOS Standalone Mode Tags */} + + + + + + + {/* Favicon / Touch Icon (Crucial for iOS to treat it as an app) */} + + - {/* Prevent MIME type sniffing */} - {/* Additional XSS protection */} - {/* Control referrer information */} label { - flex: 0 0 auto; - margin-right: 0.5rem; - user-select: none; - } - - > div { - flex: 1 1 auto; - } - } - } \ No newline at end of file +@media all and (display-mode: standalone) { + aside, + .lg\:flex, + .w-64, + [class*="Sidebar"] { + display: none !important; + width: 0 !important; + } + + main, + .flex-1, + [class*="layout-wrapper"], + [class*="main-content"] { + margin-left: 0 !important; + padding-left: 0 !important; + width: 100% !important; + max-width: 100vw !important; + } + + nav.fixed.bottom-0 { + padding-bottom: env(safe-area-inset-bottom, 32px) !important; + height: calc(85px + env(safe-area-inset-bottom, 0px)) !important; + display: flex !important; + align-items: flex-start !important; + padding-top: 14px !important; + backdrop-filter: blur(12px); + background-color: rgba(255, 255, 255, 0.75) !important; + z-index: 99999; + } + + .dark nav.fixed.bottom-0 { + background-color: rgba(9, 9, 11, 0.85) !important; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + body { + padding-bottom: calc(110px + env(safe-area-inset-bottom, 20px)) !important; + } + + .pagePadding { + padding-top: calc(1rem + env(safe-area-inset-top, 20px)) !important; + padding-left: 1.25rem !important; + padding-right: 1.25rem !important; + } +}