Skip to content

Commit

Permalink
Show nearby posts
Browse files Browse the repository at this point in the history
  • Loading branch information
wilsonzlin committed May 5, 2024
1 parent e941aba commit 5136dd2
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 123 deletions.
20 changes: 18 additions & 2 deletions app/component/PointMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useRef,
useState,
} from "react";
import { Point } from "../util/const";
import { EdgeContext } from "../util/item";
import {
MAP_DATASET,
Expand Down Expand Up @@ -49,12 +50,18 @@ export const PointMap = ({
controllerRef,
heatmaps,
height: vpHeightPx,
nearbyQuery,
onNearbyQuery,
onNearbyQueryResults,
resultPoints,
width: vpWidthPx,
}: {
controllerRef?: MutableRefObject<PointMapController | undefined>;
heatmaps: ImageBitmap[];
height: number;
nearbyQuery: { x: number; y: number } | undefined;
onNearbyQuery: (pt: { x: number; y: number } | undefined) => void;
onNearbyQueryResults: (points: Point[] | undefined) => void;
resultPoints: undefined | { x: number; y: number }[];
width: number;
}) => {
Expand Down Expand Up @@ -135,6 +142,14 @@ export const PointMap = ({
useEffect(() => map?.setEdge(edge), [map, edge]);
useEffect(() => map?.setHeatmaps(heatmaps), [map, heatmaps]);
useEffect(() => map?.setTheme(theme), [map, theme]);
useEffect(() => {
map?.setNearbyQuery(nearbyQuery);
return () => map?.setNearbyQuery(undefined);
}, [map, nearbyQuery]);
useEffect(() => {
map?.onNearbyQueryResults(onNearbyQueryResults);
return () => map?.offNearbyQueryResults();
}, [map, onNearbyQueryResults]);
useEffect(() => {
const ac = new AbortController();
(async () => {
Expand Down Expand Up @@ -218,7 +233,8 @@ export const PointMap = ({
className="canvas"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
ptrs.putIfAbsentOrThrow(e.pointerId, {
// Weirdly, duplicate keys can happen (either pointer can have multiple pointerdown events, or pointerId values can be reused), so don't assert it doesn't exist.
ptrs.set(e.pointerId, {
start: e.nativeEvent,
prev: e.nativeEvent,
cur: e.nativeEvent,
Expand Down Expand Up @@ -285,7 +301,7 @@ export const PointMap = ({
x: vpCtrXPt + scale.pxToPt(ptr.cur.clientX - vpWidthPx / 2),
y: vpCtrYPt + scale.pxToPt(ptr.cur.clientY - vpHeightPx / 2),
};
map?.setNearbyQuery(pt);
onNearbyQuery(pt);
}
}}
onWheel={(e) => {
Expand Down
17 changes: 13 additions & 4 deletions app/page/Search.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,17 @@
}
}

.queries {
.query-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;

margin: var(--panel-margin-from-edge);
margin-bottom: 0;
margin-right: 4.5rem; /* Leave room for PageSelector. */

.QueryForm,
.add-query {
.back-to-search {
background: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--card-shadow);
Expand Down Expand Up @@ -92,7 +91,7 @@
}
}

.add-query {
.back-to-search {
padding: 0.5rem;
display: flex;
align-items: center;
Expand All @@ -101,6 +100,10 @@
}
}

@media screen and (max-width: 799px) {
margin-right: 4.5rem; /* Leave room for PageSelector. */
}

.results {
&:empty {
display: none; /* Hide padding. */
Expand All @@ -110,6 +113,12 @@
h1 {
font-size: 1.25rem;
}

> .site,
> .sub {
color: #444;
font-size: 0.9rem;
}
}
}

Expand Down
176 changes: 100 additions & 76 deletions app/page/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import assertExists from "@xtjs/lib/assertExists";
import defined from "@xtjs/lib/defined";
import findAndRemove from "@xtjs/lib/findAndRemove";
import mapExists from "@xtjs/lib/mapExists";
Expand All @@ -11,9 +10,10 @@ import { Loading } from "../component/Loading";
import { PageSwitcher } from "../component/PageSwitcher";
import { PointMap, PointMapController } from "../component/PointMap";
import { heatmapApiCall, searchApiCall } from "../util/api";
import { Point } from "../util/const";
import { useMeasure } from "../util/dom";
import { usePromise } from "../util/fetch";
import { useHnItems } from "../util/item";
import { EdgePost, useEdgePosts } from "../util/item";
import "./Search.css";

type QueryResults = Array<{
Expand All @@ -25,9 +25,11 @@ type QueryResults = Array<{
}>;

const QueryForm = ({
onSubmit,
onResults,
}: {
onResults: (results: QueryResults) => void;
onSubmit: () => void;
onResults: (results: QueryResults | undefined) => void;
}) => {
const [queryRaw, setQueryRaw] = useState("");
const [weightSimilarity, setWeightSimilarity] = useState(0.7);
Expand All @@ -38,31 +40,29 @@ const QueryForm = ({
const [showParams, setShowParams] = useState(false);

const queryReq = usePromise<QueryResults>();
useEffect(() => onResults(queryReq.data), [queryReq.data]);

return (
<form
className="QueryForm"
onSubmit={async (e) => {
onSubmit={(e) => {
e.preventDefault();
const query = queryRaw.trim();
if (!query) {
queryReq.clear();
onResults([]);
return;
}
const results = await queryReq.set((signal) =>
searchApiCall(signal, {
queryReq.set(async (signal) => {
const query = queryRaw.trim();
if (!query) {
return;
}
onSubmit();
return await searchApiCall(signal, {
query,
limit: 10,
dataset: "toppost",
weightSimilarity,
weightScore,
weightTimestamp,
decayTimestamp,
}),
);
onResults(assertExists(results));
return results;
});
});
}}
>
<div className="main">
Expand Down Expand Up @@ -262,6 +262,45 @@ const HeatmapForm = ({
);
};

const Result = ({ id, item }: { id: number; item: EdgePost }) => {
if (!item || !item.ts || !item.author || !item.title) {
return null;
}
const hnUrl = `https://news.ycombinator.com/item?id=${id}`;
let url, site;
if (item.url) {
url = `${item.proto}//${item.url}`;
site = item.url.split("/")[0];
} else {
url = hnUrl;
site = "news.ycombinator.com";
}
const ts = DateTime.fromJSDate(item.ts);
const ago = ts.toRelative();
return (
<div key={id} className="result">
<p className="site">{site}</p>
<a href={url} target="_blank" rel="noopener noreferrer">
<h1 dangerouslySetInnerHTML={{ __html: item.title ?? "" }} />
</a>
<p className="sub">
<a href={hnUrl} target="_blank" rel="noopener noreferrer">
{item.score} point{item.score == 1 ? "" : "s"}
</a>{" "}
by{" "}
<a
href={`https://news.ycombinator.com/user?id=${item.author}`}
target="_blank"
rel="noopener noreferrer"
>
{item.author}
</a>{" "}
{ago}
</p>
</div>
);
};

export const SearchPage = () => {
const [$root, setRootElem] = useState<HTMLDivElement | null>(null);
const rootDim = useMeasure($root);
Expand All @@ -277,18 +316,21 @@ export const SearchPage = () => {
[heatmapQueries],
);

const [results, setResults] = useState<QueryResults>([]);
const [queryResults, setQueryResults] = useState<QueryResults>();
const [shouldAnimateToResults, setShouldAnimateToResults] = useState(false);
useEffect(() => {
if (!shouldAnimateToResults) {
return;
}
setShouldAnimateToResults(false);
if (!queryResults?.length) {
return;
}
let xMinPt = Infinity;
let xMaxPt = -Infinity;
let yMinPt = Infinity;
let yMaxPt = -Infinity;
for (const p of results) {
for (const p of queryResults) {
xMinPt = Math.min(xMinPt, p.x);
xMaxPt = Math.max(xMaxPt, p.x);
yMinPt = Math.min(yMinPt, p.y);
Expand All @@ -305,15 +347,28 @@ export const SearchPage = () => {
ANIM_MS,
);
}, [shouldAnimateToResults]);
const items = useHnItems(results.map((r) => r.id));
const [nearbyQuery, setNearbyQuery] = useState<{ x: number; y: number }>();
const [nearbyResults, setNearbyResults] = useState<Point[]>();
const itemIds = useMemo(
() => [
...(queryResults?.map((r) => r.id) ?? []),
...(nearbyResults?.map((r) => r.id) ?? []),
],
[queryResults, nearbyResults],
);
const items = useEdgePosts(itemIds);
const results = nearbyResults ?? queryResults;

return (
<div ref={setRootElem} className="SearchPage">
<PointMap
controllerRef={mapCtl}
heatmaps={heatmaps}
height={rootDim?.height ?? 0}
resultPoints={results}
nearbyQuery={nearbyQuery}
onNearbyQuery={setNearbyQuery}
onNearbyQueryResults={setNearbyResults}
resultPoints={nearbyQuery ? undefined : queryResults}
width={rootDim?.width ?? 0}
/>

Expand Down Expand Up @@ -375,72 +430,41 @@ export const SearchPage = () => {
</div>

<div className="panel">
<div className="queries">
<div className="query-container">
<QueryForm
onSubmit={() => {
setNearbyQuery(undefined);
setNearbyResults(undefined);
}}
onResults={(results) => {
setResults(results);
setQueryResults(results);
// Only animate when results come in, not for any other reason that `results` changes (e.g. clearing, deleting).
if (results.length) {
if (results?.length && !nearbyQuery) {
setShouldAnimateToResults(true);
}
}}
/>

{nearbyResults && queryResults && (
<button
className="back-to-search"
onClick={() => {
setNearbyQuery(undefined);
setNearbyResults(undefined);
setShouldAnimateToResults(true);
}}
>
Back to search
</button>
)}
</div>

<div className="results">
{results?.map(({ id, sim, final_score }) => {
const item = items[id];
if (!item || !item.time || !item.by || !item.title) {
return;
}
const hnUrl = `https://news.ycombinator.com/item?id=${id}`;
let url, site;
if (item.url) {
try {
const parsed = new URL(item.url);
url = item.url;
site = parsed.hostname.replace(/^www\./, "");
} catch {
return;
}
} else {
url = hnUrl;
site = "news.ycombinator.com";
}
const ts = DateTime.fromJSDate(item.time);
const ago = DateTime.now()
.diff(ts)
.rescale()
.toHuman({ unitDisplay: "long" })
.split(",")[0];
return (
<div key={id} className="result">
<div>
<p className="site">{site}</p>
<div>
{sim} {final_score}
</div>
</div>
<a href={url} target="_blank" rel="noopener noreferrer">
<h1 dangerouslySetInnerHTML={{ __html: item.title ?? "" }} />
</a>
<p>
<a href={hnUrl} target="_blank" rel="noopener noreferrer">
{item.score} point{item.score == 1 ? "" : "s"}
</a>{" "}
by{" "}
<a
href={`https://news.ycombinator.com/user?id=${item.by}`}
target="_blank"
rel="noopener noreferrer"
>
{item.by}
</a>{" "}
{ago} ago
</p>
</div>
);
})}
{results?.map(({ id }) =>
mapExists(items[id], (item) => (
<Result key={id} id={id} item={item} />
)),
)}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/util/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const vEdgePost = new VStruct({
proto: new VString(),
found_in_archive: new VBoolean(),
});
type EdgePost = Valid<typeof vEdgePost>;
export type EdgePost = Valid<typeof vEdgePost>;

// Use fast edge.
export const cachedFetchEdgePost = async (
Expand Down
Loading

0 comments on commit 5136dd2

Please sign in to comment.