From ff2894e76781517af58cd519d4d8e41e01ef15ce Mon Sep 17 00:00:00 2001
From: rimar1337 <132627503+rimar1337@users.noreply.github.com>
Date: Sat, 18 Oct 2025 08:09:04 +0000
Subject: [PATCH] material 3 initial design
---
package-lock.json | 12 ++++++++++++
package.json | 1 +
vite.config.ts | 3 ++-
src/auto-imports.d.ts | 1 +
src/components/Header.tsx | 29 +++++++++++++++++++++++++++++
src/components/InfiniteCustomFeed.tsx | 9 +++++----
src/components/Login.tsx | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------
src/routes/__root.tsx | 527 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------
src/routes/index.tsx | 36 +++++++++++++++++++++++-------------
src/styles/app.css | 8 ++++++++
src/routes/profile.$did/index.tsx | 17 ++++++++++++++---
src/routes/profile.$did/post.$rkey.tsx | 45 ++++++++++++++-------------------------------
12 file(s) changed, 656 insertion(s)(+), 232 deletion(s)(-)
diff --git a/package-lock.json b/package-lock.json
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"@iconify-icon/react": "^3.0.1",
"@iconify-json/material-symbols": "^1.2.42",
"@iconify-json/mdi": "^1.2.3",
+ "@iconify/json": "^2.2.396",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@testing-library/dom": "^10.4.0",
@@ -1681,6 +1682,17 @@
"license": "Apache-2.0",
"dependencies": {
"@iconify/types": "*"
+ }
+ },
+ "node_modules/@iconify/json": {
+ "version": "2.2.396",
+ "resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz",
+ "integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/types": "*",
+ "pathe": "^2.0.0"
}
},
"node_modules/@iconify/types": {
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"@iconify-icon/react": "^3.0.1",
"@iconify-json/material-symbols": "^1.2.42",
"@iconify-json/mdi": "^1.2.3",
+ "@iconify/json": "^2.2.396",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@testing-library/dom": "^10.4.0",
diff --git a/vite.config.ts b/vite.config.ts
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -39,12 +39,13 @@
IconsResolver({
prefix: 'Icon',
extension: 'jsx',
+ enabledCollections: ['mdi','material-symbols'],
}),
],
dts: 'src/auto-imports.d.ts',
}),
Icons({
- autoInstall: true,
+ //autoInstall: true,
compiler: 'jsx',
jsx: 'react'
}),
diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts
--- a/src/auto-imports.d.ts
+++ b/src/auto-imports.d.ts
@@ -8,6 +8,7 @@
declare global {
const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default
const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default
+ const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default
const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default
const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default
const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,29 @@
+import { Link, useRouter } from "@tanstack/react-router";
+
+export function Header({
+ backButtonCallback,
+ title
+}: {
+ backButtonCallback?: () => void;
+ title?: string;
+}) {
+ const router = useRouter();
+ //const what = router.history.
+ return (
+
+ {backButtonCallback ? (
{
+ e.preventDefault();
+ backButtonCallback();
+ }}
+ aria-label="Go back"
+ >
+
+ ) : (
)}
+
{title}
+
+ );
+}
diff --git a/src/components/InfiniteCustomFeed.tsx b/src/components/InfiniteCustomFeed.tsx
--- a/src/components/InfiniteCustomFeed.tsx
+++ b/src/components/InfiniteCustomFeed.tsx
@@ -1,11 +1,12 @@
import * as React from "react";
+
//import { useInView } from "react-intersection-observer";
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
import { useAuth } from "~/providers/UnifiedAuthProvider";
import {
- useQueryArbitrary,
- useQueryIdentity,
useInfiniteQueryFeedSkeleton,
+ // useQueryArbitrary,
+ // useQueryIdentity,
} from "~/utils/useQuery";
interface InfiniteCustomFeedProps {
@@ -112,10 +113,10 @@
- {isRefetching ? : }
+ {isRefetching ? : }
>
);
diff --git a/src/components/Login.tsx b/src/components/Login.tsx
--- a/src/components/Login.tsx
+++ b/src/components/Login.tsx
@@ -1,10 +1,17 @@
// src/components/Login.tsx
-import React, { useEffect, useState, useRef } from "react";
-import { useAuth } from "~/providers/UnifiedAuthProvider";
import { Agent } from "@atproto/api";
+import React, { useEffect, useRef, useState } from "react";
+
+import { useAuth } from "~/providers/UnifiedAuthProvider";
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
-export default function Login({ compact = false }: { compact?: boolean }) {
+export default function Login({
+ compact = false,
+ popup = false,
+}: {
+ compact?: boolean;
+ popup?: boolean;
+}) {
const { status, agent, logout } = useAuth();
// Loading state can be styled differently based on the prop
@@ -33,7 +40,7 @@
// Large view
if (!compact) {
return (
-
+
You are logged in!
@@ -41,7 +48,7 @@
Log out
@@ -67,14 +74,14 @@
if (!compact) {
// Large view renders the form directly in the card
return (
-
+
);
}
// Compact view renders a button that toggles the form in a dropdown
- return
;
+ return
;
}
// --- 2. The Reusable, Self-Contained Login Form Component ---
@@ -83,7 +90,7 @@
return (
-
+
{
+const CompactLoginButton = ({popup}:{popup?: boolean}) => {
const [showForm, setShowForm] = useState(false);
const formRef = useRef(null);
@@ -125,12 +132,12 @@
setShowForm(!showForm)}
- className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
+ className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors"
>
Log in
{showForm && (
-
+
)}
@@ -138,13 +145,21 @@
);
};
-const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
+const TabButton = ({
+ label,
+ active,
+ onClick,
+}: {
+ label: string;
+ active: boolean;
+ onClick: () => void;
+}) => (
{label}
@@ -169,9 +184,22 @@
};
return (
);
};
@@ -201,51 +229,109 @@
return (
);
};
// --- Profile Component (now supports a `large` prop for styling) ---
-export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => {
- const [profile, setProfile] = useState(null);
+export const ProfileThing = ({
+ agent,
+ large = false,
+}: {
+ agent: Agent | null;
+ large?: boolean;
+}) => {
+ const [profile, setProfile] = useState(null);
- useEffect(() => {
- const fetchUser = async () => {
- const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
- if (!did) return;
- try {
- const res = await agent!.getProfile({ actor: did });
- setProfile(res.data);
- } catch (e) { console.error("Failed to fetch profile", e); }
- };
- if (agent) fetchUser();
- }, [agent]);
-
- if (!profile) {
- return ( // Skeleton loader
-
- );
+ useEffect(() => {
+ const fetchUser = async () => {
+ const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
+ if (!did) return;
+ try {
+ const res = await agent!.getProfile({ actor: did });
+ setProfile(res.data);
+ } catch (e) {
+ console.error("Failed to fetch profile", e);
}
-
- return (
-
-
-
-
{profile?.displayName}
-
@{profile?.handle}
-
+ };
+ if (agent) fetchUser();
+ }, [agent]);
+
+ if (!profile) {
+ return (
+ // Skeleton loader
+
+
+
- );
-};
\ No newline at end of file
+
+ );
+ }
+
+ return (
+
+
+
+
+ {profile?.displayName}
+
+
+ @{profile?.handle}
+
+
+
+ );
+};
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -5,7 +5,7 @@
import type { QueryClient } from "@tanstack/react-query";
import {
createRootRouteWithContext,
- Link,
+ // Link,
// Outlet,
Scripts,
useLocation,
@@ -176,7 +176,7 @@
)}
-
+
@@ -186,7 +186,104 @@
*/}
-
+ }
+ ActiveIcon={ }
+ active={isHome}
+ onClickCallbback={() =>
+ navigate({
+ to: "/",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Home"
+ />
+
+
+ }
+ ActiveIcon={
+
+ }
+ active={isNotifications}
+ onClickCallbback={() =>
+ navigate({
+ to: "/notifications",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Notifications"
+ />
+ }
+ ActiveIcon={ }
+ active={location.pathname.startsWith("/feeds")}
+ onClickCallbback={() =>
+ navigate({
+ to: "/feeds",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Feeds"
+ />
+ }
+ ActiveIcon={ }
+ active={location.pathname.startsWith("/search")}
+ onClickCallbback={() =>
+ navigate({
+ to: "/search",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Search"
+ />
+
+ }
+ ActiveIcon={
+
+ }
+ active={isProfile ?? false}
+ onClickCallbback={() => {
+ if (authed && agent && agent.assertDid) {
+ //window.location.href = `/profile/${agent.assertDid}`;
+ navigate({
+ to: "/profile/$did",
+ params: { did: agent.assertDid },
+ });
+ }
+ }}
+ text="Profile"
+ />
+
+ }
+ ActiveIcon={ }
+ active={location.pathname.startsWith("/settings")}
+ onClickCallbback={() =>
+ navigate({
+ to: "/settings",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Settings"
+ />
+
+ }
+ ActiveIcon={ }
+ //active={true}
+ onClickCallbback={() => setPostOpen(true)}
+ text="Post"
+ />
+
+ {/*
) : (
- )
- }
+ )}
Profile
)}
Settings
-
- */}
+ {/* setPostOpen(true)}
type="button"
@@ -288,7 +384,7 @@
className="text-gray-600 dark:text-gray-400"
/>
Post
-
+ */}
- setPostOpen(true)}
- type="button"
- aria-label="Create Post"
- >
-
-
+ {agent?.did && (
+ setPostOpen(true)}
+ type="button"
+ aria-label="Create Post"
+ >
+
+
+ )}
-
-
-
-
- Red Dwarf{" "}
- {/*
- lite
- */}
-
-
-
-
-
-
-
{children}
@@ -368,93 +447,295 @@
-
-
-
- {!isHome ? (
-
- ) : (
-
- )}
-
Home
-
-
- {!location.pathname.startsWith("/search") ? (
-
- ) : (
-
- )}
-
Search
-
-
- {!isNotifications ? (
-
- ) : (
-
- )}
-
Notifications
-
-
{
- if (authed && agent && agent.assertDid) {
- //window.location.href = `/profile/${agent.assertDid}`;
- navigate({
- to: "/profile/$did",
- params: { did: agent.assertDid },
- });
+ {agent?.did ? (
+
+
+
}
- }}
- type="button"
- >
-
- Profile
-
-
- {!location.pathname.startsWith("/settings") ? (
-
- ) : (
-
- )}
- Settings
-
+ ActiveIcon={ }
+ active={isHome}
+ onClickCallbback={() =>
+ navigate({
+ to: "/",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Home"
+ />
+ {/*
+ {!isHome ? (
+
+ ) : (
+
+ )}
+ Home
+ */}
+ }
+ ActiveIcon={ }
+ active={location.pathname.startsWith("/search")}
+ onClickCallbback={() =>
+ navigate({
+ to: "/search",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Search"
+ />
+ {/*
+ {!location.pathname.startsWith("/search") ? (
+
+ ) : (
+
+ )}
+ Search
+ */}
+
+ }
+ ActiveIcon={
+
+ }
+ active={isNotifications}
+ onClickCallbback={() =>
+ navigate({
+ to: "/notifications",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Notifications"
+ />
+ {/*
+ {!isNotifications ? (
+
+ ) : (
+
+ )}
+ Notifications
+ */}
+
+ }
+ ActiveIcon={
+
+ }
+ active={isProfile ?? false}
+ onClickCallbback={() => {
+ if (authed && agent && agent.assertDid) {
+ //window.location.href = `/profile/${agent.assertDid}`;
+ navigate({
+ to: "/profile/$did",
+ params: { did: agent.assertDid },
+ });
+ }
+ }}
+ text="Profile"
+ />
+ {/* {
+ if (authed && agent && agent.assertDid) {
+ //window.location.href = `/profile/${agent.assertDid}`;
+ navigate({
+ to: "/profile/$did",
+ params: { did: agent.assertDid },
+ });
+ }
+ }}
+ type="button"
+ >
+
+ Profile
+ */}
+
+ }
+ ActiveIcon={ }
+ active={location.pathname.startsWith("/settings")}
+ onClickCallbback={() =>
+ navigate({
+ to: "/settings",
+ //params: { did: agent.assertDid },
+ })
+ }
+ text="Settings"
+ />
+ {/*
+ {!location.pathname.startsWith("/settings") ? (
+
+ ) : (
+
+ )}
+ Settings
+ */}
+
+
+ ) : (
+
+
+
+
+ Red Dwarf{" "}
+ {/*
+ lite
+ */}
+
+
+
+
+
-
+ )}
-
+
>
+ );
+}
+
+function MaterialNavItem({
+ InactiveIcon,
+ ActiveIcon,
+ text,
+ active,
+ onClickCallbback,
+ small,
+}: {
+ InactiveIcon: React.ReactElement;
+ ActiveIcon: React.ReactElement;
+ text: string;
+ active: boolean;
+ onClickCallbback: () => void;
+ small?: boolean;
+}) {
+ if (small)
+ return (
+ {
+ onClickCallbback();
+ }}
+ >
+
+ {active ? ActiveIcon : InactiveIcon}
+
+
+ {text}
+
+
+ );
+
+ return (
+ {
+ onClickCallbback();
+ }}
+ >
+
+ {active ? ActiveIcon : InactiveIcon}
+
+
+ {text}
+
+
+ );
+}
+
+function MaterialPillButton({
+ InactiveIcon,
+ ActiveIcon,
+ text,
+ //active,
+ onClickCallbback,
+ small,
+}: {
+ InactiveIcon: React.ReactElement;
+ ActiveIcon: React.ReactElement;
+ text: string;
+ //active: boolean;
+ onClickCallbback: () => void;
+ small?: boolean;
+}) {
+ const active = false;
+ return (
+ {
+ onClickCallbback();
+ }}
+ >
+
+ {active ? ActiveIcon : InactiveIcon}
+
+
+ {text}
+
+
);
}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -3,6 +3,7 @@
import * as React from "react";
import { useEffect, useLayoutEffect } from "react";
+import { Header } from "~/components/Header";
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
import { useAuth } from "~/providers/UnifiedAuthProvider";
import {
@@ -353,9 +354,9 @@
-
- {savedFeeds.length > 0 ? (
- savedFeeds.map((item: any, idx: number) => {
+ {savedFeeds.length > 0 ? (
+
+ {savedFeeds.map((item: any, idx: number) => {
const label = item.value.split("/").pop() || item.value;
const isActive = selectedFeed === item.value;
return (
@@ -363,27 +364,36 @@
key={item.value || idx}
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
isActive
- ? "bg-gray-500 text-white"
- : item.pinned
- ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
- : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
+ ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
+ : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
+ // ? "bg-gray-500 text-white"
+ // : item.pinned
+ // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
+ // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
}`}
onClick={() => setSelectedFeed(item.value)}
title={item.value}
>
{label}
{item.pinned && (
-
+
★
)}
);
- })
- ) : (
- Home
- )}
-
+ })}
+
+ ) : (
+ //
Home
+
+ )}
{/* {isFeedLoading &&
Loading...
}
{feedError &&
{feedError.message}
}
{!isFeedLoading && !feedError && feed.length === 0 && (
diff --git a/src/styles/app.css b/src/styles/app.css
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -1,3 +1,4 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap');
@import "tailwindcss";
/* @theme {
@@ -78,4 +79,11 @@
color: rgb(29, 122, 242);
word-break: break-all;
}
+}
+
+.font-inter {
+ font-family: "Inter", sans-serif;
+}
+.font-roboto {
+ font-family: "Roboto", sans-serif;
}
\ No newline at end of file
diff --git a/src/routes/profile.$did/index.tsx b/src/routes/profile.$did/index.tsx
--- a/src/routes/profile.$did/index.tsx
+++ b/src/routes/profile.$did/index.tsx
@@ -1,7 +1,8 @@
import { useQueryClient } from "@tanstack/react-query";
-import { createFileRoute, Link } from "@tanstack/react-router";
+import { createFileRoute } from "@tanstack/react-router";
import React from "react";
+import { Header } from "~/components/Header";
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
import { useAuth } from "~/providers/UnifiedAuthProvider";
import { toggleFollow, useGetFollowState } from "~/utils/followState";
@@ -104,7 +105,17 @@
return (
<>
-
+
{
+ if (window.history.length > 1) {
+ window.history.back();
+ } else {
+ window.location.assign("/");
+ }
+ }}
+ />
+ {/*
Profile
-
+ */}
{/* Profile Header */}
diff --git a/src/routes/profile.$did/post.$rkey.tsx b/src/routes/profile.$did/post.$rkey.tsx
--- a/src/routes/profile.$did/post.$rkey.tsx
+++ b/src/routes/profile.$did/post.$rkey.tsx
@@ -2,6 +2,7 @@
import { createFileRoute } from "@tanstack/react-router";
import React, { useLayoutEffect } from "react";
+import { Header } from "~/components/Header";
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
import {
@@ -296,38 +297,20 @@
return (
<>
-
- {!nopics ? (
- {
- e.preventDefault();
- if (window.history.length > 1) {
- window.history.back();
- } else {
- window.location.assign("/");
+ {
+ if (window.history.length > 1) {
+ window.history.back();
+ } else {
+ window.location.assign("/");
+ }
}
- }}
- aria-label="Go back"
- >
- ←
-
- ) : (
- {
- e.preventDefault();
- nopics();
- }}
- aria-label="Go back"
- >
- ←
-
- )}
- Post
-
+ }
+ />
{parentsLoading && (
--
tangled.sh