Capstone Project
Story Safe

Story Safe was my capstone project: a wiki platform built around spoiler control. The idea was that someone should be able to browse a story wiki without accidentally seeing information from chapters, arcs, or episodes they had not reached yet.
This project had a lot going on. I took a large role in the early UI/UX direction and logo work, then moved into front-end implementation, parsing cleaned wiki data into something usable, and building the spoiler-blur behavior that made the main product idea work.
My Role
Where I Worked On The Project
My work sat between product direction, visual design, frontend implementation, and the spoiler-protection logic that made the wiki feel different from a normal fandom site.
- I helped shape the early visual direction, including the logo and the wiki-style interface.
- Shhwip worked on the backend side of getting raw wiki data from the wiki database.
- After that raw HTML existed, I worked on cleaning, parsing, and displaying it in a way the React frontend could use.
- I worked on CSS across the app, including the spoiler blur behavior and responsive/mobile styling.
- As time got tight, I wrote a small JavaScript generator that produced deterministic CSS for the number of spoiler levels in a story.
System Pieces
What Had To Come Together
The project was bigger than a static UI. The wiki needed imported content, account-aware spoiler settings, search behavior, article rendering, and CSS that could protect users from future-story information.
Spoiler Level Model
Users set a spoiler level, and article content tagged above that level stays blurred so browsing a wiki does not reveal future story knowledge.
Wiki Import Pipeline
Raw wiki content had to be cleaned and transformed so the project could display imported articles instead of hand-writing every page.
UI And Identity
The project needed to feel like a real wiki product, so I worked on the brand, page structure, navigation, search, and visual system.
Deterministic CSS
The spoiler system relied on generated CSS selectors so the UI could reveal or hide content based on one predictable user setting.
Process
How I Thought Through It
I had to keep the product idea practical. The important thing was not making the most elegant possible spoiler system; it was making something deterministic that could ship for the capstone timeline.
- I started by helping define how the product should feel: familiar enough to behave like a wiki, but clearly centered around spoiler safety.
- The backend/import work gave us raw wiki content, but the raw output was not ready for the frontend. I worked on cleaning the HTML, extracting usable titles, handling imported images, and getting the rendered page closer to a wiki article.
- For spoilers, the simple rule was more important than a clever implementation: content marked with a higher story level should remain hidden until the user's selected level allowed it.
- The CSS generator was a pragmatic decision. Instead of manually writing a large number of repetitive selector combinations, I generated the CSS for the maximum chapter or arc count we needed.
Code
Snippets From The Work
These snippets show the practical parts I worked on: generating spoiler CSS, binding a user spoiler level to article content, cleaning imported wiki HTML, and filtering search results by story progress.
Generating Spoiler CSS
When time was tight, I generated the repetitive spoiler-level CSS instead of hand-writing selectors for every chapter or arc combination.
const maxSpoilLevel = 31;
let cssRules = "";
let cssRulesA = "";
for (let i = 1; i <= maxSpoilLevel; i++) {
for (let j = 1; j <= i; j++) {
cssRules += `[data-spoil-level='${i}'] .spoil_${j},`;
}
}
for (let i = 1; i <= maxSpoilLevel; i++) {
for (let j = 1; j <= i; j++) {
cssRulesA += `[data-spoil-level='${i}'] .spoil_${j} a,`;
}
}Connecting User Level To Article Content
The article page read the user's saved spoiler level and applied it as a data attribute around the parsed article HTML.
const userSpoilLevel = parseInt(localStorage.getItem("noSpoilLevel"), 10);
return (
<main className="page__main" lang="en">
<div data-spoil-level={userSpoilLevel}>
{parse(doc)}
</div>
</main>
);Cleaning Imported Wiki HTML
After the raw wiki content came through, I worked on cleanup steps that removed unused document metadata and turned imported title data into a usable article title.
const $ = cheerio.load(record);
$("html").replaceWith(function () {
return $('<div class="document"></div>').append($(this).contents());
});
$("meta, link[rel='dc:isVersionOf'], title, base").remove();
$("pre").each((index, element) => {
const preContent = $(element).html();
const titleMatch = preContent.match(/<title>(.*?)<\/title>/i);
if (titleMatch) {
$(element).replaceWith(`<div class="title">${titleMatch[1]}</div>`);
}
});Filtering Search By Spoiler Level
Search results also needed spoiler awareness, so results above the user's current story level were removed before rendering.
const noSpoilLevel = localStorage.getItem("noSpoilLevel") || 0;
const filteredResults = response.data.filter((result) => {
return result.spoiler_level <= noSpoilLevel;
});
setSearchResults(filteredResults);Reflection
What I Took From It
Story Safe was one of the first projects where design, data shape, and implementation constraints all had to answer to the same product promise.
- I learned how quickly UI/UX decisions become technical constraints once real data enters the system.
- I got more comfortable taking messy imported HTML and shaping it into a usable React experience.
- The spoiler system taught me to value deterministic behavior, especially when the product promise depends on protecting the user from seeing the wrong information.
- The capstone made collaboration feel real: backend parsing, frontend rendering, styling, routing, and product behavior all had to line up for the idea to work.