Profile: first prototype
This commit is contained in:
		
							parent
							
								
									54db6d4fbe
								
							
						
					
					
						commit
						664c1568d0
					
				
					 7 changed files with 474 additions and 80 deletions
				
			
		
							
								
								
									
										252
									
								
								src/profiles/Profile.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/profiles/Profile.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,252 @@
 | 
			
		|||
import {
 | 
			
		||||
  createRenderEffect,
 | 
			
		||||
  createResource,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  For,
 | 
			
		||||
  onCleanup,
 | 
			
		||||
  Show,
 | 
			
		||||
  type Component,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import Scaffold from "../material/Scaffold";
 | 
			
		||||
import { AppBar, Avatar, Button, IconButton, Toolbar } from "@suid/material";
 | 
			
		||||
import { Close, MoreVert, Verified } from "@suid/icons-material";
 | 
			
		||||
import { Title } from "../material/typography";
 | 
			
		||||
import { useNavigate, useParams } from "@solidjs/router";
 | 
			
		||||
import { useSessionForAcctStr } from "../masto/clients";
 | 
			
		||||
import { resolveCustomEmoji } from "../masto/toot";
 | 
			
		||||
import { FastAverageColor } from "fast-average-color";
 | 
			
		||||
import { useWindowSize } from "@solid-primitives/resize-observer";
 | 
			
		||||
import { css } from "solid-styled";
 | 
			
		||||
import { createTimeline } from "../masto/timelines";
 | 
			
		||||
import TootList from "../timelines/TootList";
 | 
			
		||||
import { createIntersectionObserver } from "@solid-primitives/intersection-observer";
 | 
			
		||||
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
 | 
			
		||||
 | 
			
		||||
const Profile: Component = () => {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const params = useParams<{ acct: string; id: string }>();
 | 
			
		||||
  const acctText = () => decodeURIComponent(params.acct);
 | 
			
		||||
  const session = useSessionForAcctStr(acctText);
 | 
			
		||||
  const [bannerSampledColors, setBannerSampledColors] = createSignal<{
 | 
			
		||||
    average: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
  }>();
 | 
			
		||||
  const windowSize = useWindowSize();
 | 
			
		||||
  const time = createTimeSource();
 | 
			
		||||
 | 
			
		||||
  const [scrolledPastBanner, setScrolledPastBanner] = createSignal(false);
 | 
			
		||||
  const obx = new IntersectionObserver(
 | 
			
		||||
    (entries) => {
 | 
			
		||||
      const ent = entries[0];
 | 
			
		||||
      if (ent.intersectionRatio < 0.1) {
 | 
			
		||||
        setScrolledPastBanner(true);
 | 
			
		||||
      } else {
 | 
			
		||||
        setScrolledPastBanner(false);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      threshold: 0.1,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  onCleanup(() => obx.disconnect());
 | 
			
		||||
 | 
			
		||||
  const [profile] = createResource(
 | 
			
		||||
    () => [session().client, params.id] as const,
 | 
			
		||||
    async ([client, id]) => {
 | 
			
		||||
      return await client.v1.accounts.$select(id).fetch();
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [recentToots] = createTimeline(
 | 
			
		||||
    () => session().client.v1.accounts.$select(params.id).statuses,
 | 
			
		||||
    () => 20,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const bannerImg = () => profile()?.header;
 | 
			
		||||
  const avatarImg = () => profile()?.avatar;
 | 
			
		||||
  const displayName = () =>
 | 
			
		||||
    resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
 | 
			
		||||
  const fullUsername = () => `@${profile()?.acct ?? "..."}`; // TODO: full user name
 | 
			
		||||
  const description = () => profile()?.note;
 | 
			
		||||
 | 
			
		||||
  css`
 | 
			
		||||
    .intro {
 | 
			
		||||
      background-color: var(--tutu-color-surface-d);
 | 
			
		||||
      color: var(--tutu-color-on-surface);
 | 
			
		||||
      padding: 16px 12px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-flow: column nowrap;
 | 
			
		||||
      gap: 16px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .acct-grp {
 | 
			
		||||
      display: grid;
 | 
			
		||||
      grid-template-columns: auto 1fr auto;
 | 
			
		||||
      gap: 16px;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .name-grp {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-flow: column nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table.acct-fields {
 | 
			
		||||
      & td > :global(a) {
 | 
			
		||||
        display: inline-flex;
 | 
			
		||||
        min-height: 44px;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        color: inherit;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      & :global(a > .invisible) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      & :global(svg) {
 | 
			
		||||
        vertical-align: middle;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .page-title {
 | 
			
		||||
      flex-grow: 1;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Scaffold
 | 
			
		||||
      topbar={
 | 
			
		||||
        <AppBar
 | 
			
		||||
          position="static"
 | 
			
		||||
          color={scrolledPastBanner() ? "primary" : "transparent"}
 | 
			
		||||
          elevation={scrolledPastBanner() ? undefined : 0}
 | 
			
		||||
        >
 | 
			
		||||
          <Toolbar
 | 
			
		||||
            variant="dense"
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              color: bannerSampledColors()?.text,
 | 
			
		||||
              paddingTop: "var(--safe-area-inset-top)",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <IconButton color="inherit" onClick={[navigate, -1]}>
 | 
			
		||||
              <Close />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
            <Title
 | 
			
		||||
              use:solid-styled
 | 
			
		||||
              class="page-title"
 | 
			
		||||
              style={{
 | 
			
		||||
                visibility: scrolledPastBanner() ? undefined : "hidden",
 | 
			
		||||
              }}
 | 
			
		||||
              ref={(e: HTMLElement) =>
 | 
			
		||||
                createRenderEffect(() => (e.innerHTML = displayName()))
 | 
			
		||||
              }
 | 
			
		||||
            ></Title>
 | 
			
		||||
            <IconButton color="inherit">
 | 
			
		||||
              <MoreVert />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Toolbar>
 | 
			
		||||
        </AppBar>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          width: "100%",
 | 
			
		||||
          height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
 | 
			
		||||
          "margin-top":
 | 
			
		||||
            "calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <img
 | 
			
		||||
          ref={(e) => obx.observe(e)}
 | 
			
		||||
          src={bannerImg()}
 | 
			
		||||
          style={{
 | 
			
		||||
            "object-fit": "contain",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            height: "100%",
 | 
			
		||||
          }}
 | 
			
		||||
          crossOrigin="anonymous"
 | 
			
		||||
          onLoad={async (event) => {
 | 
			
		||||
            const ins = new FastAverageColor();
 | 
			
		||||
            const colors = ins.getColor(event.currentTarget);
 | 
			
		||||
            setBannerSampledColors({
 | 
			
		||||
              average: colors.hex,
 | 
			
		||||
              text: colors.isDark ? "white" : "black",
 | 
			
		||||
            });
 | 
			
		||||
          }}
 | 
			
		||||
        ></img>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        class="intro"
 | 
			
		||||
        style={{
 | 
			
		||||
          "background-color": bannerSampledColors()?.average,
 | 
			
		||||
          color: bannerSampledColors()?.text,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div class="acct-grp">
 | 
			
		||||
          <Avatar
 | 
			
		||||
            src={avatarImg()}
 | 
			
		||||
            sx={{
 | 
			
		||||
              marginTop: "calc(-16px - 72px / 2)",
 | 
			
		||||
              width: "72px",
 | 
			
		||||
              height: "72px",
 | 
			
		||||
            }}
 | 
			
		||||
          ></Avatar>
 | 
			
		||||
          <div class="name-grp">
 | 
			
		||||
            <span
 | 
			
		||||
              ref={(e) =>
 | 
			
		||||
                createRenderEffect(() => (e.innerHTML = displayName()))
 | 
			
		||||
              }
 | 
			
		||||
            ></span>
 | 
			
		||||
            <span>{fullUsername()}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <Button variant="contained" color="secondary">
 | 
			
		||||
              Subscribe
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
          ref={(e) =>
 | 
			
		||||
            createRenderEffect(() => (e.innerHTML = description() || ""))
 | 
			
		||||
          }
 | 
			
		||||
        ></div>
 | 
			
		||||
        <table class="acct-fields">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <For each={profile()?.fields ?? []}>
 | 
			
		||||
              {(item, index) => {
 | 
			
		||||
                return (
 | 
			
		||||
                  <tr data-field-index={index()}>
 | 
			
		||||
                    <td>{item.name}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <Show when={item.verifiedAt}>
 | 
			
		||||
                        <Verified />
 | 
			
		||||
                      </Show>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td
 | 
			
		||||
                      ref={(e) => {
 | 
			
		||||
                        createRenderEffect(() => (e.innerHTML = item.value));
 | 
			
		||||
                      }}
 | 
			
		||||
                    ></td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
            </For>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <TimeSourceProvider value={time}>
 | 
			
		||||
        <TootList
 | 
			
		||||
          threads={recentToots.list}
 | 
			
		||||
          onUnknownThread={recentToots.getPath}
 | 
			
		||||
          onChangeToot={recentToots.set}
 | 
			
		||||
        />
 | 
			
		||||
      </TimeSourceProvider>
 | 
			
		||||
    </Scaffold>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Profile;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue