This commit is contained in:
Brooke Christiansen 2025-05-01 18:50:28 -07:00
parent 2413228ea0
commit ee2c96c885
11 changed files with 105 additions and 463 deletions

13
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils";
export default function Footer() {
return (
<div
className={cn(
"flex h-12 w-screen flex-col place-items-center justify-center bg-emerald-50",
)}
>
<h2>Copyleft 2025</h2>
</div>
);
}

View File

@ -1,51 +0,0 @@
import dataWebsites from "@/data/websites.json";
import { cn } from "@/lib/utils";
import { filteredTags } from "@/store";
import { useStore } from "@nanostores/react";
import { X } from "lucide-react";
import { useMemo } from "react";
import { Button } from "./ui/button";
export default function ListTags() {
const selectedTags: string[] = useStore(filteredTags);
const tags = useMemo(() => {
const tags = new Set<string>();
dataWebsites.forEach((website) => {
website.tags.forEach((tag) => tags.add(tag));
});
return Array.from(tags).sort((a, b) => a.localeCompare(b));
}, []);
return (
<div
className={cn(
"container mx-auto p-4 md:px-0 md:py-8",
"flex flex-wrap items-center justify-center gap-1",
)}
>
{tags.map((tag) => {
const selected = selectedTags.includes(tag);
return (
<Button
key={tag}
size="sm"
variant={selected ? "default" : "outline"}
onClick={() =>
filteredTags.set(
selected
? selectedTags.filter((e) => e !== tag)
: [...selectedTags, tag],
)
}
className={cn(
"flex cursor-pointer items-center gap-2 transition-all",
)}
>
{tag} {selected && <X size={12} />}
</Button>
);
})}
</div>
);
}

View File

@ -1,78 +0,0 @@
import { Badge } from "@/components/ui/badge";
import dataWebsites from "@/data/websites.json";
import { cn } from "@/lib/utils";
import { filteredTags, searchKeyword } from "@/store";
import { useStore } from "@nanostores/react";
import { useMemo } from "react";
export default function ListWebsites() {
const search = useStore(searchKeyword);
const tags = useStore(filteredTags);
const filteredWebsites = useMemo(() => {
if (!search && tags.length === 0) return dataWebsites;
return dataWebsites.filter((website) => {
if (
tags.length > 0 &&
!tags.every((tag) => (website.tags as string[]).includes(tag))
) {
return false;
}
if (!website.title.toLowerCase().includes(search)) return false;
return true;
});
}, [search, tags]);
return (
<div
className={cn(
"container mx-auto px-4 md:px-0",
"grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-3 xl:grid-cols-4",
)}
>
{filteredWebsites.map((website) => (
<a
key={website.url}
className={cn(
"rounded bg-background p-4 shadow",
"flex flex-col gap-4",
)}
href={website.url}
target="_blank"
>
<div className="flex gap-2">
<div className="h-12 w-12 bg-muted p-2">
<img
src={
website.favicon ||
"https://placehold.co/400?text=No%20Picture"
}
alt={website.title}
className="aspect-square w-full rounded object-cover"
/>
</div>
<p className="flex-1 text-sm font-semibold">{website.title}</p>
</div>
<div className="flex flex-1 flex-col justify-between gap-2">
<div className="flex flex-col gap-1">
<p className="line-clamp-3 text-xs text-muted-foreground">
{website.description}
</p>
<div className="flex flex-wrap gap-1">
{website.tags.map((tag) => (
<Badge className="px-1 py-0">{tag}</Badge>
))}
</div>
</div>
<div>
<span className="text-xs text-muted-foreground">
Last reviewed at{" "}
<span className="font-medium">{website.lastReviewAt}</span>
</span>
</div>
</div>
</a>
))}
</div>
);
}

View File

@ -1,86 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export default function ModalSubmitNew({
children,
}: {
children: React.ReactNode;
}) {
const title = "Submit a new website";
const description = "Request us to list your website on this website";
return (
<>
<Drawer>
<DrawerTrigger className="block md:hidden">{children}</DrawerTrigger>
<DrawerContent className="mx-2 max-h-[90%] min-h-[70%] px-4">
<DrawerHeader className="px-0 text-left">
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription>{description}</DrawerDescription>
</DrawerHeader>
<div>
<Label htmlFor="inputUrl">Website URL</Label>
<Input id="inputUrl" name="url" placeholder="https://example.com" />
</div>
<div>
<Label htmlFor="comment">Comment</Label>
<Textarea id="comment" placeholder="Leave some notes here ..." />
</div>
<DrawerFooter className="my-2 space-y-2 px-0">
<DrawerClose asChild>
<Button size="lg" variant="secondary">
Cancel
</Button>
</DrawerClose>
<Button size="lg">Submit</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
<Dialog>
<DialogTrigger className="hidden md:block">{children}</DialogTrigger>
<DialogContent>
<DialogHeader className="text-left">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div>
<Label htmlFor="inputUrl">Website URL</Label>
<Input id="inputUrl" name="url" placeholder="https://example.com" />
</div>
<div>
<Label htmlFor="comment">Comment</Label>
<Textarea id="comment" placeholder="Leave some notes here ..." />
</div>
<DialogFooter className="flex items-center pt-1">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<Button>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -1,55 +0,0 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Moon, Sun } from "lucide-react";
import * as React from "react";
export function ModeToggle() {
const [theme, setThemeState] = React.useState<
"theme-light" | "dark" | "system"
>("theme-light");
React.useEffect(() => {
const isDarkMode = document.documentElement.classList.contains("dark");
setThemeState(isDarkMode ? "dark" : "theme-light");
}, []);
React.useEffect(() => {
const isDark =
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
}, [theme]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="rounded-full border-none focus-visible:ring-0"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setThemeState("theme-light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,67 +1,64 @@
import { buttonVariants } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
function NavIcon({
icon,
tooltip,
href,
target,
onClick,
}: {
icon: React.ReactNode;
tooltip: string;
href?: string;
target?: string;
onClick?: () => void;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<a
href={href}
target={target}
onClick={onClick}
className={cn(
buttonVariants({
variant: "ghost",
size: "icon",
}),
"rounded-full text-foreground transition-all group-hover:scale-110",
)}
>
{icon}
</a>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
);
}
export default function Navbar() {
return (
<div className={cn("flex justify-end text-3xl")}>
<a href="/src/pages/GetInvolved.astro" className={cn("text-blue-500")}>
Get Involved
</a>
<Separator orientation="vertical" className="w-5" />
<a href="/src/pages/PointsOfUnity.astro" className={cn("text-green-500")}>
Points of Unity
</a>
<Separator orientation="vertical" className="w-5" />
<a
href="/src/pages/WhatWereWorkingOn.astro"
className={cn("text-red-500")}
>
What We're Working On
</a>
</div>
<>
<div className={cn("flex flex-row justify-between pb-8 pl-4 pr-4 pt-8")}>
<div>
<strong className={cn("sm:text-2xl md:text-4xl")}>
Resist Tech Monopolies
</strong>
</div>
<strong className={cn("sm:text-1xl flex gap-4 md:text-4xl")}>
<a
href="/src/pages/GetInvolved.astro"
className={cn("text-blue-500")}
>
Get Involved
</a>
{/* <Separator orientation="vertical" className="w-5" /> */}
<a
href="/src/pages/PointsOfUnity.astro"
className={cn("text-green-500")}
>
Points of Unity
</a>
{/* <Separator orientation="vertical" className="w-5" /> */}
<a
href="/src/pages/WhatWereWorkingOn.astro"
className={cn("text-red-500")}
>
What We're Working On
</a>
</strong>
</div>
<div className={cn("flex flex-row justify-between pb-8")}>
{[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
].map(() => (
<>
<a
href="https://www.flaticon.com/free-icons/computer"
title="computer icons"
>
<img
src="/computer.png"
alt="computer icon"
width="30"
height="30"
/>
</a>
<a
href="https://www.flaticon.com/free-icons/peopler"
title="people icons"
>
<img src="/people.png" alt="people icon" width="30" height="30" />
</a>
</>
))}
</div>
</>
);
}

View File

@ -1,77 +0,0 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { searchKeyword } from "@/store";
import { Search as SearchIcon, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
export default function Search({ className }: { className?: string }) {
const [search, setSearch] = useState("");
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const isDark = document.documentElement.classList.contains("dark");
setIsDark(isDark);
}, []);
const onClickSearch = () => {
onSearch();
};
const onClear = () => {
if (search !== "") setSearch("");
searchKeyword.set("");
};
const onSearch = () => {
searchKeyword.set(search);
};
return (
<div
className={cn(
"flex w-full items-center justify-center gap-2 md:gap-4",
className,
)}
>
<div className={cn("relative flex w-full")}>
<Input
placeholder="Enter something..."
value={search}
className="h-12 w-full bg-background pl-12 text-sm shadow-lg md:h-16 md:text-lg"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={(event) => {
if (event.key === "Enter" || event.keyCode === 13) {
onSearch();
}
}}
/>
<a
onClick={onClear}
className={cn(
"absolute right-4 top-1 z-10 translate-y-[50%] md:top-2",
"cursor-pointer text-muted-foreground opacity-0 transition-all",
search !== "" && "opacity-100",
)}
>
<XCircle className="size-5 md:size-6" />
</a>
<SearchIcon
className={cn(
"absolute left-4 top-1 translate-y-[50%] md:top-2",
"size-5 cursor-pointer text-muted-foreground/20 md:size-6",
)}
/>
</div>
<Button
variant={isDark ? "secondary" : "default"}
className={cn(
"h-12 w-32 text-sm font-light shadow-lg md:h-16 md:text-lg",
)}
onClick={() => onClickSearch()}
>
Search
</Button>
</div>
);
}

View File

@ -2,9 +2,9 @@
import { cn } from "@/lib/utils";
const title =
"Placeholder title of your website";
"Resist Tech Monopolies";
const description =
"Placeholder description of your website";
"Copyleft 2025";
const url = "https://placeholder.com";
const image = "/public/preview.webp";
---
@ -39,8 +39,8 @@ const image = "/public/preview.webp";
"@context": "http://schema.org",
"@type": "WebSite",
"url": "https://placeholder.com",
"name": "Placeholder title of your website",
"description": "Placeholder title of your website"
"name": "Resist Tech Monopolies",
"description": "Copyleft 2025"
}
</script>

View File

@ -1,4 +1,17 @@
<h1>Get Involved</h1>
<h2>Fill out this form</h2>
<input />
<button>submit</button>
---
import Footer from "@/components/Footer";
import Navbar from "@/components/Navbar";
import Layout from "@/layouts/Layout.astro";
---
<Layout>
<main class="flex min-h-screen flex-col justify-between">
<div>
<Navbar />
<div class="pl-4">get involved!</div>
</div>
<Footer />
</main>
</Layout>

View File

@ -1,59 +1,25 @@
---
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import Layout from "@/layouts/Layout.astro";
import { cn } from "@/lib/utils";
import "@/styles/globals.css";
---
<Layout>
<main class="flex min-h-screen flex-col">
<Navbar client:load />
<section>
<a
href="https://www.flaticon.com/free-icons/computer"
title="computer icons"
>
<img src="/computer.png" height="30px" width="30px"/></a
>
<main class="flex min-h-screen flex-col justify-between">
<div>
<Navbar />
<a
href="https://www.flaticon.com/free-icons/peopler"
title="people icons"
>
<img src="/people.png" height="30px" width="30px"/></a
>
<!-- <div class="flex flex-col items-end gap-8 md:flex-row md:gap-16"> -->
<!-- <div class="col-span-2 flex flex-1 flex-col"> -->
<h1 class="mb-6 text-4xl leading-tight">
<strong>Resist Tech Monopolies</strong><br />
<!-- <span class="font-light">Online Slide Maker Compilation</span> -->
</h1>
<!-- <Search client:load className="w-full" /> -->
<!-- </div> -->
<!-- </div> -->
</section>
<section>
<p>
Do you believe technology can and should be used for good? Do you think
a democratic internet could liberate us? Join us!
</p>
<div class="pl-4 pr-4">
<p>
Do you believe technology can and should be used for good? Do you
think a democratic internet could liberate us? Join us!
</p>
<p>We share this vision, and we want to work together to achieve it.</p>
</section>
<footer class="pb-32 pt-4 md:pb-8 md:pt-8">
<div class="container mx-auto px-4 text-sm md:px-0">
<div
class="flex flex-col-reverse items-center justify-between gap-2 md:flex-row"
>
<p class="text-muted-foreground">Copyleft 2025!</p>
<!-- <div class="flex items-center gap-1">
<a href="https://lukenguyen.me" target="_blank">
<Button variant="ghost" size="icon"><Globe /></Button>
</a>
</div> -->
</div>
<p>We share this vision, and we want to work together to achieve it.</p>
</div>
</footer>
</div>
<Footer />
</main></Layout
>