implement the thing
seems to work pretty okay.
This commit is contained in:
parent
b94c19a64d
commit
c5e7bf307a
13 changed files with 587 additions and 394 deletions
42
README.md
42
README.md
|
@ -15,45 +15,3 @@ npm run dev
|
||||||
Then visit <http://localhost:3000> to preview your app.
|
Then visit <http://localhost:3000> to preview your app.
|
||||||
|
|
||||||
For more, see <https://observablehq.com/framework/getting-started>.
|
For more, see <https://observablehq.com/framework/getting-started>.
|
||||||
|
|
||||||
## Project structure
|
|
||||||
|
|
||||||
A typical Framework project looks like this:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
.
|
|
||||||
├─ src
|
|
||||||
│ ├─ components
|
|
||||||
│ │ └─ timeline.js # an importable module
|
|
||||||
│ ├─ data
|
|
||||||
│ │ ├─ launches.csv.js # a data loader
|
|
||||||
│ │ └─ events.json # a static data file
|
|
||||||
│ ├─ example-dashboard.md # a page
|
|
||||||
│ ├─ example-report.md # another page
|
|
||||||
│ └─ index.md # the home page
|
|
||||||
├─ .gitignore
|
|
||||||
├─ observablehq.config.js # the app config file
|
|
||||||
├─ package.json
|
|
||||||
└─ README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
**`src`** - This is the “source root” — where your source files live. Pages go here. Each page is a Markdown file. Observable Framework uses [file-based routing](https://observablehq.com/framework/project-structure#routing), which means that the name of the file controls where the page is served. You can create as many pages as you like. Use folders to organize your pages.
|
|
||||||
|
|
||||||
**`src/index.md`** - This is the home page for your app. You can have as many additional pages as you’d like, but you should always have a home page, too.
|
|
||||||
|
|
||||||
**`src/data`** - You can put [data loaders](https://observablehq.com/framework/data-loaders) or static data files anywhere in your source root, but we recommend putting them here.
|
|
||||||
|
|
||||||
**`src/components`** - You can put shared [JavaScript modules](https://observablehq.com/framework/imports) anywhere in your source root, but we recommend putting them here. This helps you pull code out of Markdown files and into JavaScript modules, making it easier to reuse code across pages, write tests and run linters, and even share code with vanilla web applications.
|
|
||||||
|
|
||||||
**`observablehq.config.js`** - This is the [app configuration](https://observablehq.com/framework/config) file, such as the pages and sections in the sidebar navigation, and the app’s title.
|
|
||||||
|
|
||||||
## Command reference
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| ----------------- | -------------------------------------------------------- |
|
|
||||||
| `npm install` | Install or reinstall dependencies |
|
|
||||||
| `npm run dev` | Start local preview server |
|
|
||||||
| `npm run build` | Build your static site, generating `./dist` |
|
|
||||||
| `npm run deploy` | Deploy your app to Observable |
|
|
||||||
| `npm run clean` | Clear the local data loader cache |
|
|
||||||
| `npm run observable` | Run commands like `observable help` |
|
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
// See https://observablehq.com/framework/config for documentation.
|
// See https://observablehq.com/framework/config for documentation.
|
||||||
export default {
|
export default {
|
||||||
// The app’s title; used in the sidebar and webpage titles.
|
// The app’s title; used in the sidebar and webpage titles.
|
||||||
title: "Vrg Archive",
|
title: "/vrg/ Archive",
|
||||||
|
|
||||||
// The pages and sections in the sidebar. If you don’t specify this option,
|
pages: [
|
||||||
// all pages will be listed in alphabetical order. Listing pages explicitly
|
{ name: "Thread Browser", path: "thread-browser" },
|
||||||
// lets you organize them into sections and have unlisted pages.
|
{ name: "Substring Search", path: "substring-search" },
|
||||||
// pages: [
|
{ name: "Full Text Search", path: "full-text-search" },
|
||||||
// {
|
],
|
||||||
// name: "Examples",
|
|
||||||
// pages: [
|
|
||||||
// {name: "Dashboard", path: "/example-dashboard"},
|
|
||||||
// {name: "Report", path: "/example-report"}
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Content to add to the head of the page, e.g. for a favicon:
|
// Content to add to the head of the page, e.g. for a favicon:
|
||||||
head: '<link rel="icon" href="observable.png" type="image/png" sizes="32x32">',
|
head: '<link rel="icon" href="observable.png" type="image/png" sizes="32x32">',
|
||||||
|
@ -22,13 +15,17 @@ export default {
|
||||||
// The path to the source root.
|
// The path to the source root.
|
||||||
root: "src",
|
root: "src",
|
||||||
|
|
||||||
|
duckdb: {
|
||||||
|
extensions: ["fts"],
|
||||||
|
},
|
||||||
|
|
||||||
// Some additional configuration options and their defaults:
|
// Some additional configuration options and their defaults:
|
||||||
// theme: "default", // try "light", "dark", "slate", etc.
|
// theme: "default", // try "light", "dark", "slate", etc.
|
||||||
// header: "", // what to show in the header (HTML)
|
// header: "", // what to show in the header (HTML)
|
||||||
// footer: "Built with Observable.", // what to show in the footer (HTML)
|
// footer: "Built with Observable.", // what to show in the footer (HTML)
|
||||||
// sidebar: true, // whether to show the sidebar
|
// sidebar: true, // whether to show the sidebar
|
||||||
// toc: true, // whether to show the table of contents
|
// toc: true, // whether to show the table of contents
|
||||||
// pager: true, // whether to show previous & next links in the footer
|
pager: false, // whether to show previous & next links in the footer
|
||||||
// output: "dist", // path to the output root for build
|
// output: "dist", // path to the output root for build
|
||||||
// search: true, // activate search
|
// search: true, // activate search
|
||||||
// linkify: true, // convert URLs in Markdown to links
|
// linkify: true, // convert URLs in Markdown to links
|
||||||
|
|
147
src/components/post.jsx
Normal file
147
src/components/post.jsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import React from "npm:react";
|
||||||
|
import DOMPurify from "npm:dompurify";
|
||||||
|
|
||||||
|
// thanks to gemini 2.5 for this slop.
|
||||||
|
|
||||||
|
// Helper function to format Unix timestamp (seconds)
|
||||||
|
function formatTimestamp(unixTimestamp) {
|
||||||
|
if (!unixTimestamp) return "Invalid Date";
|
||||||
|
try {
|
||||||
|
const date = new Date(unixTimestamp * 1000);
|
||||||
|
// Simple format, adjust as needed (e.g., locale, relative time)
|
||||||
|
return date.toLocaleString(); // Example: "4/10/2025, 10:01:10 PM"
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error formatting timestamp:", unixTimestamp, e);
|
||||||
|
return "Invalid Date";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format file size
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === null || typeof bytes !== "number" || isNaN(bytes) || bytes < 0)
|
||||||
|
return "";
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormatComment({ text }) {
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = DOMPurify.sanitize(text);
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: p }}></span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- The Main Post Component ---
|
||||||
|
export function Post({ postData }) {
|
||||||
|
// Destructure with default values for safety
|
||||||
|
const {
|
||||||
|
post_num,
|
||||||
|
thread_num,
|
||||||
|
thread_title = null, // Only present if it's the OP row from the join
|
||||||
|
is_op = 0,
|
||||||
|
timestamp = null,
|
||||||
|
name = "Anonymous",
|
||||||
|
trip = null,
|
||||||
|
capcode = null,
|
||||||
|
comment = "",
|
||||||
|
media_filename = null,
|
||||||
|
media_w = null,
|
||||||
|
media_h = null,
|
||||||
|
media_size = null,
|
||||||
|
media_hash = null, // MD5 hash
|
||||||
|
media_link = null, // Direct link (may need construction)
|
||||||
|
thumb_link = null, // Thumbnail link (may need construction)
|
||||||
|
media_spoiler = 0, // Treat as boolean/int
|
||||||
|
} = postData || {}; // Handle case where postData might be null/undefined
|
||||||
|
|
||||||
|
if (!post_num) {
|
||||||
|
return <div className="post error">Error: Invalid post data provided.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOp = !!is_op; // Convert 0/1 to boolean
|
||||||
|
const postClass = `post ${isOp ? "op" : "reply"}`;
|
||||||
|
const postId = `p${post_num}`;
|
||||||
|
const postInfoId = `pi${post_num}`;
|
||||||
|
const messageId = `m${post_num}`;
|
||||||
|
const fileId = `f${post_num}`;
|
||||||
|
|
||||||
|
const hasMedia = !!media_filename;
|
||||||
|
|
||||||
|
// Construct media links if they are null but filename/hash exists (example for 4cdn)
|
||||||
|
// Adjust logic based on how links were stored or need to be generated
|
||||||
|
const effectiveMediaLink =
|
||||||
|
media_link || (hasMedia ? `https://i.4cdn.org/vg/${media_filename}` : null); // Example construction
|
||||||
|
const effectiveThumbLink =
|
||||||
|
thumb_link ||
|
||||||
|
(hasMedia
|
||||||
|
? `https://i.4cdn.org/vg/${media_filename.replace(/(\.\w+)$/, "s$1")}`
|
||||||
|
: null); // Example construction for 's' thumbnail
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={postId} className={postClass}>
|
||||||
|
{/* Post Info Bar */}
|
||||||
|
<div className="postInfo desktop" id={postInfoId}>
|
||||||
|
<span className="nameBlock">
|
||||||
|
<span className="name">{name}</span>
|
||||||
|
{trip && <span className="trip"> {trip}</span>}
|
||||||
|
{capcode && capcode !== "N" && (
|
||||||
|
<span className="capcode"> ## {capcode}</span>
|
||||||
|
)}{" "}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="dateTime"
|
||||||
|
data-utc={timestamp}
|
||||||
|
title={formatTimestamp(timestamp)}
|
||||||
|
>
|
||||||
|
{formatTimestamp(timestamp)} {/* Display formatted date/time */}
|
||||||
|
</span>
|
||||||
|
<span className="postNum desktop">
|
||||||
|
<a href={`#${postId}`} title="Link to this post">
|
||||||
|
No. {post_num}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File/Media Section */}
|
||||||
|
{hasMedia && (
|
||||||
|
<div className="file" id={fileId}>
|
||||||
|
<div className="fileText">
|
||||||
|
File:{" "}
|
||||||
|
<a href="/observable.jpg" target="_blank" rel="noopener noreferrer">
|
||||||
|
{media_filename}
|
||||||
|
</a>
|
||||||
|
{" ("}
|
||||||
|
{formatFileSize(media_size)}
|
||||||
|
{media_w && media_h && `, ${media_w}x${media_h}`}
|
||||||
|
{")"}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
className="fileThumb"
|
||||||
|
href="/observable.jpg"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{/* Basic spoiler handling: maybe blur or hide image based on state */}
|
||||||
|
<img
|
||||||
|
src="/observable.png"
|
||||||
|
alt={formatFileSize(media_size)} // Alt text with file size
|
||||||
|
data-md5={media_hash || ""} // Store md5 if available
|
||||||
|
style={{ maxWidth: "250px", maxHeight: "250px" }} // Example sizing
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Post Message/Comment */}
|
||||||
|
<blockquote className="postMessage" id={messageId}>
|
||||||
|
<FormatComment text={comment} />
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
[
|
|
||||||
{"name": "Sputnik 1", "year": 1957, "y": 10},
|
|
||||||
{"name": "Apollo 11", "year": 1969, "y": 20},
|
|
||||||
{"name": "Viking 1 and 2", "year": 1975, "y": 30},
|
|
||||||
{"name": "Space Shuttle Columbia", "year": 1981, "y": 40},
|
|
||||||
{"name": "Hubble Space Telescope", "year": 1990, "y": 50},
|
|
||||||
{"name": "ISS Construction", "year": 1998, "y": 60}
|
|
||||||
]
|
|
|
@ -1,61 +0,0 @@
|
||||||
import {csvFormat, tsvParse} from "d3-dsv";
|
|
||||||
import {utcParse} from "d3-time-format";
|
|
||||||
|
|
||||||
async function text(url) {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error(`fetch failed: ${response.status}`);
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
// “Top” vehicles
|
|
||||||
const TOP_LAUNCH_VEHICLES = new Set([
|
|
||||||
"Falcon9",
|
|
||||||
"R-7",
|
|
||||||
"R-14",
|
|
||||||
"Thor",
|
|
||||||
"DF5",
|
|
||||||
"R-36",
|
|
||||||
"Proton",
|
|
||||||
"Titan",
|
|
||||||
"Zenit",
|
|
||||||
"Atlas"
|
|
||||||
]);
|
|
||||||
|
|
||||||
// “Top” launching states
|
|
||||||
const TOP_STATES_MAP = new Map([
|
|
||||||
["US", "United States"],
|
|
||||||
["SU", "Soviet Union"],
|
|
||||||
["RU", "Russia"],
|
|
||||||
["CN", "China"]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Load and parse launch vehicles.
|
|
||||||
const launchVehicles = tsvParse(await text("https://planet4589.org/space/gcat/tsv/tables/lv.tsv"));
|
|
||||||
|
|
||||||
// Construct map to lookup vehicle family from name.
|
|
||||||
const launchVehicleFamilyMap = new Map(launchVehicles.map((d) => [d["#LV_Name"], d.LV_Family.trim()]));
|
|
||||||
|
|
||||||
// Reduce cardinality by mapping smaller states to “Other”.
|
|
||||||
function normalizeState(d) {
|
|
||||||
return TOP_STATES_MAP.get(d) ?? "Other";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce cardinality by mapping smaller launch families to “Other”.
|
|
||||||
function normalizeFamily(d) {
|
|
||||||
const family = launchVehicleFamilyMap.get(d);
|
|
||||||
return TOP_LAUNCH_VEHICLES.has(family) ? family : "Other";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dates!
|
|
||||||
const parseDate = utcParse("%Y %b %_d");
|
|
||||||
|
|
||||||
// Load and parse launch-log and trim down to smaller size.
|
|
||||||
const launchHistory = tsvParse(await text("https://planet4589.org/space/gcat/tsv/derived/launchlog.tsv"), (d) => ({
|
|
||||||
date: parseDate(d.Launch_Date.slice(0, 11)),
|
|
||||||
state: normalizeState(d.LVState),
|
|
||||||
stateId: d.LVState,
|
|
||||||
family: normalizeFamily(d.LV_Type)
|
|
||||||
})).filter((d) => d.date != null);
|
|
||||||
|
|
||||||
// Write out csv formatted data.
|
|
||||||
process.stdout.write(csvFormat(launchHistory));
|
|
3
src/data/vrgarchive.parquet.sh
Executable file
3
src/data/vrgarchive.parquet.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
curl -f https://file.vrg.party/vrgarchive-2025-04-11.parquet
|
2
src/data/vrgindex.tar.sh
Executable file
2
src/data/vrgindex.tar.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
curl -f https://file.vrg.party/vrgarchive-2025-04-11-fts-index.tar
|
|
@ -1,99 +0,0 @@
|
||||||
---
|
|
||||||
theme: dashboard
|
|
||||||
title: Example dashboard
|
|
||||||
toc: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rocket launches 🚀
|
|
||||||
|
|
||||||
<!-- Load and transform the data -->
|
|
||||||
|
|
||||||
```js
|
|
||||||
const launches = FileAttachment("data/launches.csv").csv({typed: true});
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- A shared color scale for consistency, sorted by the number of launches -->
|
|
||||||
|
|
||||||
```js
|
|
||||||
const color = Plot.scale({
|
|
||||||
color: {
|
|
||||||
type: "categorical",
|
|
||||||
domain: d3.groupSort(launches, (D) => -D.length, (d) => d.state).filter((d) => d !== "Other"),
|
|
||||||
unknown: "var(--theme-foreground-muted)"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- Cards with big numbers -->
|
|
||||||
|
|
||||||
<div class="grid grid-cols-4">
|
|
||||||
<div class="card">
|
|
||||||
<h2>United States 🇺🇸</h2>
|
|
||||||
<span class="big">${launches.filter((d) => d.stateId === "US").length.toLocaleString("en-US")}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>Russia 🇷🇺 <span class="muted">/ Soviet Union</span></h2>
|
|
||||||
<span class="big">${launches.filter((d) => d.stateId === "SU" || d.stateId === "RU").length.toLocaleString("en-US")}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>China 🇨🇳</h2>
|
|
||||||
<span class="big">${launches.filter((d) => d.stateId === "CN").length.toLocaleString("en-US")}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>Other</h2>
|
|
||||||
<span class="big">${launches.filter((d) => d.stateId !== "US" && d.stateId !== "SU" && d.stateId !== "RU" && d.stateId !== "CN").length.toLocaleString("en-US")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plot of launch history -->
|
|
||||||
|
|
||||||
```js
|
|
||||||
function launchTimeline(data, {width} = {}) {
|
|
||||||
return Plot.plot({
|
|
||||||
title: "Launches over the years",
|
|
||||||
width,
|
|
||||||
height: 300,
|
|
||||||
y: {grid: true, label: "Launches"},
|
|
||||||
color: {...color, legend: true},
|
|
||||||
marks: [
|
|
||||||
Plot.rectY(data, Plot.binX({y: "count"}, {x: "date", fill: "state", interval: "year", tip: true})),
|
|
||||||
Plot.ruleY([0])
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1">
|
|
||||||
<div class="card">
|
|
||||||
${resize((width) => launchTimeline(launches, {width}))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plot of launch vehicles -->
|
|
||||||
|
|
||||||
```js
|
|
||||||
function vehicleChart(data, {width}) {
|
|
||||||
return Plot.plot({
|
|
||||||
title: "Popular launch vehicles",
|
|
||||||
width,
|
|
||||||
height: 300,
|
|
||||||
marginTop: 0,
|
|
||||||
marginLeft: 50,
|
|
||||||
x: {grid: true, label: "Launches"},
|
|
||||||
y: {label: null},
|
|
||||||
color: {...color, legend: true},
|
|
||||||
marks: [
|
|
||||||
Plot.rectX(data, Plot.groupY({x: "count"}, {y: "family", fill: "state", tip: true, sort: {y: "-x"}})),
|
|
||||||
Plot.ruleX([0])
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1">
|
|
||||||
<div class="card">
|
|
||||||
${resize((width) => vehicleChart(launches, {width}))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Data: Jonathan C. McDowell, [General Catalog of Artificial Space Objects](https://planet4589.org/space/gcat)
|
|
|
@ -1,75 +0,0 @@
|
||||||
---
|
|
||||||
title: Example report
|
|
||||||
---
|
|
||||||
|
|
||||||
# A brief history of space exploration
|
|
||||||
|
|
||||||
This report is a brief overview of the history and current state of rocket launches and space exploration.
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
The history of rocket launches dates back to ancient China, where gunpowder-filled tubes were used as primitive forms of propulsion.
|
|
||||||
|
|
||||||
Fast-forward to the 20th century during the Cold War era, the United States and the Soviet Union embarked on a space race, a competition to innovate and explore beyond Earth.
|
|
||||||
|
|
||||||
This led to the launch of the first artificial satellite, Sputnik 1, and the crewed moon landing by Apollo 11. As technology advanced, rocket launches became synonymous with space exploration and satellite deployment.
|
|
||||||
|
|
||||||
## The Space Shuttle era
|
|
||||||
|
|
||||||
```js
|
|
||||||
import {timeline} from "./components/timeline.js";
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
const events = FileAttachment("./data/events.json").json();
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
timeline(events, {height: 300})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sputnik 1 (1957)
|
|
||||||
|
|
||||||
This was the first artificial satellite. Launched by the Soviet Union, it marked the beginning of the space age.
|
|
||||||
|
|
||||||
### Apollo 11 (1969)
|
|
||||||
|
|
||||||
The historic Apollo 11 mission, led by NASA, marked the first successful human landing on the Moon. Astronauts Neil Armstrong and Buzz Aldrin became the first humans to set foot on the lunar surface.
|
|
||||||
|
|
||||||
### Viking 1 and 2 (1975)
|
|
||||||
|
|
||||||
NASA’s Viking program successfully launched two spacecraft, Viking 1 and Viking 2, to Mars. These missions were the first to successfully land and operate on the Martian surface, conducting experiments to search for signs of life.
|
|
||||||
|
|
||||||
### Space Shuttle Columbia (1981)
|
|
||||||
|
|
||||||
The first orbital space shuttle mission, STS-1, launched the Space Shuttle Columbia on April 12, 1981. The shuttle program revolutionized space travel, providing a reusable spacecraft for a variety of missions.
|
|
||||||
|
|
||||||
### Hubble Space Telescope (1990)
|
|
||||||
|
|
||||||
The Hubble Space Telescope has provided unparalleled images and data, revolutionizing our understanding of the universe and contributing to countless astronomical discoveries.
|
|
||||||
|
|
||||||
### International Space Station (ISS) construction (1998—2011)
|
|
||||||
|
|
||||||
The ISS, a collaborative effort involving multiple space agencies, began construction with the launch of its first module, Zarya, in 1998. Over the following years, various modules were added, making the ISS a symbol of international cooperation in space exploration.
|
|
||||||
|
|
||||||
## Commercial spaceflight
|
|
||||||
|
|
||||||
After the Space Shuttle program, a new era emerged with a shift towards commercial spaceflight.
|
|
||||||
|
|
||||||
Private companies like Blue Origin, founded by Jeff Bezos in 2000, and SpaceX, founded by Elon Musk in 2002, entered the scene. These companies focused on developing reusable rocket technologies, significantly reducing launch costs.
|
|
||||||
|
|
||||||
SpaceX, in particular, achieved milestones like the first privately developed spacecraft to reach orbit (Dragon in 2010) and the first privately funded spacecraft to dock with the ISS (Dragon in 2012).
|
|
||||||
|
|
||||||
## Recent launch activity
|
|
||||||
|
|
||||||
The proliferation of commercial space companies has driven a surge in global launch activity within the last few years.
|
|
||||||
|
|
||||||
SpaceX’s Falcon 9 and Falcon Heavy, along with other vehicles from companies like Rocket Lab, have become workhorses for deploying satellites, conducting scientific missions, and ferrying crew to the ISS.
|
|
||||||
|
|
||||||
The advent of small satellite constellations, such as Starlink by SpaceX, has further fueled this increase in launches. The push for lunar exploration has added momentum to launch activities, with initiatives like NASA’s Artemis program and plans for crewed missions to the Moon and Mars.
|
|
||||||
|
|
||||||
## Looking forward
|
|
||||||
|
|
||||||
As technology continues to advance and global interest in space exploration grows, the future promises even more exciting developments in the realm of rocket launches and space travel.
|
|
||||||
|
|
||||||
Exploration will not only be limited to the Moon or Mars, but extend to other parts of our solar system such as Jupiter and Saturn’s moons, and beyond.
|
|
77
src/full-text-search.md
Normal file
77
src/full-text-search.md
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
title: Full Text Search
|
||||||
|
---
|
||||||
|
|
||||||
|
# Full Text Search
|
||||||
|
|
||||||
|
This uses [DuckDB's full text search extention](https://duckdb.org/docs/stable/extensions/full_text_search.html)
|
||||||
|
to search through all the post comments. It's freakishly fast for running entirely in your browser. The index
|
||||||
|
is built offline and loaded as parquet files.
|
||||||
|
|
||||||
|
It is a bit limited, in that it only searches stems of common words, and no search operators (I think).
|
||||||
|
But still, very fast for ~1.3 million posts.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const schema_sql = `
|
||||||
|
LOAD fts;
|
||||||
|
CREATE SCHEMA fts_main_posts;
|
||||||
|
create table fts_main_posts.dict AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_dict.parquet").href}';
|
||||||
|
create table fts_main_posts.docs AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_docs.parquet").href}';
|
||||||
|
create table fts_main_posts.fields AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_fields.parquet").href}';
|
||||||
|
create table fts_main_posts.stats AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_stats.parquet").href}';
|
||||||
|
create table fts_main_posts.stopwords AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_stopwords.parquet").href}';
|
||||||
|
create table fts_main_posts.terms AS FROM '${FileAttachment("data/vrgindex/fts_main_posts_terms.parquet").href}';
|
||||||
|
create table posts as from '${FileAttachment("data/vrgarchive.parquet").href}';
|
||||||
|
CREATE MACRO if not exists fts_main_posts.tokenize (s) AS (string_split_regex(regexp_replace(lower(strip_accents(CAST(s AS VARCHAR))), '[0-9!@#$%^&*()_+={}\\[\\]:;<>,.?~\\\/\\|''''"\`-]+', ' ', 'g'), '\\s+'));;
|
||||||
|
CREATE MACRO if not exists fts_main_posts.match_bm25 (docname, query_string, b := 0.75, conjunctive := false, k := 1.2, fields := NULL) AS ((WITH tokens AS (SELECT DISTINCT stem(unnest(fts_main_posts.tokenize(query_string)), 'porter') AS t), fieldids AS (SELECT fieldid FROM fts_main_posts.fields WHERE CASE WHEN ((fields IS NULL)) THEN (1) ELSE (field = ANY(SELECT * FROM (SELECT unnest(string_split(fields, ','))) AS fsq)) END), qtermids AS (SELECT termid FROM fts_main_posts.dict AS dict , tokens WHERE (dict.term = tokens.t)), qterms AS (SELECT termid, docid FROM fts_main_posts.terms AS terms WHERE (CASE WHEN ((fields IS NULL)) THEN (1) ELSE (fieldid = ANY(SELECT * FROM fieldids)) END AND (termid = ANY(SELECT qtermids.termid FROM qtermids)))), term_tf AS (SELECT termid, docid, count_star() AS tf FROM qterms GROUP BY docid, termid), cdocs AS (SELECT docid FROM qterms GROUP BY docid HAVING CASE WHEN (conjunctive) THEN ((count(DISTINCT termid) = (SELECT count_star() FROM tokens))) ELSE 1 END), subscores AS (SELECT docs.docid, len, term_tf.termid, tf, df, (log((((((SELECT num_docs FROM fts_main_posts.stats) - df) + 0.5) / (df + 0.5)) + 1)) * ((tf * (k + 1)) / (tf + (k * ((1 - b) + (b * (len / (SELECT avgdl FROM fts_main_posts.stats)))))))) AS subscore FROM term_tf , cdocs , fts_main_posts.docs AS docs , fts_main_posts.dict AS dict WHERE ((term_tf.docid = cdocs.docid) AND (term_tf.docid = docs.docid) AND (term_tf.termid = dict.termid))), scores AS (SELECT docid, sum(subscore) AS score FROM subscores GROUP BY docid)SELECT score FROM scores , fts_main_posts.docs AS docs WHERE ((scores.docid = docs.docid) AND (docs."name" = docname))));;
|
||||||
|
`;
|
||||||
|
const db = await DuckDBClient.of();
|
||||||
|
await db.sql([schema_sql]);
|
||||||
|
const sql = db.sql.bind(db);
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import DOMPurify from 'npm:dompurify';
|
||||||
|
````
|
||||||
|
|
||||||
|
|
||||||
|
```js
|
||||||
|
const search_term = view(
|
||||||
|
Inputs.text({
|
||||||
|
label: "Search",
|
||||||
|
placeholder: "search for something",
|
||||||
|
value: "sneed"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
const search_results = await sql`
|
||||||
|
select post_num, epoch_ms(timestamp * 1000) as post_time, comment from (select *, fts_main_posts.match_bm25(post_num, ${search_term}) as score from posts) sq where score is not null order by score desc`;
|
||||||
|
```
|
||||||
|
|
||||||
|
${search_results.numRows} results.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Plot.plot({
|
||||||
|
width: width,
|
||||||
|
x: {
|
||||||
|
interval: "month"
|
||||||
|
},
|
||||||
|
marks: [
|
||||||
|
Plot.barY(search_results, Plot.binX({"y": "count"}, { x: "post_time" , interval: "month", "tip": true}))
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
Inputs.table(search_results, { width: {"comment": 500 },
|
||||||
|
format: {
|
||||||
|
"comment": (v) => {
|
||||||
|
let p = DOMPurify.sanitize(v);
|
||||||
|
let span = document.createElement("span");
|
||||||
|
span.innerHTML = p;
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
```
|
168
src/index.md
168
src/index.md
|
@ -1,111 +1,89 @@
|
||||||
---
|
---
|
||||||
|
title: /vrg/ Archive
|
||||||
toc: false
|
toc: false
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="hero">
|
# /vrg/ Archive
|
||||||
<h1>Vrg Archive</h1>
|
|
||||||
<h2>Welcome to your new app! Edit <code style="font-size: 90%;">src/index.md</code> to change this page.</h2>
|
Welcome to the archive of Virtual Reality General threads from 4chan's /vg/ board.
|
||||||
<a href="https://observablehq.com/framework/getting-started">Get started<span style="display: inline-block; margin-left: 0.25rem;">↗︎</span></a>
|
|
||||||
|
While the [b4k archive](https://b4k.dev) also has the /vg/ archived, it's slow
|
||||||
|
and is missing data before ~august 2019. Thanks to an anon from Bibliotheca
|
||||||
|
Anonoma, I got a copy of the text archive that goes back all the way to the
|
||||||
|
first thread in ~2016. And thanks to the industrial revolution and its
|
||||||
|
consequences, you can actually query the entire archive pretty efficiently in
|
||||||
|
your browser.
|
||||||
|
|
||||||
|
I don't have thumbnails or images (yet), but I'm working on it. Until then,
|
||||||
|
enjoy the data.
|
||||||
|
|
||||||
|
<div class="warning">All queries in your browser, which means you'll download a
|
||||||
|
fair amount (~100MB) of data. So probably don't browse this your phone.</div>
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3">
|
||||||
|
<a class="card" href="thread-browser">
|
||||||
|
<h2>Thread Browser</h2>
|
||||||
|
<p>Browse old threads in a somewhat faithful format.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="substring-search">
|
||||||
|
<h2>Substring Search</h2>
|
||||||
|
<p>See how freqently certain substrings occur in the posts over time, a la google's ngram thing.</p>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="full-text-search">
|
||||||
|
<h2>Full-text Search</h2>
|
||||||
|
<p>Search posts with an inverted index. Freakishly fast.</p>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2" style="grid-auto-rows: 504px;">
|
```js
|
||||||
<div class="card">${
|
const archive_href = FileAttachment("data/vrgarchive.parquet").href;
|
||||||
resize((width) => Plot.plot({
|
```
|
||||||
title: "Your awesomeness over time 🚀",
|
|
||||||
subtitle: "Up and to the right!",
|
|
||||||
width,
|
|
||||||
y: {grid: true, label: "Awesomeness"},
|
|
||||||
marks: [
|
|
||||||
Plot.ruleY([0]),
|
|
||||||
Plot.lineY(aapl, {x: "Date", y: "Close", tip: true})
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
}</div>
|
|
||||||
<div class="card">${
|
|
||||||
resize((width) => Plot.plot({
|
|
||||||
title: "How big are penguins, anyway? 🐧",
|
|
||||||
width,
|
|
||||||
grid: true,
|
|
||||||
x: {label: "Body mass (g)"},
|
|
||||||
y: {label: "Flipper length (mm)"},
|
|
||||||
color: {legend: true},
|
|
||||||
marks: [
|
|
||||||
Plot.linearRegressionY(penguins, {x: "body_mass_g", y: "flipper_length_mm", stroke: "species"}),
|
|
||||||
Plot.dot(penguins, {x: "body_mass_g", y: "flipper_length_mm", stroke: "species", tip: true})
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
## FAQ
|
||||||
|
|
||||||
## Next steps
|
### wtf is this
|
||||||
|
|
||||||
Here are some ideas of things you could try…
|
it's an archive of [/vrg/](https://boards.4chan.org/vg/catalog#s=vrg) with a
|
||||||
|
bunch of javascript so you can query it efficiently in your browser.
|
||||||
|
|
||||||
<div class="grid grid-cols-4">
|
### Where did you get the archive data?
|
||||||
<div class="card">
|
|
||||||
Chart your own data using <a href="https://observablehq.com/framework/lib/plot"><code>Plot</code></a> and <a href="https://observablehq.com/framework/files"><code>FileAttachment</code></a>. Make it responsive using <a href="https://observablehq.com/framework/javascript#resize(render)"><code>resize</code></a>.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Create a <a href="https://observablehq.com/framework/project-structure">new page</a> by adding a Markdown file (<code>whatever.md</code>) to the <code>src</code> folder.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Add a drop-down menu using <a href="https://observablehq.com/framework/inputs/select"><code>Inputs.select</code></a> and use it to filter the data shown in a chart.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Write a <a href="https://observablehq.com/framework/loaders">data loader</a> that queries a local database or API, generating a data snapshot on build.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Import a <a href="https://observablehq.com/framework/imports">recommended library</a> from npm, such as <a href="https://observablehq.com/framework/lib/leaflet">Leaflet</a>, <a href="https://observablehq.com/framework/lib/dot">GraphViz</a>, <a href="https://observablehq.com/framework/lib/tex">TeX</a>, or <a href="https://observablehq.com/framework/lib/duckdb">DuckDB</a>.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Ask for help, or share your work or ideas, on our <a href="https://github.com/observablehq/framework/discussions">GitHub discussions</a>.
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
Visit <a href="https://github.com/observablehq/framework">Framework on GitHub</a> and give us a star. Or file an issue if you’ve found a bug!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
Data after august 2019 is scraped from b4k, and the older data is from an anon
|
||||||
|
on the Bibliotheca Anonoma matrix channel who happened to have a private
|
||||||
|
archiver. Thanks anon for uploading it for me.
|
||||||
|
|
||||||
.hero {
|
### Where are the images?
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-family: var(--sans-serif);
|
|
||||||
margin: 4rem 0 8rem;
|
|
||||||
text-wrap: balance;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 {
|
Don't have them yet, but I'm working on it. 2 more weeks.
|
||||||
margin: 1rem 0;
|
|
||||||
padding: 1rem 0;
|
|
||||||
max-width: none;
|
|
||||||
font-size: 14vw;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1;
|
|
||||||
background: linear-gradient(30deg, var(--theme-foreground-focus), currentColor);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h2 {
|
### Can I get the raw data?
|
||||||
margin: 0;
|
|
||||||
max-width: 34em;
|
|
||||||
font-size: 20px;
|
|
||||||
font-style: initial;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--theme-foreground-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
Yeah, download the ${html`<a href=${archive_href}>vrgarchive.parquet</a>`} file. It's not quite
|
||||||
.hero h1 {
|
raw as from the 4chan API, but it is easier to query and it's only ~90MB or so.
|
||||||
font-size: 90px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
### How is the data so small?
|
||||||
|
|
||||||
|
The data compresses extremely well with ZSTD and parquet. The uncompresed data is ~1.5GB, but
|
||||||
|
I guess after all these years we've only posted ~80MB of insightful, original text.
|
||||||
|
|
||||||
|
### How does it work?
|
||||||
|
|
||||||
|
This site uses [Observable Framework](https://observablehq.com), which includes
|
||||||
|
a [DuckDB](https://duckdb.org) wasm build, which queries the archive as parquet
|
||||||
|
files. It's kind of horrifying yeah but also cool.
|
||||||
|
|
||||||
|
### Can you add X feature?
|
||||||
|
|
||||||
|
Maybe, post in the thread about it. If you don't want to wait, you can also just
|
||||||
|
download the raw data and query it yourself, with duckDB or whatever.
|
||||||
|
|
||||||
|
### archives are bad
|
||||||
|
|
||||||
|
Yeah, I'm kind of ambivalent, but I have autism for data visualization and
|
||||||
|
awful javascript frameworks, so I did it anyway.
|
||||||
|
|
||||||
|
### How can I contact you?
|
||||||
|
|
||||||
|
Post in the thread, I'll see it.
|
||||||
|
|
87
src/substring-search.md
Normal file
87
src/substring-search.md
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
title: Substring Search
|
||||||
|
toc: false
|
||||||
|
sql:
|
||||||
|
posts: ./data/vrgarchive.parquet
|
||||||
|
---
|
||||||
|
|
||||||
|
# Substring Search
|
||||||
|
|
||||||
|
This uses plain substring search on the post comments and plots number of matching posts per month.
|
||||||
|
To search for multiple terms, separate them with a comma.
|
||||||
|
|
||||||
|
This is sort of slow (though still impressively fast for running in your browser), and afaik
|
||||||
|
there's no way to get a progress bar, but it should eventually finish.
|
||||||
|
|
||||||
|
If you want to actually see the matching posts, try the [full text search](full-text-search).
|
||||||
|
|
||||||
|
```js
|
||||||
|
function debounce(input, delay = 500) {
|
||||||
|
return Generators.observe(notify => {
|
||||||
|
let timer = null;
|
||||||
|
let value;
|
||||||
|
|
||||||
|
// On input, check if we recently reported a value.
|
||||||
|
// If we did, do nothing and wait for a delay;
|
||||||
|
// otherwise, report the current value and set a timeout.
|
||||||
|
function inputted() {
|
||||||
|
if (timer !== null) return;
|
||||||
|
notify(value = input.value);
|
||||||
|
timer = setTimeout(delayed, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a delay, check if the last-reported value is the current value.
|
||||||
|
// If it’s not, report the new value.
|
||||||
|
function delayed() {
|
||||||
|
timer = null;
|
||||||
|
if (value === input.value) return;
|
||||||
|
notify(value = input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", inputted), inputted();
|
||||||
|
return () => input.removeEventListener("input", inputted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const input_text = Inputs.text({
|
||||||
|
label: "substring search",
|
||||||
|
placeholder: "search for something",
|
||||||
|
value: "sneed, feed, seed"
|
||||||
|
});
|
||||||
|
|
||||||
|
const pattern_term = debounce(input_text);
|
||||||
|
|
||||||
|
display(input_text);
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql id=pattern_over_time
|
||||||
|
WITH search_terms AS (
|
||||||
|
-- Split the comma-separated list into rows, trim whitespace, and lowercase
|
||||||
|
SELECT lower(trim(unnest(string_split(${pattern_term}, ',')))) AS term
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cast(date_trunc('month', epoch_ms(p.timestamp * 1000)) as timestamp) AS themonth,
|
||||||
|
st.term as term, -- Include the specific term found
|
||||||
|
count(*) as num_comments -- Count occurrences for that specific term in that month
|
||||||
|
FROM
|
||||||
|
posts p
|
||||||
|
CROSS JOIN -- Check every post against every search term
|
||||||
|
search_terms st
|
||||||
|
WHERE
|
||||||
|
p.comment IS NOT NULL -- Avoid errors with contains on NULL
|
||||||
|
AND contains(lower(p.comment), st.term) -- Case-insensitive check against the term
|
||||||
|
GROUP BY
|
||||||
|
themonth,
|
||||||
|
st.term -- Group by month AND by term
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
Plot.plot({
|
||||||
|
color: {legend: true},
|
||||||
|
width: width,
|
||||||
|
x: { interval: "month"},
|
||||||
|
marks: [
|
||||||
|
Plot.lineY(pattern_over_time, {curve: "step", "x": "themonth", stroke: "term", y: "num_comments", tip: 'x' })
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
187
src/thread-browser.md
Normal file
187
src/thread-browser.md
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
---
|
||||||
|
title: Thread Browser
|
||||||
|
toc: false
|
||||||
|
sql:
|
||||||
|
posts: ./data/vrgarchive.parquet
|
||||||
|
---
|
||||||
|
|
||||||
|
# Thread Browser
|
||||||
|
|
||||||
|
Browser posts in old threads in a somewhat faithful format. It takes a bit to load and this framework is bad about indicating loading progress, so wait around if you don't see anything.
|
||||||
|
|
||||||
|
There are no thumbnails or full images (yet).
|
||||||
|
|
||||||
|
```sql id=threads
|
||||||
|
select
|
||||||
|
first(thread_num) as thread_num,
|
||||||
|
any_value(thread_title) as thread_title,
|
||||||
|
first(epoch_ms(timestamp * 1000)) as thread_time,
|
||||||
|
count(*) as num_comments
|
||||||
|
from posts
|
||||||
|
group by thread_num;
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
const search = view(
|
||||||
|
Inputs.search(threads,
|
||||||
|
{label: "filter threads: ",
|
||||||
|
placeholder: "filter",
|
||||||
|
filter: (query) => (c) => c.thread_title.toLowerCase().includes(query.toLowerCase())}));
|
||||||
|
```
|
||||||
|
|
||||||
|
Click the radio button on the left of each column to select a thread to display.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const thread = view(Inputs.table(search, {
|
||||||
|
format: {
|
||||||
|
"thread_time": (v) => new Date(v).toLocaleString()
|
||||||
|
},
|
||||||
|
value: threads[0], multiple: false, width: {thread_title: 300, thread_time: 200}}));
|
||||||
|
````
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {Post} from "./components/post.js";
|
||||||
|
|
||||||
|
let rows = thread == null ? []: await sql`select * from posts where thread_num = ${thread.thread_num}`;
|
||||||
|
|
||||||
|
const posts = [...rows];
|
||||||
|
|
||||||
|
let b4k_url = `https://arch.b4k.dev/vg/post/${thread.thread_num}/`;
|
||||||
|
```
|
||||||
|
|
||||||
|
${posts.length} posts.
|
||||||
|
|
||||||
|
${html`<a href=${b4k_url} target="_blank">View on b4k archive (for threads after ~#613)</a>`}.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
display(posts.map(p => <Post postData={p} />))
|
||||||
|
```
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.post {
|
||||||
|
margin: 4px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: light-dark(black, #c5c8c6);
|
||||||
|
font-family: arial, helvetica, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.thread>div:nth-of-type(2)>div.reply {
|
||||||
|
margin-top: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.op {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reply {
|
||||||
|
background-color: light-dark(#D6DAF0, #282a2e);
|
||||||
|
border: 1px solid light-dark(#B7C5D9, #282a2e);
|
||||||
|
display: table;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reply input {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileText {
|
||||||
|
max-width: 600px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.postNum {
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.postNum > a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: light-dark(black, #C5C8C6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover {
|
||||||
|
color: light-dark(black, #c5F89AC) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.nameBlock {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.nameBlock span.name {
|
||||||
|
color: light-dark(#117743, #c5c8c6);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.nameBlock span.postertrip {
|
||||||
|
color: light-dark(#117743, #c5c8c6);
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.date {
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.time {
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.postInfo span.subject {
|
||||||
|
color: #b294bb;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post blockquote.postMessage {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote span.quote {
|
||||||
|
color: light-dark(#789922, #b5bd68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quoteLink, .quotelink, .deadlink {
|
||||||
|
color: light-dark(#DD0000, #5F89AC) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.quoteLink:hover, a.quotelink:hover {
|
||||||
|
color: #81a2be !important;
|
||||||
|
}
|
||||||
|
div.post div.file {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.file div.fileInfo {
|
||||||
|
margin-right: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.replyContainer div.post div.file div.fileInfo {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.file .fileThumb {
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fileThumb {
|
||||||
|
margin-left: 0px !important;
|
||||||
|
margin-right: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reply span.fileThumb, div.reply span.fileThumb img {
|
||||||
|
float: none !important;
|
||||||
|
margin-top: 0px !important;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post div.file .fileThumb img {
|
||||||
|
border: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue