add favorite stories and searches + probably other things

so much shit was changed idek what did lmao
master
Drake 1 year ago
parent bf0d511282
commit dd311ef468

@ -11,4 +11,9 @@ Imho the Wattpad website is insufferable to use. The layout feels broken half th
4. Congrats, you are now running Voltpad
### Contributing
You probably shouldn't because the code is jank, but I mean you can
You probably shouldn't because the code is jank, but I mean you can
### FAQ
#### Why is the favicon a W and not a V?
The favicon is two Vs, not a W. Are you blind 🙄

@ -7,13 +7,20 @@ interface BaseHeaderProps extends Props {
entries: {
name: string;
href: string;
element?: any;
}[];
}
export default (props: BaseHeaderProps) => (
<header id={props.id} class="header">
{props.entries
.map((entry) => <a href={entry.href}>{entry.name}</a>)
.map((entry) =>
entry.element ? (
entry.element
) : (
<a href={entry.href}>{entry.name}</a>
)
)
.join("")}
</header>
);

@ -0,0 +1,16 @@
import { Props } from "../types.d.ts";
import { h } from "../jsx.ts";
interface BaseScriptProps extends Props {
// deno-lint-ignore ban-types
function?: Function;
// deno-lint-ignore no-explicit-any
children?: any;
}
export default (props: BaseScriptProps) => (
<script>
{props.function ? `(${props.function.toString()})();` : undefined}
{props.children ?? undefined}
</script>
);

@ -0,0 +1,3 @@
import { h } from "../jsx.ts";
export default () => <div class="vertical-line" />;

@ -12,6 +12,9 @@ a {
}
img {
object-fit: scale-down;
width: auto;
height: auto;
max-width: 75%;
}
@ -56,8 +59,15 @@ img {
margin-left: 0.5em;
}
margin-top: 1em;
min-width: 75%;
max-width: 75%;
width: 75%;
@media (max-width: 650px) {
flex-direction: column;
align-items: center;
div {
align-items: center;
}
}
}
.pagenav {
@ -104,4 +114,70 @@ img {
margin-right: 0.5em;
width:fit-content;
}
}
.vertical-line {
border-radius: 0.5em;
border-style: solid;
border-color:#0f0f0f;
margin-right: 0.5em;
height: 100%;
}
.search-form {
flex-direction:column;
display: flex;
align-items: center;
justify-content: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
@media (max-width: 560px) {
flex-direction: column;
div {
flex-direction: row;
margin-top: 0.5em;
margin-bottom: 0.25em;
}
}
}
}
.favorite-story-button {
margin-top: 0.5em;
}
.saved-container {
display: flex;
flex-direction: row;
flex-grow: 1;
@media (max-width: 650px) {
flex-direction: column;
align-items: center;
div {
align-items: center;
}
}
}
.saved-child {
.story {
flex-direction: column;
align-items: center;
div {
align-items: center;
}
}
.saved-search {
background-color: darkgrey;
border-radius: 0.5em;
border-style: solid;
border-width: 1px;
border-color:#0f0f0f;
padding: 0.2em;
margin-bottom: 0.5em;
}
}

@ -2,11 +2,23 @@ import { h } from "../jsx.ts";
import Base from "../templates/Base.tsx";
import Script from "../components/Script.tsx";
import IndexScript from "../scripts/Index.ts";
export default () => (
<Base title="Home" description="The homepage." stylepath="css/index.scss">
<Script function={IndexScript} />
<div style="text-align: center;">
<h1>Voltpad</h1>
<p>A blazingly fast (🚀) Wattpad frontend</p>
</div>
<div class="saved-container">
<div id="saved-searches" class="saved-child stories">
<h3>Saved Searches</h3>
</div>
<div id="saved-stories" class="saved-child stories">
<h3>Saved Stories</h3>
</div>
</div>
</Base>
);

@ -2,6 +2,9 @@ import { h } from "../jsx.ts";
import watt, { watt as originalWatt } from "../wattpad.ts";
import Base from "../templates/Base.tsx";
import Script from "../components/Script.tsx";
import FavoriteSearchButtonScript from "../scripts/FavoriteSearch.ts";
function search() {
console.log(document.getElementById("query"));
}
@ -37,11 +40,9 @@ export default async (params: URLSearchParams) => {
description="The homepage."
stylepath="css/index.scss"
>
<Script function={FavoriteSearchButtonScript} />
{/* TODO: impl autocomplete for tags? */}
<form
method="GET"
style="display:flex;flex-direction:column;align-items:center;justify-content:center;"
>
<form method="GET" class="search-form">
<div>
<input
id="query"
@ -52,38 +53,52 @@ export default async (params: URLSearchParams) => {
value={params.get("query") ?? ""}
autocomplete="off"
/>
<div>
<input
type="radio"
id="normal"
name="type"
value="text"
checked={
params.get("type") === "text" ||
!params.get("type")
}
/>
<label for="id">Text search</label>
<input
type="radio"
id="title"
name="type"
value="title"
checked={params.get("type") === "title"}
/>
<label for="title">Title search</label>
<input
type="radio"
id="tag"
name="type"
value="tag"
checked={params.get("type") === "tag"}
/>
<label for="tag">Tag search</label>
</div>
</div>
<div>
<input
type="radio"
id="normal"
name="type"
value="text"
checked={
params.get("type") === "text" || !params.get("type")
}
/>
<label for="id">Text search</label>
<input
type="radio"
id="title"
name="type"
value="title"
checked={params.get("type") === "title"}
/>
<label for="title">Title search</label>
<input
type="radio"
id="tag"
name="type"
value="tag"
checked={params.get("type") === "tag"}
style="margin-top:0.25em;margin-right:0.5em"
type="submit"
value="Submit"
/>
<label for="tag">Tag search</label>
{params.get("query") && params.get("type") ? (
<button
type="button"
style="margin-top:0.25em;border-radius: 0.25em;"
id="favorite"
>
Favorite
</button>
) : undefined}
</div>
<input
style="margin-top:0.25em;"
type="submit"
value="Submit"
/>
</form>
<div id="stories" class="stories">

@ -4,6 +4,9 @@ import Story from "../templates/Story.tsx";
import watt from "../wattpad.ts";
import FourOhFour from "./404.tsx";
import Script from "../components/Script.tsx";
import FavoriteStoryButtonScript from "../scripts/FavoriteStory.ts";
export default async (params: URLSearchParams) => {
if (!params.get("id")) return <FourOhFour />;
@ -70,29 +73,32 @@ export default async (params: URLSearchParams) => {
else {
return (
<Story story={story} stylepath="css/index.scss">
<Script function={FavoriteStoryButtonScript} />
<div style="display:flex;align-items:center;justify-content:center;flex-direction:column;">
<div class="story">
<img src={story.storyJSON.cover} />
<img id="storycover" src={story.storyJSON.cover} />
<div>
<h3>{story.name}</h3>
<h3 id="storyname">{story.name}</h3>
<p>{story.storyJSON.description}</p>
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;flex-direction:row;text-align:center; margin-left: 0;">
{story.tags
.map((v) => (
<div>
<a
href={`/search?query=${v}&type=tag`}
style="background-color:darkgrey; border-radius: 0.3em; padding: 0.1em; border: none;outline: none;text-decoration: none; margin-bottom: 0.5em;"
>
#{v}
</a>
</div>
))
.join("")}
</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;flex-direction:row;margin-top: 0.5em;text-align:center;">
{story.tags
.map((v) => (
<div>
<a
href={`/search?query=${v}&type=tag`}
style="background-color:darkgrey; border-radius: 0.3em; padding: 0.1em; margin-right: 0.5em;border: none;outline: none;text-decoration: none;"
>
#{v}
</a>
<div style="margin-top: 0.5em;" />
</div>
))
.join("")}
</div>
<button id="favorite" class="favorite-story-button">
Favorite
</button>
</div>
</Story>
);

@ -0,0 +1,55 @@
interface SavedSearch {
query: string;
type: "text" | "title" | "tag";
}
export default () => {
window.onload = () => {
const button = document.getElementById("favorite");
if (!button) throw "how did your button not init what";
let savedSearches: SavedSearch[] = JSON.parse(
localStorage.getItem("savedSearches") ?? "[]",
);
function handler() {
if (button?.textContent === "Favorite") {
savedSearches.push({
query: (new URL(document.URL)).searchParams.get(
"query",
) as string,
type: (new URL(document.URL)).searchParams.get(
"type",
) as "text" | "title" | "tag",
});
button.textContent = "Unfavorite";
} else if (button?.textContent === "Unfavorite") {
savedSearches = savedSearches.filter((search) =>
search.query !==
(new URL(document.URL)).searchParams.get("query") &&
search.type !==
(new URL(document.URL)).searchParams.get("type")
);
button.textContent = "Favorite";
}
localStorage.setItem(
"savedSearches",
JSON.stringify(savedSearches),
);
}
if (
savedSearches.filter((search) =>
search.query ===
(new URL(document.URL)).searchParams.get("query") &&
search.type ===
(new URL(document.URL)).searchParams.get("type")
).length > 0
) {
button.textContent = "Unfavorite";
}
button.onclick = handler;
};
};

@ -0,0 +1,51 @@
interface SavedStory {
id: string;
// in *theory* we could fetch these from the cache, but if it's not there you'd have to wait years to open the homepage lol
name: string;
coverURL: string;
}
export default () => {
window.onload = () => {
const button = document.getElementById("favorite");
if (!button) throw "how did your button not init what";
let savedStories: SavedStory[] = JSON.parse(
localStorage.getItem("savedStories") ?? "[]",
);
function handler() {
if (button?.textContent === "Favorite") {
savedStories.push({
id: (new URL(document.URL)).searchParams.get(
"id",
) as string,
name: document.getElementById("storyname")
?.textContent as string,
coverURL: (document.getElementById(
"storycover",
) as HTMLImageElement)
.src,
});
button.textContent = "Unfavorite";
} else if (button?.textContent === "Unfavorite") {
savedStories = savedStories.filter((story) =>
story.id !== (new URL(document.URL)).searchParams.get("id")
);
button.textContent = "Favorite";
}
localStorage.setItem("savedStories", JSON.stringify(savedStories));
}
if (
savedStories.filter((story) =>
story.id === (new URL(document.URL)).searchParams.get("id")
).length > 0
) {
button.textContent = "Unfavorite";
}
button.onclick = handler;
};
};

@ -0,0 +1,49 @@
interface SavedSearch {
query: string;
type: "text" | "title" | "tag";
}
interface SavedStory {
id: string;
// in *theory* we could fetch these from the cache, but if it's not there you'd have to wait years to open the homepage lol
name: string;
coverURL: string;
}
export default () => {
window.onload = () => {
const savedSearchesDiv = document.getElementById("saved-searches");
const savedStoriesDiv = document.getElementById("saved-stories");
const savedSearches: SavedSearch[] = JSON.parse(
localStorage.getItem("savedSearches") ?? "[]",
);
const savedStories: SavedStory[] = JSON.parse(
localStorage.getItem("savedStories") ?? "[]",
);
savedSearches.forEach((search) => {
const searchEle = document.createElement("a");
searchEle.href =
`/search?query=${search.query}&type=${search.type}`;
searchEle.textContent = `${search.query} (${search.type} search)`;
searchEle.className = "saved-search";
savedSearchesDiv?.appendChild(searchEle);
});
savedStories.forEach((story) => {
const searchEle = document.createElement("a");
searchEle.className = "story";
/* TODO: perhaps save story progress as well? */
searchEle.href = "/story?id=" + story.id;
const coverEle = document.createElement("img");
coverEle.src = story.coverURL;
const div = document.createElement("div");
const titleEle = document.createElement("h3");
titleEle.textContent = story.name;
div.appendChild(titleEle);
searchEle.appendChild(coverEle);
searchEle.appendChild(div);
savedStoriesDiv?.appendChild(searchEle);
});
};
};

@ -2,6 +2,7 @@ import sass from "https://deno.land/x/denosass@1.0.6/mod.ts";
import { h, PropsWithChildren } from "../jsx.ts";
import Header from "../components/Header.tsx";
import Script from "../components/Script.tsx";
interface TemplateProps {
title: string;

@ -3,6 +3,7 @@ import { Story } from "../wattpad.ts";
import { h, PropsWithChildren } from "../jsx.ts";
import Header from "../components/Header.tsx";
import VR from "../components/VerticalLine.tsx";
interface StoryProps {
story: Story;
@ -10,60 +11,75 @@ interface StoryProps {
stylepath?: string;
}
export default (props: PropsWithChildren<StoryProps>) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
export default (props: PropsWithChildren<StoryProps>) => {
let chaptersInHeader: { name: string; href: string; element?: any }[] = [
{
name: "Details",
href: `/story?id=${props.story.id}`
},
{
name: "",
href: "",
element: <VR />
}
];
{/* favicon stuff generated with https://realfavicongenerator.net */}
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link
rel="mask-icon"
href="/safari-pinned-tab.svg"
color="#5bbad5"
/>
<meta name="apple-mobile-web-app-title" content="Voltpad" />
<meta name="application-name" content="Voltpad" />
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="theme-color" content="#ffffff" />
chaptersInHeader = chaptersInHeader.concat(
props.story.chapters.map((ch, i) => ({
name: ch.name,
href: `/story?id=${ch.workID}&chapter=${i}`
}))
);
<title>{props.story.name}</title>
{(() => {
if (!props.style) props.style = "";
if (props.stylepath)
props.style += Deno.readTextFileSync(props.stylepath);
else return <div></div>;
return <style>{sass(props.style).to_string()}</style>;
})()}
</head>
<Header
id="storyHeader"
entries={props.story.chapters.map((ch, i) => ({
name: ch.name,
href: `/story?id=${ch.workID}&chapter=${i}`
}))}
></Header>
<div class="main">{props.children}</div>
</html>
);
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
{/* favicon stuff generated with https://realfavicongenerator.net */}
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link
rel="mask-icon"
href="/safari-pinned-tab.svg"
color="#5bbad5"
/>
<meta name="apple-mobile-web-app-title" content="Voltpad" />
<meta name="application-name" content="Voltpad" />
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="theme-color" content="#ffffff" />
<title>{props.story.name}</title>
{(() => {
if (!props.style) props.style = "";
if (props.stylepath)
props.style += Deno.readTextFileSync(props.stylepath);
else return <div></div>;
return <style>{sass(props.style).to_string()}</style>;
})()}
</head>
<Header id="storyHeader" entries={chaptersInHeader}></Header>
<div class="main">{props.children}</div>
</html>
);
};

Loading…
Cancel
Save