better
This commit is contained in:
@@ -173,7 +173,6 @@ function AppLayout(props: { children: any }) {
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div
|
<div
|
||||||
class="py-16"
|
|
||||||
onMouseUp={handleCenterTapRelease}
|
onMouseUp={handleCenterTapRelease}
|
||||||
onTouchEnd={handleCenterTapRelease}
|
onTouchEnd={handleCenterTapRelease}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,11 +12,19 @@ export interface PostSortingProps {
|
|||||||
privilegeLevel: "anonymous" | "admin" | "user";
|
privilegeLevel: "anonymous" | "admin" | "user";
|
||||||
filters?: string;
|
filters?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
|
include?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostSorting(props: PostSortingProps) {
|
export default function PostSorting(props: PostSortingProps) {
|
||||||
// Build set of tags that are ALLOWED (not filtered out)
|
// Build set of tags that are ALLOWED
|
||||||
const allowedTags = createMemo(() => {
|
const allowedTags = createMemo(() => {
|
||||||
|
// WHITELIST MODE: If 'include' param is present, only show posts with those tags
|
||||||
|
if (props.include) {
|
||||||
|
const includeList = props.include.split("|").filter(Boolean);
|
||||||
|
return new Set(includeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BLACKLIST MODE: Filter out tags in 'filter' param
|
||||||
const filterList = props.filters?.split("|").filter(Boolean) || [];
|
const filterList = props.filters?.split("|").filter(Boolean) || [];
|
||||||
|
|
||||||
// If no filters set, all tags are allowed
|
// If no filters set, all tags are allowed
|
||||||
@@ -42,7 +50,33 @@ export default function PostSorting(props: PostSortingProps) {
|
|||||||
const filteredPosts = createMemo(() => {
|
const filteredPosts = createMemo(() => {
|
||||||
const allowed = allowedTags();
|
const allowed = allowedTags();
|
||||||
|
|
||||||
// If all tags are allowed, show all posts
|
// In whitelist mode, only show posts with allowed tags
|
||||||
|
if (props.include) {
|
||||||
|
// Build map of post_id -> tags for that post
|
||||||
|
const postTags = new Map<number, Set<string>>();
|
||||||
|
props.tags.forEach((tag) => {
|
||||||
|
if (!postTags.has(tag.post_id)) {
|
||||||
|
postTags.set(tag.post_id, new Set());
|
||||||
|
}
|
||||||
|
postTags.get(tag.post_id)!.add(tag.value.slice(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep posts that have at least one allowed tag
|
||||||
|
return props.posts.filter((post) => {
|
||||||
|
const tags = postTags.get(post.id);
|
||||||
|
if (!tags) return false; // Post has no tags
|
||||||
|
|
||||||
|
// Check if post has at least one allowed tag
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (allowed.has(tag)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In blacklist mode, show all posts if all tags are allowed
|
||||||
if (
|
if (
|
||||||
allowed.size ===
|
allowed.size ===
|
||||||
props.tags
|
props.tags
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const currentFilters = () => searchParams.filter || null;
|
const currentFilters = () => searchParams.filter || null;
|
||||||
|
const currentInclude = () => searchParams.include || null;
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
let newRoute = location.pathname + "?sort=" + selected().val;
|
let newRoute = location.pathname + "?sort=" + selected().val;
|
||||||
if (currentFilters()) {
|
if (currentFilters()) {
|
||||||
newRoute += "&filter=" + currentFilters();
|
newRoute += "&filter=" + currentFilters();
|
||||||
}
|
}
|
||||||
|
if (currentInclude()) {
|
||||||
|
newRoute += "&include=" + currentInclude();
|
||||||
|
}
|
||||||
navigate(newRoute);
|
navigate(newRoute);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export interface TagSelectorProps {
|
|||||||
export default function TagSelector(props: TagSelectorProps) {
|
export default function TagSelector(props: TagSelectorProps) {
|
||||||
const [showingMenu, setShowingMenu] = createSignal(false);
|
const [showingMenu, setShowingMenu] = createSignal(false);
|
||||||
const [showingRareTags, setShowingRareTags] = createSignal(false);
|
const [showingRareTags, setShowingRareTags] = createSignal(false);
|
||||||
|
const [filterMode, setFilterMode] = createSignal<"whitelist" | "blacklist">(
|
||||||
|
"blacklist"
|
||||||
|
);
|
||||||
let buttonRef: HTMLButtonElement | undefined;
|
let buttonRef: HTMLButtonElement | undefined;
|
||||||
let menuRef: HTMLDivElement | undefined;
|
let menuRef: HTMLDivElement | undefined;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -22,7 +25,19 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const currentSort = () => searchParams.sort || "";
|
const currentSort = () => searchParams.sort || "";
|
||||||
const currentFilters = () => searchParams.filter?.split("|") || [];
|
const currentFilters = () =>
|
||||||
|
searchParams.filter?.split("|").filter(Boolean) || [];
|
||||||
|
const currentInclude = () =>
|
||||||
|
searchParams.include?.split("|").filter(Boolean) || [];
|
||||||
|
|
||||||
|
// Sync filter mode with URL params
|
||||||
|
createEffect(() => {
|
||||||
|
if (searchParams.include) {
|
||||||
|
setFilterMode("whitelist");
|
||||||
|
} else if (searchParams.filter) {
|
||||||
|
setFilterMode("blacklist");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const frequentTags = createMemo(() =>
|
const frequentTags = createMemo(() =>
|
||||||
Object.entries(props.tagMap).filter(([_, count]) => count > 1)
|
Object.entries(props.tagMap).filter(([_, count]) => count > 1)
|
||||||
@@ -36,9 +51,23 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
Object.keys(props.tagMap).map((key) => key.slice(1))
|
Object.keys(props.tagMap).map((key) => key.slice(1))
|
||||||
);
|
);
|
||||||
|
|
||||||
const allChecked = createMemo(() =>
|
// In blacklist mode: checked = not filtered out
|
||||||
allTagKeys().every((tag) => !currentFilters().includes(tag))
|
// In whitelist mode: checked = included in whitelist
|
||||||
);
|
const isTagChecked = (tag: string) => {
|
||||||
|
if (filterMode() === "whitelist") {
|
||||||
|
return currentInclude().includes(tag);
|
||||||
|
} else {
|
||||||
|
return !currentFilters().includes(tag);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allChecked = createMemo(() => {
|
||||||
|
if (filterMode() === "whitelist") {
|
||||||
|
return currentInclude().length === allTagKeys().length;
|
||||||
|
} else {
|
||||||
|
return allTagKeys().every((tag) => !currentFilters().includes(tag));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -64,42 +93,87 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
setShowingMenu(!showingMenu());
|
setShowingMenu(!showingMenu());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheck = (filter: string, isChecked: boolean) => {
|
const handleCheck = (tag: string, isChecked: boolean) => {
|
||||||
if (isChecked) {
|
if (filterMode() === "whitelist") {
|
||||||
const newFilters = searchParams.filter?.replace(filter + "|", "");
|
// Whitelist mode: manage include param
|
||||||
if (newFilters && newFilters.length >= 1) {
|
let newInclude: string[];
|
||||||
|
if (isChecked) {
|
||||||
|
// Add to whitelist
|
||||||
|
newInclude = [...currentInclude(), tag];
|
||||||
|
} else {
|
||||||
|
// Remove from whitelist
|
||||||
|
newInclude = currentInclude().filter((t) => t !== tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newInclude.length > 0) {
|
||||||
|
const includeStr = newInclude.map((t) => `#${t}`).join("|");
|
||||||
navigate(
|
navigate(
|
||||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
`${location.pathname}?sort=${currentSort()}&include=${includeStr}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// If no tags selected, clear whitelist
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const currentFiltersStr = searchParams.filter;
|
// Blacklist mode: manage filter param
|
||||||
if (currentFiltersStr) {
|
if (isChecked) {
|
||||||
const newFilters = currentFiltersStr + filter + "|";
|
const newFilters = searchParams.filter?.replace(tag + "|", "");
|
||||||
navigate(
|
if (newFilters && newFilters.length >= 1) {
|
||||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
navigate(
|
||||||
);
|
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate(
|
const currentFiltersStr = searchParams.filter;
|
||||||
`${location.pathname}?sort=${currentSort()}&filter=${filter}|`
|
if (currentFiltersStr) {
|
||||||
);
|
const newFilters = currentFiltersStr + tag + "|";
|
||||||
|
navigate(
|
||||||
|
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navigate(`${location.pathname}?sort=${currentSort()}&filter=${tag}|`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAll = () => {
|
const handleToggleAll = () => {
|
||||||
if (allChecked()) {
|
if (filterMode() === "whitelist") {
|
||||||
// Uncheck all: Build filter string with all tags
|
if (allChecked()) {
|
||||||
const allTags = allTagKeys().join("|") + "|";
|
// Uncheck all: clear whitelist
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${allTags}`);
|
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||||
|
} else {
|
||||||
|
// Check all: add all tags to whitelist
|
||||||
|
const allTags = allTagKeys()
|
||||||
|
.map((t) => `#${t}`)
|
||||||
|
.join("|");
|
||||||
|
navigate(
|
||||||
|
`${location.pathname}?sort=${currentSort()}&include=${allTags}`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check all: Remove filter param
|
if (allChecked()) {
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
// Uncheck all: Build filter string with all tags
|
||||||
|
const allTags = allTagKeys().join("|") + "|";
|
||||||
|
navigate(
|
||||||
|
`${location.pathname}?sort=${currentSort()}&filter=${allTags}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Check all: Remove filter param
|
||||||
|
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleFilterMode = () => {
|
||||||
|
const newMode = filterMode() === "whitelist" ? "blacklist" : "whitelist";
|
||||||
|
setFilterMode(newMode);
|
||||||
|
// Clear all filters when switching modes
|
||||||
|
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
@@ -113,23 +187,51 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
<Show when={showingMenu()}>
|
<Show when={showingMenu()}>
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
class="bg-surface0 absolute top-full left-0 z-50 mt-2 rounded-lg py-2 pr-4 pl-2 shadow-lg"
|
class="bg-surface0 absolute top-full left-0 z-50 mt-2 min-w-64 rounded-lg py-2 pr-4 pl-2 shadow-lg"
|
||||||
>
|
>
|
||||||
|
{/* Filter Mode Toggle */}
|
||||||
|
<div class="border-overlay0 mb-2 border-b pb-2">
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<span class="text-subtext0 text-xs font-medium">
|
||||||
|
Filter Mode:
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleFilterMode}
|
||||||
|
class={`rounded px-2 py-1 text-xs font-semibold transition-all duration-200 hover:brightness-110 ${
|
||||||
|
filterMode() === "whitelist"
|
||||||
|
? "bg-green text-base"
|
||||||
|
: "bg-red text-base"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filterMode() === "whitelist" ? "✓ Whitelist" : "✗ Blacklist"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-subtext1 text-xs italic">
|
||||||
|
{filterMode() === "whitelist"
|
||||||
|
? "Check tags to show ONLY those posts"
|
||||||
|
: "Uncheck tags to HIDE those posts"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle All Button */}
|
||||||
<div class="border-overlay0 mb-2 flex justify-center border-b pb-2">
|
<div class="border-overlay0 mb-2 flex justify-center border-b pb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleAll}
|
onClick={handleToggleAll}
|
||||||
class="text-text hover:text-red text-xs font-medium underline"
|
class="text-text hover:text-blue text-xs font-medium underline"
|
||||||
>
|
>
|
||||||
{allChecked() ? "Uncheck All" : "Check All"}
|
{allChecked() ? "Uncheck All" : "Check All"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Frequent Tags */}
|
||||||
<For each={frequentTags()}>
|
<For each={frequentTags()}>
|
||||||
{([key, value]) => (
|
{([key, value]) => (
|
||||||
<div class="mx-auto my-2 flex">
|
<div class="mx-auto my-2 flex">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!currentFilters().includes(key.slice(1))}
|
checked={isTagChecked(key.slice(1))}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCheck(key.slice(1), e.currentTarget.checked)
|
handleCheck(key.slice(1), e.currentTarget.checked)
|
||||||
}
|
}
|
||||||
@@ -140,6 +242,8 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
|
{/* Rare Tags Section */}
|
||||||
<Show when={rareTags().length > 0}>
|
<Show when={rareTags().length > 0}>
|
||||||
<div class="border-overlay0 mt-2 border-t pt-2">
|
<div class="border-overlay0 mt-2 border-t pt-2">
|
||||||
<button
|
<button
|
||||||
@@ -155,7 +259,7 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
<div class="mx-auto my-2 flex">
|
<div class="mx-auto my-2 flex">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!currentFilters().includes(key.slice(1))}
|
checked={isTagChecked(key.slice(1))}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCheck(key.slice(1), e.currentTarget.checked)
|
handleCheck(key.slice(1), e.currentTarget.checked)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,11 +317,17 @@ export default function PostPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
|
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
|
||||||
<For each={postData.tags as any[]}>
|
<For each={postData.tags as any[]}>
|
||||||
{(tag) => (
|
{(tag) => {
|
||||||
<div class="group relative m-1 h-fit w-fit rounded-xl bg-purple-600 px-2 py-1 text-sm">
|
const tagValue = tag.value;
|
||||||
<div class="text-white">{tag.value}</div>
|
return tagValue ? (
|
||||||
</div>
|
<A
|
||||||
)}
|
href={`/blog?include=${encodeURIComponent(tagValue.split("#")[1])}`}
|
||||||
|
class="group bg-rosewater relative m-1 h-fit w-fit rounded-xl px-2 py-1 text-sm transition-all duration-200 hover:brightness-110 active:scale-95"
|
||||||
|
>
|
||||||
|
<div class="text-white">{tagValue}</div>
|
||||||
|
</A>
|
||||||
|
) : null;
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +339,7 @@ export default function PostPage() {
|
|||||||
<Fire
|
<Fire
|
||||||
height={32}
|
height={32}
|
||||||
width={32}
|
width={32}
|
||||||
color="var(--color-text)"
|
color="var(--color-red)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
|
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
|
||||||
@@ -344,7 +350,14 @@ export default function PostPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="#comments" class="mx-2">
|
<a href="#comments" class="mx-2">
|
||||||
<div class="tooltip flex flex-col">
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
document
|
||||||
|
.getElementById("comments")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}}
|
||||||
|
class="tooltip flex flex-col"
|
||||||
|
>
|
||||||
<div class="mx-auto hover:brightness-125">
|
<div class="mx-auto hover:brightness-125">
|
||||||
<CommentIcon
|
<CommentIcon
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
@@ -358,7 +371,7 @@ export default function PostPage() {
|
|||||||
? "Comment"
|
? "Comment"
|
||||||
: "Comments"}
|
: "Comments"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="mx-2">
|
<div class="mx-2">
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function BlogIndex() {
|
|||||||
|
|
||||||
const sort = () => searchParams.sort || "newest";
|
const sort = () => searchParams.sort || "newest";
|
||||||
const filters = () => searchParams.filter || "";
|
const filters = () => searchParams.filter || "";
|
||||||
|
const include = () => searchParams.include || "";
|
||||||
|
|
||||||
const data = createAsync(() => getPosts(), { deferStream: true });
|
const data = createAsync(() => getPosts(), { deferStream: true });
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export default function BlogIndex() {
|
|||||||
<>
|
<>
|
||||||
<Title>Blog | Michael Freno</Title>
|
<Title>Blog | Michael Freno</Title>
|
||||||
|
|
||||||
<div class="mx-auto pt-8 pb-24">
|
<div class="mx-auto py-16 pb-24">
|
||||||
<Show when={data()} fallback={<TerminalSplash />}>
|
<Show when={data()} fallback={<TerminalSplash />}>
|
||||||
{(loadedData) => (
|
{(loadedData) => (
|
||||||
<>
|
<>
|
||||||
@@ -118,6 +119,7 @@ export default function BlogIndex() {
|
|||||||
privilegeLevel={loadedData().privilegeLevel}
|
privilegeLevel={loadedData().privilegeLevel}
|
||||||
filters={filters()}
|
filters={filters()}
|
||||||
sort={sort()}
|
sort={sort()}
|
||||||
|
include={include()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Title, Meta } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
||||||
import GitHub from "~/components/icons/GitHub";
|
|
||||||
import LinkedIn from "~/components/icons/LinkedIn";
|
|
||||||
|
|
||||||
export default function DownloadsPage() {
|
export default function DownloadsPage() {
|
||||||
const download = (assetName: string) => {
|
const download = (assetName: string) => {
|
||||||
@@ -15,12 +13,6 @@ export default function DownloadsPage() {
|
|||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
const joinBetaPrompt = () => {
|
|
||||||
window.alert(
|
|
||||||
"This isn't released yet, if you would like to help test, please go the contact page and include the game and platform you would like to help test in the message. Otherwise the apk is available for direct install. Thanks!"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Downloads | Michael Freno</Title>
|
<Title>Downloads | Michael Freno</Title>
|
||||||
@@ -40,12 +32,12 @@ export default function DownloadsPage() {
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-evenly md:mx-[25vw]">
|
<div class="flex justify-evenly">
|
||||||
<div class="flex w-1/3 flex-col">
|
<div class="flex w-1/3 flex-col">
|
||||||
<div class="text-center text-lg">Android (apk only)</div>
|
<div class="text-center text-lg">Android</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => download("lineage")}
|
onClick={() => download("lineage")}
|
||||||
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
class="bg-blue mx-auto mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
||||||
>
|
>
|
||||||
Download APK
|
Download APK
|
||||||
</button>
|
</button>
|
||||||
@@ -53,20 +45,6 @@ export default function DownloadsPage() {
|
|||||||
Note the android version is not well tested, and has performance
|
Note the android version is not well tested, and has performance
|
||||||
issues.
|
issues.
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-around">Or</div>
|
|
||||||
|
|
||||||
<div class="mx-auto italic">(Coming soon)</div>
|
|
||||||
<button
|
|
||||||
onClick={joinBetaPrompt}
|
|
||||||
class="mx-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/google-play-badge.png"
|
|
||||||
alt="google-play"
|
|
||||||
width={180}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -88,28 +66,15 @@ export default function DownloadsPage() {
|
|||||||
(apk and iOS)
|
(apk and iOS)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-evenly md:mx-[25vw]">
|
<div class="flex justify-evenly">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="text-center text-lg">Android</div>
|
<div class="text-center text-lg">Android</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => download("shapes-with-abigail")}
|
onClick={() => download("shapes-with-abigail")}
|
||||||
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
class="bg-blue mx-auto mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
||||||
>
|
>
|
||||||
Download APK
|
Download APK
|
||||||
</button>
|
</button>
|
||||||
<div class="rule-around">Or</div>
|
|
||||||
<div class="mx-auto italic">(Coming soon)</div>
|
|
||||||
<button
|
|
||||||
onClick={joinBetaPrompt}
|
|
||||||
class="transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/google-play-badge.png"
|
|
||||||
alt="google-play"
|
|
||||||
width={180}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -142,33 +107,6 @@ export default function DownloadsPage() {
|
|||||||
Just unzip and drag into 'Applications' folder
|
Just unzip and drag into 'Applications' folder
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="icons flex justify-center gap-4 pt-24 pb-6">
|
|
||||||
<li>
|
|
||||||
<A
|
|
||||||
href="https://github.com/MikeFreno/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
|
||||||
>
|
|
||||||
<span class="m-auto block p-2">
|
|
||||||
<GitHub height={24} width={24} fill={undefined} />
|
|
||||||
</span>
|
|
||||||
</A>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<A
|
|
||||||
href="https://www.linkedin.com/in/michael-freno-176001256/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
|
||||||
>
|
|
||||||
<span class="m-auto block rounded-md p-2">
|
|
||||||
<LinkedIn height={24} width={24} fill={undefined} />
|
|
||||||
</span>
|
|
||||||
</A>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function Home() {
|
|||||||
content="Michael Freno - Software Engineer based in Brooklyn, NY"
|
content="Michael Freno - Software Engineer based in Brooklyn, NY"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="flex h-full flex-col gap-8 px-4 text-xl">
|
<main class="flex h-full flex-col gap-8 px-4 py-16 text-xl">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Typewriter speed={30} keepAlive={2000}>
|
<Typewriter speed={30} keepAlive={2000}>
|
||||||
<div class="text-4xl">Hey!</div>
|
<div class="text-4xl">Hey!</div>
|
||||||
@@ -153,39 +153,40 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Typewriter speed={120} class="mx-auto max-w-3/4 pt-8 md:max-w-1/2">
|
<div class="flex justify-between">
|
||||||
And if you love the color schemes of this site
|
<Typewriter speed={120} class="mx-auto max-w-3/4 pt-8 md:max-w-1/2">
|
||||||
<div class="mx-auto w-fit">
|
And if you love the color schemes of this site
|
||||||
<DarkModeToggle />
|
<div class="mx-auto w-fit">
|
||||||
|
<DarkModeToggle />
|
||||||
|
</div>
|
||||||
|
(which of course you do), you can see{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>{" "}
|
||||||
|
- and also see the rest of my various dot files idk. There's a
|
||||||
|
macos and arch linux rice in there if you're into that kinda thing
|
||||||
|
and a home server setup too. Which I will write about soon™.
|
||||||
|
</Typewriter>
|
||||||
|
<div class="flex flex-col items-end justify-center gap-4 pr-4">
|
||||||
|
<Typewriter speed={30} keepAlive={false}>
|
||||||
|
<div>
|
||||||
|
My Collection of
|
||||||
|
<br />
|
||||||
|
By-the-ways:
|
||||||
|
</div>
|
||||||
|
</Typewriter>
|
||||||
|
<Typewriter speed={30} keepAlive={false}>
|
||||||
|
<ul class="list-disc">
|
||||||
|
<li>I use Neovim</li>
|
||||||
|
<li>I use Arch Linux</li>
|
||||||
|
<li>I use Rust</li>
|
||||||
|
</ul>
|
||||||
|
</Typewriter>
|
||||||
</div>
|
</div>
|
||||||
(which of course you do), you can see{" "}
|
</div>
|
||||||
<a
|
|
||||||
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
|
|
||||||
class="text-blue hover-underline-animation"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</a>{" "}
|
|
||||||
- and also see the rest of my various dot files idk. There's a macos
|
|
||||||
and arch linux rice in there if you're into that kinda thing and a
|
|
||||||
home server setup too. Which I will write about soon™.
|
|
||||||
</Typewriter>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-end gap-4 pr-4">
|
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
|
||||||
<div>
|
|
||||||
My Collection of
|
|
||||||
<br />
|
|
||||||
By-the-ways:
|
|
||||||
</div>
|
|
||||||
</Typewriter>
|
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
|
||||||
<ul class="list-disc">
|
|
||||||
<li>I use Neovim</li>
|
|
||||||
<li>I use Arch Linux</li>
|
|
||||||
<li>I use Rust</li>
|
|
||||||
</ul>
|
|
||||||
</Typewriter>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user