Moved from Solidjs to Nextjs for better compatibility with docs & blogs stuff

This commit is contained in:
Kingkor Roy Tirtho 2022-07-17 15:13:42 +06:00
parent 852dce34ce
commit 5d099fb2e4
43 changed files with 3939 additions and 1420 deletions

6
website/.eslintrc.json Executable file
View File

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"prettier"
]
}

37
website/.gitignore vendored Normal file → Executable file
View File

@ -1,2 +1,35 @@
node_modules # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
dist
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo

35
website/README.md Normal file → Executable file
View File

@ -1,3 +1,34 @@
# Website This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
The official Website of Spotube ## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -0,0 +1,123 @@
import {
Button,
Heading,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Text,
useDisclosure,
VStack,
} from "@chakra-ui/react";
import { FC, ReactNode, useEffect, useState } from "react";
const AdDetector: FC<{ children: ReactNode }> = ({ children }) => {
const [adBlockEnabled, setAdBlockEnabled] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const [joke, setJoke] = useState<Record<string, any>>({});
useEffect(() => {
(async () => {
const googleAdUrl =
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
try {
await fetch(new Request(googleAdUrl));
} catch (e) {
setAdBlockEnabled(true);
setJoke(
await (
await fetch(
"https://v2.jokeapi.dev/joke/Any?blacklistFlags=racist,sexist"
)
).json()
);
onOpen();
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent mt="5" mx="3">
<ModalHeader>Support the Creator💚</ModalHeader>
<ModalBody>
<p>
Open source developers work really hard to provide the best,
secure & efficient software experience for you & people all around
the world. Most of the time we work without any wages at all but
we need minimum support to live & these <b> Ads Helps Us</b> earn
the minimum wage that we need to live.{" "}
<Text color="green.500" fontWeight="bold" textAlign="justify">
So, please support Spotube by disabling the AdBlocker on this
page or by sponsoring or donating to our collectives directly
</Text>
</p>
</ModalBody>
<ModalFooter>
<Button onClick={() => window.location.reload()}>
Reload without AdBlocker
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{!adBlockEnabled ? (
children
) : (
<Stack
direction="column"
w="100vw"
h="100vh"
justifyContent="space-between"
alignItems="center"
p="5"
>
<Heading></Heading>
<VStack spacing="2" alignItems="flex-start">
<Heading size="sm">Here&apos;s something interesting:</Heading>
<Heading size="md">
{joke.joke ?? (
<>
<p>{joke.setup}</p>
<p>{joke.delivery}</p>
</>
)}
</Heading>
</VStack>
<VStack justifySelf="flex-end">
<Heading
mt="10"
size={{
base: "lg",
lg: "xl",
}}
maxW="700px"
textAlign="justify"
lineHeight="1.5"
>
Be grateful for all the favors you get. But don&apos;t let it
become a pile of debt. Try returning them as soon as you can.
You&apos;ll feel relieved
</Heading>
<Heading
size={{
lg: "lg",
base: "md",
}}
alignSelf="flex-end"
>
- Kingkor Roy Tirtho
</Heading>
</VStack>
</Stack>
)}
</>
);
};
export default AdDetector;

View File

@ -0,0 +1,48 @@
import {
Button,
ButtonGroup,
Heading,
HStack,
IconButton,
useColorMode,
} from "@chakra-ui/react";
import NavLink from "next/link";
import { GoLightBulb } from "react-icons/go";
import { FiSun, } from "react-icons/fi";
const Navbar = () => {
const { colorMode, toggleColorMode } = useColorMode();
return (
<HStack justifyContent="space-between" as="nav" w="full">
<HStack alignItems="center">
<NavLink href="/" passHref>
<Heading p="2" as="a" size="lg" mr="2">
Spotube
</Heading>
</NavLink>
<ButtonGroup>
<NavLink href="/other-downloads" passHref>
<Button as="a" colorScheme="gray" variant="ghost">
Other Downloads
</Button>
</NavLink>
<NavLink href="/about" passHref>
<Button as="a" variant="ghost" colorScheme="gray">
About
</Button>
</NavLink>
</ButtonGroup>
</HStack>
<IconButton
variant="ghost"
icon={colorMode == "light" ? <GoLightBulb /> : <FiSun />}
aria-label="Dark Mode Toggle"
onClick={() => {
toggleColorMode();
}}
/>
</HStack>
);
};
export default Navbar;

View File

@ -0,0 +1,72 @@
import Script from "next/script";
import { FC, useId } from "react";
type AdComponent = FC<{
slot: string;
}>;
export const DisplayAd: AdComponent = ({ slot }) => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
data-ad-slot={slot}
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
<Script
id={id + "#" + slot}
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};
export const GridMultiplexAd: AdComponent = ({ slot }) => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-format="autorelaxed"
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
data-ad-slot={slot}
></ins>
<Script
id={id + "#" + slot}
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};
export const InFeedAd = () => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-format="fluid"
data-ad-layout-key="-e5+6n-34-bt+x0"
data-ad-client="ca-pub-6419300932495863"
data-ad-slot="6460144484"
></ins>
<Script
id={id}
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};

View File

@ -0,0 +1,17 @@
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = (url: any) => {
(window as any).gtag("config", GA_TRACKING_ID, {
page_path: url,
});
};
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }: any) => {
(window as any).gtag("event", action, {
event_category: category,
event_label: label,
value: value,
});
};

View File

@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
export enum Platform {
linux = "Linux",
windows = "Windows",
mac = "Mac",
android = "Android",
}
export function usePlatform(): Platform {
const [platform, setPlatform] = useState(Platform.linux);
useEffect(() => {
const platform = (
((navigator as unknown as any).userAgentData?.platform as
| string
| undefined) ?? navigator.platform
).toLowerCase();
if (platform.includes("windows")) setPlatform(Platform.windows);
else if (platform.includes("mac")) setPlatform(Platform.mac);
else if (platform.includes("android")) setPlatform(Platform.android);
}, []);
return platform;
}

View File

@ -1,52 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/src/assets/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/src/assets/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/src/assets/favicon-16x16.png"
/>
<link rel="manifest" href="/src/assets/site.webmanifest" />
<title>Spotube</title>
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6419300932495863"
crossorigin="anonymous"
></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-L593ZMVP33"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-L593ZMVP33");
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
import { Link as Anchor, Heading, Text, chakra } from "@chakra-ui/react";
export const MarkdownComponentDefs = {
a: (props: any) => <Anchor {...props} color="blue.500" />,
h1: (props: any) => <Heading {...props} size="xl" mt="5" mb="1.5" />,
h2: (props: any) => <Heading {...props} size="lg" mt="5" mb="1.5" />,
h3: (props: any) => <Heading {...props} size="md" mt="5" mb="1.5" />,
h4: (props: any) => <Heading {...props} size="sm" />,
h5: (props: any) => <Heading {...props} size="xs" />,
h6: (props: any) => <Heading {...props} size="xs" />,
p: (props: any) => <Text {...props} />,
li: (props: any) => <chakra.li {...props} ml="4" />,
};

5
website/next-env.d.ts vendored Executable file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

7
website/next.config.js Executable file
View File

@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

View File

@ -1,30 +1,34 @@
{ {
"name": "vite-template-solid", "name": "website",
"version": "0.0.0", "version": "0.1.0",
"description": "", "private": true,
"scripts": { "scripts": {
"start": "vite", "dev": "next dev",
"dev": "vite", "build": "next build",
"build": "vite build", "start": "next start",
"serve": "vite preview" "lint": "next lint"
},
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.4",
"vite": "^3.0.0",
"vite-plugin-solid": "^2.3.0"
}, },
"dependencies": { "dependencies": {
"@hope-ui/solid": "^0.6.2", "@chakra-ui/react": "^2.2.4",
"@chakra-ui/theme-tools": "^2.0.5",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@octokit/rest": "^19.0.3", "@octokit/rest": "^19.0.3",
"@stitches/core": "^1.2.8", "framer-motion": "^6",
"isomorphic-fetch": "^3.0.0", "next": "12.2.2",
"remark-gfm": "^3.0.1", "react": "18.2.0",
"solid-app-router": "^0.4.1", "react-dom": "18.2.0",
"solid-cached-resource": "^0.3.0", "react-icons": "^4.4.0",
"solid-icons": "^0.5.0", "react-markdown": "^8.0.3",
"solid-js": "^1.4.7", "remark-gfm": "^3.0.1"
"solid-markdown": "^1.2.0", },
"solid-transition-group": "^0.0.10" "devDependencies": {
"@types/node": "18.0.5",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"eslint": "8.20.0",
"eslint-config-next": "12.2.2",
"eslint-config-prettier": "^8.5.0",
"typescript": "4.7.4"
} }
} }

125
website/pages/_app.tsx Executable file
View File

@ -0,0 +1,125 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import {
ChakraProvider,
extendTheme,
withDefaultColorScheme,
} from "@chakra-ui/react";
import Navbar from "components/Navbar";
import { mode } from "@chakra-ui/theme-tools";
import Head from "next/head";
import Script from "next/script";
import * as gtag from "configurations/gtag";
import { useRouter } from "next/router";
import { useEffect } from "react";
import AdDetector from "components/AdDetector";
const customTheme = extendTheme(
{
styles: {
global: (props: any) => ({
body: {
bg: mode("white", "#171717")(props),
},
}),
},
colors: {
green: {
50: "#d4f3df",
100: "#b7ecca",
200: "#9be4b4",
300: "#61d48a",
400: "#45cd74",
500: "#32ba62",
600: "#2b9e53",
700: "#238144",
800: "#1b6435",
900: "#134826",
},
components: {
Link: {
baseStyle: {
color: "green",
},
},
},
},
},
withDefaultColorScheme({ colorScheme: "green" })
);
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url: string) => {
gtag.pageview(url);
};
router.events.on("routeChangeComplete", handleRouteChange);
router.events.on("hashChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
router.events.off("hashChangeComplete", handleRouteChange);
};
}, [router.events]);
return (
<>
<Script
async
onError={(e) => {
console.error("Script failed to load", e);
}}
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}`}
crossOrigin="anonymous"
/>
{/* Global Site Tag (gtag.js) - Google Analytics */}
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtag.GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
<ChakraProvider theme={customTheme}>
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<title>Spotube</title>
</Head>
<AdDetector>
<Navbar />
<Component {...pageProps} />
</AdDetector>
</ChakraProvider>
</>
);
}
export default MyApp;

13
website/pages/api/hello.ts Executable file
View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

106
website/pages/index.tsx Executable file
View File

@ -0,0 +1,106 @@
import {
Heading,
Menu,
ButtonGroup,
Button,
IconButton,
MenuItem,
VStack,
MenuButton,
Link as Anchor,
chakra,
MenuList,
} from "@chakra-ui/react";
import { FaCaretDown } from "react-icons/fa";
import { DisplayAd } from "../components/special";
import { Platform, usePlatform } from "../hooks/usePlatform";
const baseURL = "https://github.com/KRTirtho/spotube/releases/latest/download/";
const DownloadLinks = Object.freeze({
[Platform.linux]: [
{ name: "deb", url: baseURL + "Spotube-linux-x86_64.deb" },
{ name: "tar", url: baseURL + "Spotube-linux-x86_64.tar.xz" },
{ name: "AppImage", url: baseURL + "Spotube-linux-x86_64.AppImage" },
],
[Platform.android]: [
{
name: "apk",
url: baseURL + "Spotube-android-all-arch.apk",
},
],
[Platform.mac]: [{ name: "dmg", url: baseURL + "Spotube-macos-x86_64.dmg" }],
[Platform.windows]: [
{ name: "exe", url: baseURL + "Spotube-windows-x86_64-setup.exe" },
{ name: "nupkg", url: baseURL + "Spotube-windows-x86_64.nupkg " },
],
});
const Root = () => {
const platform = usePlatform();
const allPlatforms = Object.entries(Platform)
.map(([, value]) => {
return DownloadLinks[value].map((s) => ({
...s,
name: `${value} (.${s.name})`,
}));
})
.flat(1);
const currentPlatform = DownloadLinks[platform][0];
return (
<>
<VStack spacing="$4" alignItems="stretch">
<chakra.section
h="60vh"
bgColor="#f5f5f5"
bgImage="url(/spotube-screenshot-web.jpg)"
bgRepeat="no-repeat"
bgSize="contain"
bgPos="right"
>
<VStack mt="10" mx="6" spacing="4" alignItems="flex-start">
<Heading color="#212121" size="2xl">
Spotube
</Heading>
<Heading color="#212121" size="lg" textAlign="justify" maxW="500px">
A fast, modern, lightweight & efficient Spotify Music Client for
every platform
</Heading>
<Menu placement="bottom-end">
<ButtonGroup spacing="0.5">
<Button
variant="solid"
as={Anchor}
href={currentPlatform.url}
_hover={{ textDecoration: "none" }}
>
Download for {platform} (.{currentPlatform.name})
</Button>
<MenuButton
aria-label="Show More Downloads"
as={IconButton}
variant="solid"
icon={<FaCaretDown />}
/>
</ButtonGroup>
<MenuList>
{allPlatforms.map(({ name, url }) => {
return (
<MenuItem key={url} as={Anchor} href={url}>
{name}
</MenuItem>
);
})}
</MenuList>
</Menu>
</VStack>
</chakra.section>
<DisplayAd slot="9501208974" />
</VStack>
</>
);
};
export default Root;

View File

@ -0,0 +1,36 @@
import { Link as Anchor, Heading, VStack, chakra } from "@chakra-ui/react";
import NavLink from "next/link";
import { GridMultiplexAd } from "components/special";
import { useRouter } from "next/router";
function OtherDownloads() {
const router = useRouter();
return (
<>
<VStack my="20" mx="5">
<Heading size="lg">Download other versions of Spotube</Heading>
<chakra.ul pl="5">
<li>
<NavLink href={router.pathname + "/stable-downloads"} passHref>
<Anchor color="blue.500">
Download previous versions of Spotube
</Anchor>
</NavLink>
</li>
<li>
<NavLink href={router.pathname + "/nightly-downloads"} passHref>
<Anchor color="blue.500">
Download Bleeding Edge Nightly version of Spotube
</Anchor>
</NavLink>
&nbsp;(Nightly&nbsp;releases)
</li>
</chakra.ul>
</VStack>
<GridMultiplexAd slot="4575915852" />
</>
);
}
export default OtherDownloads;

View File

@ -0,0 +1,87 @@
import {
Link as Anchor,
Button,
Heading,
Table,
Td,
Th,
VStack,
chakra,
Text,
Tr,
HStack,
} from "@chakra-ui/react";
import { GridMultiplexAd } from "components/special";
import NavLink from "next/link";
const baseURL =
"https://nightly.link/KRTirtho/spotube/workflows/spotube-nightly/build/";
const DownloadLinks = Object.freeze({
Linux: baseURL + "Spotube-Linux-Bundle.zip",
Android: baseURL + "Spotube-Android-Bundle.zip",
Windows: baseURL + "Spotube-Windows-Bundle.zip",
MacOS: baseURL + "Spotube-Macos-Bundle.zip",
});
function NightlyDownloads() {
return (
<>
<VStack>
<VStack
alignSelf="center"
alignItems="flex-start"
spacing="4"
maxW="500px"
overflow="auto"
m="5"
>
<Heading size="2xl">Nightly Release</Heading>
<Text>Download latest & most bleeding edge version of Spotube</Text>
<Text size="sm" color="red.500" textAlign="justify">
Disclaimer!: Nightly versions are untested and not the final version
of spotube. So it can consists of many hidden or unknown bugs but at
the same time will be opted with latest & greatest features too. So
use it at your own risk! If you don&apos;t know what you&apos;re
doing than we recommend you to download the{" "}
<NavLink href="/" passHref>
<Anchor color="blue.500">latest stable version of</Anchor>
</NavLink>{" "}
Spotube
</Text>
<chakra.section
border="2px solid"
borderColor="gray"
borderRadius="md"
px="4"
py="2"
w="100%"
>
{Object.entries(DownloadLinks).map(([platform, url]) => {
const segments = url.split("/");
return (
<HStack key={url}>
<Text w="100px">{platform}</Text>
<Anchor
overflowWrap="break-word"
wordBreak="break-word"
w="full"
href={url}
color="blue.500"
>
{segments.at(segments.length - 1)?.replace("build", "")}
</Anchor>
</HStack>
);
})}
</chakra.section>
</VStack>
<chakra.div w="full">
<GridMultiplexAd slot="3192619797" />
</chakra.div>
</VStack>
</>
);
}
export default NightlyDownloads;

View File

@ -4,20 +4,20 @@ import {
AccordionIcon, AccordionIcon,
AccordionItem, AccordionItem,
AccordionPanel, AccordionPanel,
Anchor, Link as Anchor,
Heading, Heading,
HStack, HStack,
Skeleton,
Text, Text,
VStack, VStack,
} from "@hope-ui/solid"; chakra,
} from "@chakra-ui/react";
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"; import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
import { createCachedResource } from "solid-cached-resource"; import ReactMarkdown from "react-markdown";
import SolidMarkdown from "solid-markdown"; import { Platform } from "hooks/usePlatform";
import { Platform } from "../hooks/usePlatform";
import gfm from "remark-gfm"; import gfm from "remark-gfm";
import { MarkdownComponentDefs } from "../misc/MarkdownComponentDefs"; import { DisplayAd, InFeedAd } from "components/special";
import { DisplayAd } from "../components/special"; import { GetServerSideProps, NextPage } from "next";
import { MarkdownComponentDefs } from "misc/MarkdownComponentDefs";
enum AssetTypes { enum AssetTypes {
sums = "sums", sums = "sums",
@ -27,30 +27,73 @@ enum AssetTypes {
android = "android", android = "android",
} }
export const octokit = new Octokit(); export const octokit: Octokit = new Octokit();
function StableDownloads() {
const [data] = createCachedResource("gh-releases", () => { type ReleaseResponse = {
return octokit.repos.listReleases({ id: number;
body: string | null | undefined;
tag_name: string;
assets: {
id: number;
name: string;
browser_download_url: string;
}[];
}[];
type Props = {
data: ReleaseResponse;
};
export const getServerSideProps: GetServerSideProps<Props> = async ({
res,
}) => {
res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59"
);
const { data } = await octokit.repos.listReleases({
owner: "KRTirtho", owner: "KRTirtho",
repo: "spotube", repo: "spotube",
}); });
const releaseResponse: ReleaseResponse = data.map((data) => {
return {
tag_name: data.tag_name,
id: data.id,
body: data.body,
assets: data.assets.map((asset) => ({
id: asset.id,
name: asset.name,
browser_download_url: asset.browser_download_url,
})),
};
}); });
return {
props: {
data: releaseResponse,
},
};
};
const StableDownloads: NextPage<Props> = ({ data }) => {
return ( return (
<VStack alignItems="stretch" m="$3"> <VStack alignItems="stretch" m="3">
<Heading size="3xl">Previous Versions</Heading> <Heading size="xl">Previous Versions</Heading>
<Text my="$5"> <Text my="5">
If any of your version is not working correctly than you can download & If any of your version is not working correctly than you can download &
use previous versions of Spotube too use previous versions of Spotube too
</Text> </Text>
<HStack alignItems="flex-start"> <HStack alignItems="flex-start" wrap="wrap">
<VStack alignItems="stretch" spacing="$3" mr="$1"> <VStack
{data.loading && alignItems="stretch"
!data.latest && [ w={{
<Skeleton h="$6" w="$56" />, base: "full",
...Array.from({ length: 8 }, () => <Skeleton h="$6" w="$96" />), sm: "70%",
]} md: "60%",
{(data.latest ?? data())?.data.map((release, i) => { }}
spacing="3"
mr="1"
>
{data.map((release, i) => {
const releaseSome = release.assets const releaseSome = release.assets
.map((asset) => { .map((asset) => {
const platform = const platform =
@ -70,46 +113,53 @@ function StableDownloads() {
}; };
return ( return (
<VStack <VStack
py="$3" key={release.id}
py="3"
alignItems="flex-start" alignItems="flex-start"
borderBottom="1px solid grey" borderBottom="1px solid grey"
_last={{ borderBottom: "none" }} _last={{ borderBottom: "none" }}
> >
<Heading size="xl"> <Heading size="md">
Version{" "} Version{" "}
<Text as="span" color="$success8"> <Text as="span" color="green.500">
{release.tag_name} {release.tag_name}
</Text>{" "} </Text>{" "}
{i == 0 && "(Latest)"} {i == 0 && "(Latest)"}
</Heading> </Heading>
{Object.entries(releaseSome).map(([type, assets]) => { {Object.entries(releaseSome).map(([type, assets], i) => {
return ( return (
<HStack py="$2" alignItems="flex-start"> <HStack key={i} spacing={0} py="2" alignItems="flex-start">
<Heading <Heading
w={90} w={90}
p="$2" p="2"
color="$info12" colorScheme="blue"
border={`2px solid #404040`} border="2px solid"
borderColor="gray"
borderRadius="5px 0 0 5px" borderRadius="5px 0 0 5px"
borderRight="none" borderRight="none"
size="sm"
> >
{type[0].toUpperCase() + type.slice(1)} {type[0].toUpperCase() + type.slice(1)}
</Heading> </Heading>
<VStack <VStack
alignItems="flex-start" alignItems="flex-start"
border={`2px solid #404040`} border="2px solid"
borderColor="gray"
borderRadius={`0 5px 5px ${ borderRadius={`0 5px 5px ${
assets.length !== 1 ? 5 : 0 assets.length !== 1 ? 5 : 0
}px`} }px`}
w="$72" w="72"
> >
{assets.map((asset) => { {assets.map((asset) => {
return ( return (
<Anchor <Anchor
color="$info11" key={asset.id}
width="$full" color="blue.500"
p="$2" width="full"
href={asset.name} p="1.5"
href={asset.browser_download_url}
target="_blank"
referrerPolicy="no-referrer"
> >
{asset.name} {asset.name}
</Anchor> </Anchor>
@ -119,18 +169,18 @@ function StableDownloads() {
</HStack> </HStack>
); );
})} })}
<Accordion defaultIndex={i}> <Accordion defaultIndex={i} allowToggle>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
Release Notes <AccordionIcon /> Release Notes <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel> <AccordionPanel>
<SolidMarkdown <ReactMarkdown
components={MarkdownComponentDefs} components={MarkdownComponentDefs}
remarkPlugins={[gfm]} remarkPlugins={[gfm]}
> >
{release.body ?? ""} {release.body ?? ""}
</SolidMarkdown> </ReactMarkdown>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
@ -138,15 +188,22 @@ function StableDownloads() {
); );
})} })}
</VStack> </VStack>
<VStack id="Ad"> <chakra.div
id="Ad"
w={{
base: "full",
sm: "25%",
md: "35%",
}}
>
<DisplayAd slot="1391349310" /> <DisplayAd slot="1391349310" />
<DisplayAd slot="6452104301" /> <DisplayAd slot="6452104301" />
<DisplayAd slot="1199777626" /> <DisplayAd slot="1199777626" />
<DisplayAd slot="2001723409" /> <DisplayAd slot="2001723409" />
</VStack> </chakra.div>
</HStack> </HStack>
</VStack> </VStack>
); );
} };
export default StableDownloads; export default StableDownloads;

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
/all-versions /index.html 200
/about /index.html 200

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 892 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 323 KiB

View File

@ -1,146 +0,0 @@
import {
Button,
createDisclosure,
Heading,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Text,
VStack,
} from "@hope-ui/solid";
import {
Component,
createEffect,
createResource,
createSignal,
} from "solid-js";
import Navbar from "./components/Navbar";
import { Route, Router, Routes } from "solid-app-router";
import { Root } from "./pages/Root";
import OtherDownloads from "./pages/OtherDownloads";
import NightlyDownloads from "./pages/NightlyDownloads";
import StableDownloads from "./pages/StableDownloads";
const App: Component = () => {
const [adBlockEnabled, setAdBlockEnabled] = createSignal(false);
const { isOpen, onOpen, onClose } = createDisclosure();
const [joke, setJoke] = createSignal<Record<string, any>>({});
createEffect(() => {
(async () => {
const googleAdUrl =
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
try {
await fetch(new Request(googleAdUrl)).catch((_) =>
setAdBlockEnabled(true)
);
} catch (e) {
setAdBlockEnabled(true);
} finally {
if (adBlockEnabled()) {
setJoke(
await (
await fetch(
"https://v2.jokeapi.dev/joke/Any?blacklistFlags=racist,sexist"
)
).json()
);
onOpen();
}
}
})();
}, []);
return (
<Router>
<Modal opened={isOpen()} onClose={onClose}>
<ModalOverlay />
<ModalContent mt="$5" mx="$3">
<ModalHeader>Support the Creator💚</ModalHeader>
<ModalBody>
<p>
Open source developers work really hard to provide the best,
secure & efficient software experience for you & people all around
the world. Most of the time we work without any wages at all but
we need minimum support to live & these <b> Ads Helps Us</b> earn
the minimum wage that we need to live.{" "}
<Text color="$success10" fontWeight="bold" textAlign="justify">
So, please support Spotube by disabling the AdBlocker on this
page or by sponsoring or donating to our collectives directly
</Text>
</p>
</ModalBody>
<ModalFooter>
<Button onClick={() => window.location.reload()}>
Reload without AdBlocker
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{!adBlockEnabled() ? (
<VStack alignItems="stretch">
<Navbar />
<Routes>
<Route path="/" component={Root} />
<Route path="/other-downloads" component={OtherDownloads} />
<Route path="/stable-downloads" component={StableDownloads} />
<Route path="/nightly-downloads" component={NightlyDownloads} />
</Routes>
</VStack>
) : (
<Stack
direction="column"
w="100vw"
h="100vh"
justifyContent="space-between"
alignItems="center"
p="$5"
>
<Heading></Heading>
<VStack spacing="$2" alignItems="flex-start">
<Heading>Here's something interesting:</Heading>
<Heading size="xl">
{joke().joke ?? (
<>
<p>{joke().setup}</p>
<p>{joke().delivery}</p>
</>
)}
</Heading>
</VStack>
<VStack justifySelf="flex-end">
<Heading
mt="$10"
size={{
"@lg": "4xl",
"@initial": "2xl",
}}
maxW="700px"
textAlign="justify"
lineHeight="1.5"
>
Be grateful for all the favors you get. But don't let it become a
pile of debt. Try returning them as soon as you can. You'll feel
relieved
</Heading>
<Heading
size={{
"@lg": "2xl",
"@initial": "lg",
}}
alignSelf="flex-end"
>
- Kingkor Roy Tirtho
</Heading>
</VStack>
</Stack>
)}
</Router>
);
};
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,47 +0,0 @@
import {
Button,
ButtonGroup,
Heading,
HStack,
IconButton,
useColorMode,
} from "@hope-ui/solid";
import { NavLink } from "solid-app-router";
import { FaLightbulb } from "solid-icons/fa";
import { FiSun } from "solid-icons/fi";
const Navbar = () => {
const { colorMode, toggleColorMode } = useColorMode();
return (
<HStack py="$2" justifyContent="space-between" as="nav" w="$full">
<section>
<Heading p="$2" size="3xl" mr="$2" as={NavLink} href="/">
Spotube
</Heading>
<ButtonGroup>
<Button
variant="ghost"
size="sm"
as={NavLink}
href="/other-downloads"
>
Other Downloads
</Button>
<Button variant="ghost" size="sm" as={NavLink} href="/about">
About
</Button>
</ButtonGroup>
</section>
<IconButton
variant="ghost"
icon={colorMode() == "dark" ? <FaLightbulb /> : <FiSun />}
aria-label="Dark Mode Toggle"
onClick={() => {
toggleColorMode();
}}
/>
</HStack>
);
};
export default Navbar;

View File

@ -1,40 +0,0 @@
import { Component, createEffect } from "solid-js";
type AdComponent = Component<{
slot: string;
}>;
export const DisplayAd: AdComponent = ({ slot }) => {
createEffect(() => {
//@ts-ignore
(window.adsbygoogle = window.adsbygoogle || []).push({});
}, []);
return (
<ins
class="adsbygoogle"
style={{ display: "block" }}
data-ad-client="ca-pub-6419300932495863"
data-ad-slot={slot}
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
);
};
export const GridMultiplexAd: AdComponent = ({ slot }) => {
createEffect(() => {
//@ts-ignore
(window.adsbygoogle = window.adsbygoogle || []).push({});
}, []);
return (
<ins
class="adsbygoogle"
style={{ display: "block" }}
data-ad-format="autorelaxed"
data-ad-client="ca-pub-6419300932495863"
data-ad-slot={slot}
></ins>
);
};

View File

@ -1,19 +0,0 @@
export enum Platform {
linux = "Linux",
windows = "Windows",
mac = "Mac",
android = "Android",
}
export function usePlatform(): Platform {
const platform = (
((navigator as unknown as any).userAgentData?.platform as
| string
| undefined) ?? navigator.platform
).toLowerCase();
if (platform.includes("windows")) return Platform.windows;
else if (platform.includes("mac")) return Platform.mac;
else if (platform.includes("android")) return Platform.android;
else return Platform.linux;
}

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,35 +0,0 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./index.css";
import App from "./App";
import { HopeProvider } from "@hope-ui/solid";
render(
() => (
<HopeProvider
config={{
lightTheme: {
colors: {
primary1: "#d4f3df",
primary2: "#d4f3df",
primary3: "#b7ecca",
primary4: "#9be4b4",
primary5: "#7edc9f",
primary6: "#61d48a",
primary7: "#45cd74",
primary8: "#32ba62",
primary9: "#2b9e53",
primary10: "#238144",
primary11: "#1b6435",
primary12: "#134826",
},
},
initialColorMode: "system",
}}
>
<App />
</HopeProvider>
),
document.getElementById("root") as HTMLElement
);

View File

@ -1,13 +0,0 @@
import { Anchor, Heading, Text, hope } from "@hope-ui/solid";
export const MarkdownComponentDefs = {
a: (props: any) => <Anchor {...props} color="$info10" />,
h1: (props: any) => <Heading {...props} size="4xl" mt="$5" mb="$1_5" />,
h2: (props: any) => <Heading {...props} size="3xl" mt="$5" mb="$1_5" />,
h3: (props: any) => <Heading {...props} size="2xl" mt="$5" mb="$1_5" />,
h4: (props: any) => <Heading {...props} size="md" />,
h5: (props: any) => <Heading {...props} size="lg" />,
h6: (props: any) => <Heading {...props} size="md" />,
p: (props: any) => <Text {...props} />,
li: (props: any) => <hope.li {...props} ml="$4" />,
};

View File

@ -1,75 +0,0 @@
import {
Anchor,
Button,
Heading,
Table,
Td,
Th,
Tr,
VStack,
hope,
Text,
} from "@hope-ui/solid";
import { NavLink } from "solid-app-router";
import { GridMultiplexAd } from "../components/special";
const baseURL =
"https://nightly.link/KRTirtho/spotube/workflows/spotube-nightly/build";
const DownloadLinks = Object.freeze({
Linux: baseURL + "Spotube-Linux-Bundle.zip",
Android: baseURL + "Spotube-Android-Bundle.zip",
Windows: baseURL + "Spotube-Windows-Bundle.zip",
MacOS: baseURL + "Spotube-Macos-Bundle.zip",
});
function NightlyDownloads() {
return (
<VStack
mx="$3"
alignSelf="center"
alignItems="flex-start"
spacing="$4"
maxW="500px"
overflow="auto"
>
<Heading size="2xl">Nightly Release</Heading>
<Text>Download latest & most bleeding edge version of Spotube</Text>
<Text size="sm" color="$danger11" textAlign="justify">
Disclaimer!: Nightly versions are untested and not the final version of
spotube. So it can consists of many hidden or unknown bugs but at the
same time will be opted with latest & greatest features too. So use it
at your own risk! If you don't know what you're doing than we recommend
you to download the{" "}
<Anchor as={NavLink} color="$info10" href="/">
latest stable version of
</Anchor>{" "}
Spotube
</Text>
<hope.section
border="2px solid"
borderColor="$neutral10"
borderRadius="$md"
>
<Table overflow="auto">
{Object.entries(DownloadLinks).map(([platform, url]) => {
const segments = url.split("/");
return (
<Tr>
<Th>{platform}</Th>
<Td>
<Button as={Anchor} href={url} variant="ghost">
{segments.at(segments.length - 1)?.replace("build", "")}
</Button>
</Td>
</Tr>
);
})}
</Table>
</hope.section>
<GridMultiplexAd slot="3192619797" />
</VStack>
);
}
export default NightlyDownloads;

View File

@ -1,27 +0,0 @@
import { Anchor, Heading, VStack } from "@hope-ui/solid";
import { NavLink } from "solid-app-router";
import { GridMultiplexAd } from "../components/special";
function OtherDownloads() {
return (
<VStack alignItems="flex-start" ml="$10" mt="$20">
<Heading size="2xl">Download other versions of Spotube</Heading>
<ul>
<li>
<Anchor color="$info10" as={NavLink} href="/stable-downloads">
Download previous versions of Spotube
</Anchor>
</li>
<li>
<Anchor color="$info10" as={NavLink} href="/nightly-downloads">
Download Bleeding Edge Nightly version of Spotube
</Anchor>{" "}
(Nightly releases)
</li>
</ul>
<GridMultiplexAd slot="4575915852" />
</VStack>
);
}
export default OtherDownloads;

View File

@ -1,101 +0,0 @@
import {
Heading,
Menu,
ButtonGroup,
Button,
Anchor,
MenuTrigger,
IconButton,
MenuContent,
MenuItem,
VStack,
hope,
} from "@hope-ui/solid";
import { FaSolidCaretDown } from "solid-icons/fa";
import { DisplayAd } from "../components/special";
import { Platform, usePlatform } from "../hooks/usePlatform";
const baseURL = "https://github.com/KRTirtho/spotube/releases/latest/download/";
const DownloadLinks = Object.freeze({
[Platform.linux]: [
{ name: "deb", url: baseURL + "Spotube-linux-x86_64.deb" },
{ name: "tar", url: baseURL + "Spotube-linux-x86_64.tar.xz" },
{ name: "AppImage", url: baseURL + "Spotube-linux-x86_64.AppImage" },
],
[Platform.android]: [
{
name: "apk",
url: baseURL + "Spotube-android-all-arch.apk",
},
],
[Platform.mac]: [{ name: "dmg", url: baseURL + "Spotube-macos-x86_64.dmg" }],
[Platform.windows]: [
{ name: "exe", url: baseURL + "Spotube-windows-x86_64-setup.exe" },
{ name: "nupkg", url: baseURL + "Spotube-windows-x86_64.nupkg " },
],
});
export const Root = () => {
const platform = usePlatform();
const allPlatforms = Object.entries(Platform)
.map(([key, value]) => {
return DownloadLinks[value].map((s) => ({
...s,
name: `${value} (.${s.name})`,
}));
})
.flat(1);
const currentPlatform = DownloadLinks[platform][0];
return (
<VStack spacing="$4" alignItems="stretch">
<hope.section
h="60vh"
backgroundColor="#f5f5f5"
style={{
"background-image": "url(/src/assets/spotube-screenshot-web.jpg)",
"background-repeat": "no-repeat",
"background-size": "contain",
"background-position": "right",
}}
>
<VStack mt="$10" mx="$6" spacing="$4" alignItems="flex-start">
<Heading color="#212121" size="5xl">
Spotube
</Heading>
<Heading color="#212121" size="2xl" textAlign="justify" maxW="500px">
A fast, modern, lightweight & efficient Spotify Music Client for
every platform
</Heading>
<Menu placement="bottom-end">
<ButtonGroup spacing="$0_5">
<Button
variant="subtle"
as={Anchor}
href={currentPlatform.url}
_hover={{ textDecoration: "none" }}
>
Download for {platform} (.{currentPlatform.name})
</Button>
<MenuTrigger as={IconButton} variant="subtle">
<FaSolidCaretDown />
</MenuTrigger>
</ButtonGroup>
<MenuContent>
{allPlatforms.map(({ name, url }) => {
return (
<MenuItem as={Anchor} href={url}>
{name}
</MenuItem>
);
})}
</MenuContent>
</Menu>
</VStack>
</hope.section>
<DisplayAd slot="9501208974" />
</VStack>
);
};

16
website/styles/globals.css Executable file
View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

28
website/tsconfig.json Normal file → Executable file
View File

@ -1,15 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true, "strict": true,
"target": "ESNext", "forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true, "noEmit": true,
"isolatedModules": true "declaration": true,
} "sourceMap": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
} }

View File

@ -1,17 +0,0 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
export default defineConfig({
plugins: [solidPlugin()],
server: {
port: 3000,
},
resolve: {
alias: {
"node-fetch": "isomorphic-fetch",
},
},
build: {
target: "esnext",
},
});