mirror of
https://github.com/MbinOrg/mbin-website.git
synced 2025-06-29 06:38:55 +00:00
FEAT: initial setup and servers page
This commit is contained in:
parent
ca3e55be5d
commit
d3e53e7c47
26 changed files with 7773 additions and 0 deletions
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
dist
|
||||
.solid
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.vinxi
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
32
README.md
Normal file
32
README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# SolidStart
|
||||
|
||||
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||
|
||||
## Creating a project
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init solid@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm init solid@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||
|
||||
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||
|
||||
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
|
8
app.config.ts
Normal file
8
app.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from '@solidjs/start/config';
|
||||
|
||||
export default defineConfig({
|
||||
ssr: false,
|
||||
// server: {
|
||||
// static: true,
|
||||
// },
|
||||
});
|
29
package.json
Normal file
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "example-with-tailwindcss",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vinxi dev",
|
||||
"build": "vinxi build",
|
||||
"start": "vinxi start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-icon/solid": "^2.1.1",
|
||||
"@kobalte/core": "^0.13.3",
|
||||
"@solidjs/router": "^0.13.6",
|
||||
"@solidjs/start": "^1.0.2",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"postcss": "^8.4.38",
|
||||
"solid-js": "^1.8.17",
|
||||
"solid-markdown": "^2.0.13",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vinxi": "^0.3.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
|
||||
}
|
6695
pnpm-lock.yaml
generated
Normal file
6695
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 664 B |
98
src/app.css
Normal file
98
src/app.css
Normal file
|
@ -0,0 +1,98 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--info: 204 94% 94%;
|
||||
--info-foreground: 199 89% 48%;
|
||||
|
||||
--success: 149 80% 90%;
|
||||
--success-foreground: 160 84% 39%;
|
||||
|
||||
--warning: 48 96% 89%;
|
||||
--warning-foreground: 25 95% 53%;
|
||||
|
||||
--error: 0 93% 94%;
|
||||
--error-foreground: 0 84% 60%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: #020617;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
@apply mt-2 text-5xl;
|
||||
}
|
||||
.markdown h2 {
|
||||
@apply mt-2 text-4xl;
|
||||
}
|
||||
.markdown h3 {
|
||||
@apply mt-2 text-3xl;
|
||||
}
|
||||
.markdown h4 {
|
||||
@apply mt-2 text-2xl;
|
||||
}
|
||||
.markdown h5 {
|
||||
@apply mt-2 text-xl;
|
||||
}
|
||||
.markdown h6 {
|
||||
@apply mt-2 text-lg;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
@apply text-sky-600;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
@apply my-2;
|
||||
}
|
20
src/app.tsx
Normal file
20
src/app.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { Suspense } from "solid-js";
|
||||
import Nav from "~/components/Nav";
|
||||
import "./app.css";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
root={props => (
|
||||
<>
|
||||
<Nav />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
</Router>
|
||||
);
|
||||
}
|
18
src/components/Chip.tsx
Normal file
18
src/components/Chip.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { ParentComponent, Show } from 'solid-js';
|
||||
import { Icon } from '@iconify-icon/solid';
|
||||
|
||||
const Chip: ParentComponent<{ icon?: string; title?: string }> = (props) => {
|
||||
return (
|
||||
<span
|
||||
class="p-1 m-1 border rounded-lg inline-flex items-center"
|
||||
title={props.title}
|
||||
>
|
||||
<Show when={props.icon}>
|
||||
<Icon icon={props.icon!} class="pr-1" />
|
||||
</Show>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chip;
|
10
src/components/Markdown.tsx
Normal file
10
src/components/Markdown.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { clientOnly } from '@solidjs/start';
|
||||
import { ParentComponent } from 'solid-js';
|
||||
|
||||
const ClientOnlyComp = clientOnly(() => import('./MarkdownInner'));
|
||||
|
||||
const Markdown: ParentComponent = (props) => {
|
||||
return <ClientOnlyComp>{props.children}</ClientOnlyComp>;
|
||||
};
|
||||
|
||||
export default Markdown;
|
11
src/components/MarkdownInner.tsx
Normal file
11
src/components/MarkdownInner.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { clientOnly } from '@solidjs/start';
|
||||
import { ParentComponent } from 'solid-js';
|
||||
import { SolidMarkdown } from 'solid-markdown';
|
||||
|
||||
const MarkdownInner: ParentComponent = (props) => {
|
||||
return (
|
||||
<SolidMarkdown class="text-left markdown">{props.children}</SolidMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownInner;
|
21
src/components/Nav.tsx
Normal file
21
src/components/Nav.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useLocation } from '@solidjs/router';
|
||||
|
||||
export default function Nav() {
|
||||
const location = useLocation();
|
||||
const active = (path: string) =>
|
||||
path == location.pathname
|
||||
? 'border-sky-600'
|
||||
: 'border-transparent hover:border-sky-600';
|
||||
return (
|
||||
<nav class="bg-sky-800">
|
||||
<ul class="container flex items-center p-3 text-gray-200">
|
||||
<li class={`border-b-2 ${active('/')} mx-1.5 sm:mx-6`}>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li class={`border-b-2 ${active('/servers')} mx-1.5 sm:mx-6`}>
|
||||
<a href="/servers">Servers</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
82
src/components/ui/accordion.tsx
Normal file
82
src/components/ui/accordion.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { JSX, splitProps, ValidComponent } from "solid-js"
|
||||
|
||||
import * as AccordionPrimitive from "@kobalte/core/accordion"
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
type AccordionItemProps<T extends ValidComponent = "div"> =
|
||||
AccordionPrimitive.AccordionItemProps<T> & {
|
||||
class?: string | undefined
|
||||
}
|
||||
|
||||
const AccordionItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, AccordionItemProps<T>>
|
||||
) => {
|
||||
const [local, others] = splitProps(props as AccordionItemProps, ["class"])
|
||||
return <AccordionPrimitive.Item class={cn("border-b", local.class)} {...others} />
|
||||
}
|
||||
|
||||
type AccordionTriggerProps<T extends ValidComponent = "button"> =
|
||||
AccordionPrimitive.AccordionTriggerProps<T> & {
|
||||
class?: string | undefined
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
const AccordionTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, AccordionTriggerProps<T>>
|
||||
) => {
|
||||
const [local, others] = splitProps(props as AccordionTriggerProps, ["class", "children"])
|
||||
return (
|
||||
<AccordionPrimitive.Header class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-expanded]>svg]:rotate-180",
|
||||
local.class
|
||||
)}
|
||||
{...others}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="size-4 shrink-0 transition-transform duration-200"
|
||||
>
|
||||
<path d="M6 9l6 6l6 -6" />
|
||||
</svg>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
type AccordionContentProps<T extends ValidComponent = "div"> =
|
||||
AccordionPrimitive.AccordionContentProps<T> & {
|
||||
class?: string | undefined
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
const AccordionContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, AccordionContentProps<T>>
|
||||
) => {
|
||||
const [local, others] = splitProps(props as AccordionContentProps, ["class", "children"])
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
class={cn(
|
||||
"animate-accordion-up overflow-hidden text-sm transition-all data-[expanded]:animate-accordion-down",
|
||||
local.class
|
||||
)}
|
||||
{...others}
|
||||
>
|
||||
<div class="pb-4 pt-0">{local.children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
52
src/components/ui/button.tsx
Normal file
52
src/components/ui/button.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { JSX, splitProps, ValidComponent } from "solid-js"
|
||||
|
||||
import * as ButtonPrimitive from "@kobalte/core/button"
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "size-10"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type ButtonProps<T extends ValidComponent = "button"> = ButtonPrimitive.ButtonRootProps<T> &
|
||||
VariantProps<typeof buttonVariants> & { class?: string | undefined; children?: JSX.Element }
|
||||
|
||||
const Button = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, ButtonProps<T>>
|
||||
) => {
|
||||
const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"])
|
||||
return (
|
||||
<ButtonPrimitive.Root
|
||||
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type { ButtonProps }
|
||||
export { Button, buttonVariants }
|
141
src/components/ui/dialog.tsx
Normal file
141
src/components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
|
||||
import * as DialogPrimitive from "@kobalte/core/dialog"
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal: Component<DialogPrimitive.DialogPortalProps> = (props) => {
|
||||
const [, rest] = splitProps(props, ["children"])
|
||||
return (
|
||||
<DialogPrimitive.Portal {...rest}>
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{props.children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogOverlayProps<T extends ValidComponent = "div"> =
|
||||
DialogPrimitive.DialogOverlayProps<T> & { class?: string | undefined }
|
||||
|
||||
const DialogOverlay = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, DialogOverlayProps<T>>
|
||||
) => {
|
||||
const [, rest] = splitProps(props as DialogOverlayProps, ["class"])
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
class={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogContentProps<T extends ValidComponent = "div"> =
|
||||
DialogPrimitive.DialogContentProps<T> & {
|
||||
class?: string | undefined
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
const DialogContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, DialogContentProps<T>>
|
||||
) => {
|
||||
const [, rest] = splitProps(props as DialogContentProps, ["class", "children"])
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
class={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{props.children}
|
||||
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[expanded]:bg-accent data-[expanded]:text-muted-foreground">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="size-4"
|
||||
>
|
||||
<path d="M18 6l-12 12" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.CloseButton>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogHeader: Component<ComponentProps<"div">> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", props.class)} {...rest} />
|
||||
)
|
||||
}
|
||||
|
||||
const DialogFooter: Component<ComponentProps<"div">> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<div
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogTitleProps<T extends ValidComponent = "h2"> = DialogPrimitive.DialogTitleProps<T> & {
|
||||
class?: string | undefined
|
||||
}
|
||||
|
||||
const DialogTitle = <T extends ValidComponent = "h2">(
|
||||
props: PolymorphicProps<T, DialogTitleProps<T>>
|
||||
) => {
|
||||
const [, rest] = splitProps(props as DialogTitleProps, ["class"])
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogDescriptionProps<T extends ValidComponent = "p"> =
|
||||
DialogPrimitive.DialogDescriptionProps<T> & {
|
||||
class?: string | undefined
|
||||
}
|
||||
|
||||
const DialogDescription = <T extends ValidComponent = "p">(
|
||||
props: PolymorphicProps<T, DialogDescriptionProps<T>>
|
||||
) => {
|
||||
const [, rest] = splitProps(props as DialogDescriptionProps, ["class"])
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
}
|
4
src/entry-client.tsx
Normal file
4
src/entry-client.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
// @refresh reload
|
||||
import { mount, StartClient } from "@solidjs/start/client";
|
||||
|
||||
mount(() => <StartClient />, document.getElementById("app")!);
|
21
src/entry-server.tsx
Normal file
21
src/entry-server.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// @refresh reload
|
||||
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||
|
||||
export default createHandler(() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">{children}</div>
|
||||
{scripts}
|
||||
</body>
|
||||
</html>
|
||||
)}
|
||||
/>
|
||||
));
|
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="@solidjs/start/env" />
|
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
25
src/routes/[...404].tsx
Normal file
25
src/routes/[...404].tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { A } from "@solidjs/router";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main class="text-center mx-auto text-gray-700 p-4">
|
||||
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">Not Found</h1>
|
||||
<p class="mt-8">
|
||||
Visit{" "}
|
||||
<a href="https://solidjs.com" target="_blank" class="text-sky-600 hover:underline">
|
||||
solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build Solid apps.
|
||||
</p>
|
||||
<p class="my-4">
|
||||
<A href="/" class="text-sky-600 hover:underline">
|
||||
Home
|
||||
</A>
|
||||
{" - "}
|
||||
<A href="/about" class="text-sky-600 hover:underline">
|
||||
About Page
|
||||
</A>
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
14
src/routes/index.tsx
Normal file
14
src/routes/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Button } from '~/components/ui/button';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main class="text-center mx-auto text-gray-700 p-4">
|
||||
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">
|
||||
Mbin
|
||||
</h1>
|
||||
<Button href="/servers" as="a">
|
||||
Join a Server
|
||||
</Button>
|
||||
</main>
|
||||
);
|
||||
}
|
319
src/routes/servers.tsx
Normal file
319
src/routes/servers.tsx
Normal file
|
@ -0,0 +1,319 @@
|
|||
import { For, Show, createSignal } from 'solid-js';
|
||||
import { createAsync, cache } from '@solidjs/router';
|
||||
import Markdown from '~/components/Markdown';
|
||||
import Chip from '~/components/Chip';
|
||||
import { Button } from '~/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '~/components/ui/dialog';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '~/components/ui/accordion';
|
||||
|
||||
type Server = {
|
||||
domain: string;
|
||||
version: string;
|
||||
name: string;
|
||||
description: string;
|
||||
openRegistrations: boolean;
|
||||
federationEnabled: boolean;
|
||||
language: string;
|
||||
contactEmail: string;
|
||||
totalUsers: number;
|
||||
activeHalfyearUsers: number;
|
||||
activeMonthUsers: number;
|
||||
localPosts: number;
|
||||
localComments: number;
|
||||
pages: {
|
||||
about?: string;
|
||||
contact?: string;
|
||||
faq?: string;
|
||||
privacyPolicy?: string;
|
||||
terms?: string;
|
||||
};
|
||||
defederated: string[];
|
||||
};
|
||||
|
||||
const pageNames: Required<Server['pages']> = {
|
||||
about: 'About',
|
||||
contact: 'Contact',
|
||||
faq: 'Frequently Asked Questions',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
terms: 'Terms of Service',
|
||||
};
|
||||
|
||||
const getServers = cache(async () => {
|
||||
'use server';
|
||||
|
||||
const fediverseObserver = await (
|
||||
await fetch('https://api.fediverse.observer/', {
|
||||
body: '{"query":"{nodes(softwarename:\\"mbin\\" status: \\"UP\\"){domain}}"}',
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
|
||||
const servers: string[] = fediverseObserver.data.nodes.map((v) => v.domain);
|
||||
|
||||
return (
|
||||
await Promise.allSettled(
|
||||
servers.map(async (serverHost) => {
|
||||
console.log('START:', serverHost);
|
||||
|
||||
const jsonNodeInfo = await (
|
||||
await fetch(`https://${serverHost}/nodeinfo/2.1.json`)
|
||||
).json();
|
||||
|
||||
if (jsonNodeInfo.software.name != 'mbin') {
|
||||
throw new Error(
|
||||
`${serverHost} software does not match mbin (skipped)`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonApiInfo = await (
|
||||
await fetch(`https://${serverHost}/api/info`)
|
||||
).json();
|
||||
if (jsonApiInfo.websiteDomain != serverHost) {
|
||||
throw new Error(`${serverHost} api not setup correctly (skipped)`);
|
||||
}
|
||||
const jsonApiInstance = await (
|
||||
await fetch(`https://${serverHost}/api/instance`)
|
||||
).json();
|
||||
const jsonApiDefederated = await (
|
||||
await fetch(`https://${serverHost}/api/defederated`)
|
||||
).json();
|
||||
|
||||
console.log('FINISH:', serverHost);
|
||||
|
||||
const server: Server = {
|
||||
domain: serverHost,
|
||||
version: jsonNodeInfo.software.version,
|
||||
name: jsonNodeInfo.metadata.nodeName,
|
||||
description: jsonNodeInfo.metadata.nodeDescription,
|
||||
openRegistrations: jsonNodeInfo.openRegistrations,
|
||||
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
||||
language: jsonApiInfo.websiteDefaultLang ?? 'en',
|
||||
contactEmail: jsonApiInfo.websiteContactEmail,
|
||||
totalUsers: jsonNodeInfo.usage.users.total,
|
||||
activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear,
|
||||
activeMonthUsers: jsonNodeInfo.usage.users.activeMonth,
|
||||
localPosts: jsonNodeInfo.usage.localPosts,
|
||||
localComments: jsonNodeInfo.usage.localComments,
|
||||
pages: jsonApiInstance,
|
||||
defederated: jsonApiDefederated.instances ?? [],
|
||||
};
|
||||
|
||||
return server;
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((v) => v.status == 'fulfilled')
|
||||
.map((v) => v.value as Server);
|
||||
}, 'users');
|
||||
|
||||
export const route = {
|
||||
load: () => getServers(),
|
||||
};
|
||||
|
||||
const languageNames = new Intl.DisplayNames(['en'], {
|
||||
type: 'language',
|
||||
});
|
||||
|
||||
export default function ServersPage() {
|
||||
const servers = createAsync(() => getServers());
|
||||
|
||||
const [filterRegistration, setFilterRegistration] = createSignal(true);
|
||||
const [langFilter, setLangFilter] = createSignal<string>('');
|
||||
|
||||
const resultServers = () => {
|
||||
if (!servers()) return undefined;
|
||||
|
||||
return servers()!.filter(
|
||||
(server) =>
|
||||
(!filterRegistration() || server.openRegistrations) &&
|
||||
(!langFilter() || server.language == langFilter()),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main class="text-center mx-auto text-gray-700 p-4 max-w-screen-xl">
|
||||
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
|
||||
Mbin Servers
|
||||
</h1>
|
||||
Open Registration Only:{' '}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filterRegistration()}
|
||||
onChange={(e) => setFilterRegistration(e.target.checked)}
|
||||
/>
|
||||
<br />
|
||||
Language:
|
||||
<select onChange={(e) => setLangFilter(e.target.value)}>
|
||||
<option value="">All Languages</option>
|
||||
<For each={[...new Set(servers()?.map((v) => v.language))]}>
|
||||
{(lang) => <option value={lang}>{languageNames.of(lang)}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<div
|
||||
class="grid"
|
||||
style={{
|
||||
'grid-template-columns': 'repeat(auto-fit, minmax(350px, 1fr))',
|
||||
}}
|
||||
>
|
||||
<For each={resultServers()}>
|
||||
{(server) => {
|
||||
const StatChips = () => (
|
||||
<>
|
||||
<Chip>Version {server.version}</Chip>
|
||||
<Chip title="Language" icon="material-symbols:language">
|
||||
{languageNames.of(server.language)}
|
||||
</Chip>
|
||||
<Chip title="Total users" icon="material-symbols:person">
|
||||
{server.totalUsers}
|
||||
</Chip>
|
||||
<Chip title="Active users" icon="material-symbols:person-check">
|
||||
{server.activeMonthUsers}
|
||||
<span class="opacity-75">/month</span>
|
||||
</Chip>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="border rounded-lg p-4 m-4 text-white block relative">
|
||||
<a
|
||||
class="text-3xl text-sky-600 block text-center"
|
||||
href={`https://${server.domain}`}
|
||||
>
|
||||
<img
|
||||
src={`https://${server.domain}/favicon.ico`}
|
||||
class="inline-block size-16"
|
||||
/>
|
||||
<br />
|
||||
{server.domain}
|
||||
</a>
|
||||
|
||||
<Show when={server.name != '' && server.name != server.domain}>
|
||||
<div class="text-2xl text-sky-600 font-light">
|
||||
{server.name}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<p>{server.description}</p>
|
||||
|
||||
<StatChips />
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
as={Button<'button'>}
|
||||
variant="outline"
|
||||
class="my-1 w-full"
|
||||
>
|
||||
More info
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-screen-xl max-h-[95%] overflow-scroll">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{server.domain}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{server.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<StatChips />
|
||||
<Chip
|
||||
title="Active users"
|
||||
icon="material-symbols:person-check"
|
||||
>
|
||||
{server.activeHalfyearUsers}
|
||||
<span class="opacity-75">/halfyear</span>
|
||||
</Chip>
|
||||
<Chip title="Local posts" icon="material-symbols:news">
|
||||
{server.localPosts}
|
||||
</Chip>
|
||||
<Chip
|
||||
title="Local comments"
|
||||
icon="material-symbols:comment"
|
||||
>
|
||||
{server.localComments}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
<Accordion
|
||||
multiple={false}
|
||||
collapsible
|
||||
defaultValue={['about']}
|
||||
>
|
||||
<For each={Object.entries(server.pages)}>
|
||||
{([page, pageContent]) => (
|
||||
<Show when={pageContent || page === 'contact'}>
|
||||
<AccordionItem value={page}>
|
||||
<AccordionTrigger>
|
||||
{pageNames[page as keyof Server['pages']]}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Show when={page == 'contact'}>
|
||||
<Button
|
||||
href={`mailto:${server.contactEmail}`}
|
||||
as="a"
|
||||
>
|
||||
Email admin: {server.contactEmail}
|
||||
</Button>
|
||||
</Show>
|
||||
<Markdown>{pageContent}</Markdown>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<Show when={server.defederated.length}>
|
||||
<AccordionItem value="defederated">
|
||||
<AccordionTrigger>
|
||||
Defederated Servers ({server.defederated.length})
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<For each={server.defederated}>
|
||||
{(server) => <Chip>{server}</Chip>}
|
||||
</For>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Show>
|
||||
</Accordion>
|
||||
|
||||
<Show when={server.openRegistrations}>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
href={`https://${server.domain}/register`}
|
||||
as="a"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Show>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Show when={server.openRegistrations}>
|
||||
<Button
|
||||
href={`https://${server.domain}/register`}
|
||||
as="a"
|
||||
variant="default"
|
||||
class="my-1 w-full"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
97
tailwind.config.cjs
Normal file
97
tailwind.config.cjs
Normal file
|
@ -0,0 +1,97 @@
|
|||
/**@type {import("tailwindcss").Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
info: {
|
||||
DEFAULT: 'hsl(var(--info))',
|
||||
foreground: 'hsl(var(--info-foreground))',
|
||||
},
|
||||
success: {
|
||||
DEFAULT: 'hsl(var(--success))',
|
||||
foreground: 'hsl(var(--success-foreground))',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'hsl(var(--warning))',
|
||||
foreground: 'hsl(var(--warning-foreground))',
|
||||
},
|
||||
error: {
|
||||
DEFAULT: 'hsl(var(--error))',
|
||||
foreground: 'hsl(var(--error-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
xl: 'calc(var(--radius) + 4px)',
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: 0 },
|
||||
to: { height: 'var(--kb-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--kb-accordion-content-height)' },
|
||||
to: { height: 0 },
|
||||
},
|
||||
'content-show': {
|
||||
from: { opacity: 0, transform: 'scale(0.96)' },
|
||||
to: { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
'content-hide': {
|
||||
from: { opacity: 1, transform: 'scale(1)' },
|
||||
to: { opacity: 0, transform: 'scale(0.96)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'content-show': 'content-show 0.2s ease-out',
|
||||
'content-hide': 'content-hide 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
};
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"vinxi/types/client"
|
||||
],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
11
ui.config.json
Normal file
11
ui.config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"tsx": true,
|
||||
"componentDir": "./src/components/ui",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.cjs",
|
||||
"css": "src/app.css"
|
||||
},
|
||||
"aliases": {
|
||||
"path": "~/*"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue