latest
This commit is contained in:
parent
2413228ea0
commit
ee2c96c885
13
src/components/Footer.tsx
Normal file
13
src/components/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
>
|
||||
|
Loading…
x
Reference in New Issue
Block a user