├── .env.example
├── .dockerignore
├── .prettierrc
├── public
├── favicon.ico
└── assets
│ ├── fonts
│ ├── WhitneyBold.woff
│ ├── WhitneyBook.woff
│ ├── WhitneyLight.woff
│ ├── WhitneyMedium.woff
│ ├── ABCGintoNordTrial.woff
│ ├── WhitneyBookItalic.woff
│ ├── WhitneySemibold.woff
│ ├── WhitneyLightItalic.woff
│ ├── WhitneyMediumItalic.woff
│ └── WhitneySemiboldItalic.woff
│ ├── images
│ ├── sample-summary-preview.png
│ ├── theme-change.svg
│ ├── missing-wumpus.svg
│ ├── clyde-triangle.svg
│ └── summaries-banner.svg
│ └── icons
│ ├── hamburger.svg
│ ├── arrow-single-left.svg
│ ├── arrow-single-right.svg
│ ├── privacypolicy.svg
│ ├── arrow-double-right.svg
│ ├── arrow-double-left.svg
│ ├── documentation.svg
│ ├── features.svg
│ ├── Discord-Logo-White.svg
│ ├── site-logo-white.svg
│ └── summaries.svg
├── .eslintrc.json
├── lib
├── api-models
│ ├── author.ts
│ ├── currentUser.ts
│ ├── partialEvent.ts
│ ├── event.ts
│ └── summary.ts
├── constants.ts
├── relativeDate.ts
└── api.ts
├── next-env.d.ts
├── renovate.json
├── pages
├── _middleware.tsx
├── documentation.tsx
├── _document.tsx
├── index.tsx
├── logout.tsx
├── 404.tsx
├── login.tsx
├── _app.tsx
├── summaries.tsx
└── events
│ └── [id].tsx
├── next.config.js
├── components
├── util
│ ├── Icon.tsx
│ ├── Logo.tsx
│ ├── ThemeToggle.tsx
│ ├── Metric.tsx
│ ├── Panel.tsx
│ ├── Spinner.tsx
│ ├── ToggleableArrow.tsx
│ ├── GracefulImage.tsx
│ ├── Table.tsx
│ └── form
│ │ └── Searchbar.tsx
├── page
│ ├── summary
│ │ ├── CardList.tsx
│ │ ├── Card.tsx
│ │ └── DatePicker.tsx
│ └── events
│ │ ├── SidebarEventCard.tsx
│ │ ├── EventSidebar.tsx
│ │ ├── EventHeader.tsx
│ │ └── Summary.tsx
├── layout
│ ├── Scrollbar.tsx
│ ├── account
│ │ ├── AccountHeader.tsx
│ │ ├── LoginButton.tsx
│ │ └── AccountButton.tsx
│ ├── footer
│ │ └── Footer.tsx
│ └── Sidebar.tsx
├── typography
│ ├── Header.tsx
│ ├── Link.tsx
│ ├── Text.tsx
│ └── Markdown.tsx
└── context
│ └── AuthContext.tsx
├── tsconfig.json
├── .gitignore
├── .github
└── workflows
│ ├── docker.yml
│ └── codeql-analysis.yml
├── package.json
├── stitches.config.ts
├── Dockerfile
├── README.md
├── styles
└── global.css
└── LICENSE
/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .next/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false
4 | }
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react/no-children-prop": 0
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyBold.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyBook.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyBook.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyLight.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyMedium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyMedium.woff
--------------------------------------------------------------------------------
/public/assets/fonts/ABCGintoNordTrial.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/ABCGintoNordTrial.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyBookItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyBookItalic.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneySemibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneySemibold.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyLightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyLightItalic.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneyMediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneyMediumItalic.woff
--------------------------------------------------------------------------------
/public/assets/fonts/WhitneySemiboldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/fonts/WhitneySemiboldItalic.woff
--------------------------------------------------------------------------------
/public/assets/images/sample-summary-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discord-docs/ddocs/HEAD/public/assets/images/sample-summary-preview.png
--------------------------------------------------------------------------------
/lib/api-models/author.ts:
--------------------------------------------------------------------------------
1 | export default interface Author {
2 | username: string;
3 | discriminator: string;
4 | avatar: string;
5 | id: string;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/api-models/currentUser.ts:
--------------------------------------------------------------------------------
1 | export default interface CurrentUser {
2 | id: string;
3 | username: string;
4 | discriminator: string;
5 | avatar: string;
6 | isAuthor: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/api-models/partialEvent.ts:
--------------------------------------------------------------------------------
1 | export default interface PartialEvent {
2 | id: string;
3 | title: string;
4 | description?: string;
5 | thumbnail?: string;
6 | heldAt: string;
7 | }
8 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/public/assets/icons/hamburger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/api-models/event.ts:
--------------------------------------------------------------------------------
1 | import Author from "./author";
2 | import PartialEvent from "./partialEvent";
3 | import Summary from "./summary";
4 |
5 | export default interface Event extends PartialEvent {
6 | author: Author;
7 | contributors: Author[];
8 | summaries: Summary[];
9 | lastRevised: string;
10 | }
11 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "lockFileMaintenance": { "enabled": true },
4 | "prHourlyLimit": 2,
5 | "labels": ["dependencies"],
6 | "packageRules": [
7 | {
8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
9 | "automerge": true
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/pages/_middleware.tsx:
--------------------------------------------------------------------------------
1 | import { NextResponse, NextRequest } from "next/server";
2 |
3 | export async function middleware(req: NextRequest, ev: NextResponse) {
4 | const { pathname } = req.nextUrl;
5 | if (pathname === "/") {
6 | return NextResponse.redirect(new URL("/summaries", req.url));
7 | }
8 | return NextResponse.next();
9 | }
10 |
--------------------------------------------------------------------------------
/pages/documentation.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from "next";
2 |
3 | const Documentation: NextPage = () => {
4 | return (
5 | <>
6 |
Placeholder
7 | >
8 | );
9 | };
10 |
11 | export function getStaticProps() {
12 | return {
13 | props: {
14 | title: "Documentation",
15 | },
16 | };
17 | }
18 |
19 | export default Documentation;
20 |
--------------------------------------------------------------------------------
/public/assets/icons/arrow-single-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/assets/icons/arrow-single-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/api-models/summary.ts:
--------------------------------------------------------------------------------
1 | export default interface Summary {
2 | id: string;
3 | title: string;
4 | content?: string;
5 | type: "feature" | "qnaanswer";
6 | isNew: boolean;
7 | thumbnail?: string;
8 | featureType?:
9 | | "plannedq1"
10 | | "plannedq2"
11 | | "plannedq3"
12 | | "plannedq4"
13 | | "unknown"
14 | | "closedbeta"
15 | | "workinprogress"
16 | | "released";
17 | }
18 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | webpack(config) {
6 | config.module.rules.push({
7 | test: /\.svg$/,
8 | use: ["@svgr/webpack"],
9 | });
10 |
11 | return config;
12 | },
13 | experimental: {
14 | outputStandalone: true,
15 | },
16 | images: {
17 | domains: ["s3-alpha-sig.figma.com", "localhost"],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/public/assets/images/theme-change.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/assets/icons/privacypolicy.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/util/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FC, useEffect, useState } from "react";
3 |
4 | interface IconProps extends React.SVGAttributes {
5 | icon: string;
6 | }
7 |
8 | const Icon: FC = ({ icon, ...props }) => {
9 | const [component, setComponent] = useState(null);
10 |
11 | useEffect(() => {
12 | import(`../../public/assets/icons/${icon}.svg`).then((module) => {
13 | setComponent((module.default as any)(props));
14 | });
15 | }, [icon]);
16 |
17 | return component;
18 | };
19 |
20 | export default Icon;
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
39 | .idea
40 | .env
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import NextDocument, { Html, Head, Main, NextScript } from "next/document";
3 | import { getCssText } from "../stitches.config";
4 |
5 | export default class Document extends NextDocument {
6 | render() {
7 | return (
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/public/assets/icons/arrow-double-right.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/icons/arrow-double-left.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import type { SidebarItem } from "../components/layout/Sidebar";
2 |
3 | export const DEFAULT_SIDEBAR_ITEMS: SidebarItem[] = [
4 | {
5 | label: "Summaries",
6 | href: "/summaries",
7 | icon: "summaries",
8 | },
9 | {
10 | label: "Features",
11 | href: "/features",
12 | icon: "features",
13 | },
14 | {
15 | label: "API Documentation",
16 | href: "/documentation",
17 | icon: "documentation",
18 | },
19 | {
20 | label: "Privacy Policy",
21 | href: "/privacy",
22 | icon: "privacypolicy",
23 | },
24 | ];
25 |
26 | export enum BuildDetailsTab {
27 | DETAILS,
28 | EXPERIMENTS,
29 | SCRIPTS,
30 | }
31 |
--------------------------------------------------------------------------------
/components/util/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import { styled } from "../../stitches.config";
3 | import Icon from "./Icon";
4 |
5 | interface LogoProps extends React.SVGAttributes {}
6 |
7 | const SiteLogo = styled(Icon, {
8 | ".site-logo-white_svg__logo-rect": {
9 | fill: "none !important",
10 | },
11 |
12 | ".site-logo-white_svg__logo-body": {
13 | fill: "$textNormal",
14 | },
15 |
16 | ".site-logo-white_svg__logo-lines": {
17 | fill: "$backgroundPrimary",
18 | },
19 | });
20 |
21 | const Logo: FunctionComponent = ({ ...props }) => {
22 | return ;
23 | };
24 |
25 | export default Logo;
26 |
--------------------------------------------------------------------------------
/components/page/summary/CardList.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "../../../stitches.config";
2 |
3 | const CardList = styled("div", {
4 | position: "relative",
5 | display: "grid",
6 | justifyContent: "center",
7 | gridTemplateColumns: "repeat(auto-fit, minmax(250px, max-content))",
8 | gridTemplateRows: "290px",
9 | gap: 40,
10 | width: "100%",
11 |
12 | "@mobile": {
13 | gridTemplateColumns: "repeat(auto-fit, 25%)",
14 | gap: 0,
15 | gridTemplateRows: "266px",
16 | },
17 |
18 | "@media (max-width: 700px)": {
19 | gridTemplateColumns: "repeat(auto-fit, 33%)",
20 | },
21 |
22 | "@media (max-width: 520px)": {
23 | gridTemplateColumns: "repeat(auto-fit, 50%)",
24 | },
25 | });
26 |
27 | export default CardList;
28 |
--------------------------------------------------------------------------------
/components/util/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { FunctionComponent } from "react";
3 | import { styled, CSS } from "../../stitches.config";
4 | import Icon from "../../public/assets/images/theme-change.svg";
5 |
6 | interface ThemeToggleProps {
7 | css?: CSS;
8 | }
9 |
10 | const ThemeToggleButton = styled("div", {
11 | cursor: "pointer",
12 | });
13 |
14 | const ThemeToggle: FunctionComponent = ({ css }) => {
15 | const { theme, setTheme } = useTheme();
16 |
17 | return (
18 | (theme === "dark" ? setTheme("light") : setTheme("dark"))}
21 | >
22 |
23 |
24 | );
25 | };
26 |
27 | export default ThemeToggle;
28 |
--------------------------------------------------------------------------------
/components/layout/Scrollbar.tsx:
--------------------------------------------------------------------------------
1 | import { css } from "../../stitches.config";
2 |
3 | export default function Scrollbar(
4 | marginTop?: string,
5 | marginBottom?: string,
6 | mobileMarginTop?: string,
7 | mobileMarginBottom?: string
8 | ) {
9 | return css({
10 | "&::-webkit-scrollbar": {
11 | width: "0.5rem",
12 | },
13 |
14 | "&::-webkit-scrollbar-track": {
15 | background: "$scrollbarTrack",
16 | borderRadius: "0.25rem",
17 | marginTop,
18 | marginBottom,
19 |
20 | "@mobile": {
21 | marginTop: mobileMarginTop,
22 | marginBottom: mobileMarginBottom,
23 | },
24 | },
25 |
26 | "&::-webkit-scrollbar-thumb": {
27 | background: "$scrollbarThumb",
28 | borderRadius: "0.25rem",
29 | },
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { FC } from "react";
3 |
4 | interface OverviewProps {
5 | description: string;
6 | }
7 |
8 | const Overview: FC = ({ description }) => {
9 | return (
10 | <>
11 | Placeholder
12 | {description}
13 | >
14 | );
15 | };
16 |
17 | interface APIStatus {
18 | date_generated: string;
19 | statistics: {
20 | builds: number;
21 | experiments: number;
22 | };
23 | }
24 |
25 | export async function getServerSideProps() {
26 | // TODO: Template stuff, change this
27 | const response = await axios.get("https://api.discord.sale/");
28 |
29 | return {
30 | props: {
31 | description: `Discord Docs`,
32 | },
33 | };
34 | }
35 |
36 | export default Overview;
37 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | image:
9 | name: Build Docker image
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: read
13 | packages: write
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v3
18 |
19 | - name: Log into registry ${{ env.REGISTRY }}
20 | uses: docker/login-action@v1
21 | with:
22 | registry: ghcr.io
23 | username: ${{ github.actor }}
24 | password: ${{ secrets.GITHUB_TOKEN }}
25 |
26 | - name: Build and push Docker image
27 | uses: docker/build-push-action@v2
28 | with:
29 | context: .
30 | push: ${{ github.event_name != 'pull_request' }}
31 | tags: |
32 | ghcr.io/discord-docs/frontend:${{ github.sha }}
33 | ghcr.io/discord-docs/frontend:latest
34 | labels: ${{ steps.meta.outputs.labels }}
35 |
--------------------------------------------------------------------------------
/components/util/Metric.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { css, styled } from "../../stitches.config";
3 | import Panel from "./Panel";
4 | import Text from "../typography/Text";
5 |
6 | interface MetricProps {
7 | title: string;
8 | formattedValue?: any;
9 | isLoading?: boolean;
10 | pctChange: number;
11 | className?: string;
12 | }
13 |
14 | const Header = styled("header", {
15 | color: "#b9bbbe",
16 | fontSize: "12px",
17 | fontWeight: "700",
18 | lineHeight: "16px",
19 | textTransform: "uppercase",
20 | display: "flex",
21 | alignItems: "center",
22 | });
23 |
24 | const value = css({
25 | margin: "8px 0",
26 | });
27 |
28 | const Metric: FC = ({ title, formattedValue, className }) => {
29 | return (
30 |
31 |
32 |
33 | {formattedValue}
34 |
35 |
36 | );
37 | };
38 |
39 | export default Metric;
40 |
--------------------------------------------------------------------------------
/public/assets/icons/documentation.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/util/Panel.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "../../stitches.config";
2 |
3 | const Panel = styled("div", {
4 | padding: 16,
5 | height: "100%",
6 | boxSizing: "border-box",
7 | borderColor: "$border",
8 | borderRadius: 5,
9 | overflow: "hidden",
10 | borderStyle: "solid",
11 | transition: "border .125s",
12 | backgroundColor: "$backgroundSecondaryAlt",
13 | variants: {
14 | border: {
15 | none: {
16 | borderStyle: "none",
17 | borderWidth: 0,
18 | },
19 | primary: {
20 | borderColor: "#040405",
21 | },
22 | red: {
23 | borderColor: "#ed4245",
24 | },
25 | },
26 | padding: {
27 | nopad: {
28 | padding: 0,
29 | },
30 | small: {
31 | padding: 12,
32 | },
33 | medium: {
34 | padding: 20,
35 | },
36 | },
37 | },
38 | defaultVariants: {
39 | border: "none",
40 | padding: "small",
41 | },
42 | });
43 |
44 | Panel.displayName = "Panel";
45 |
46 | export default Panel;
47 |
--------------------------------------------------------------------------------
/pages/logout.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from "next";
2 | import { useEffect, useState } from "react";
3 | import { useAuth } from "../components/context/AuthContext";
4 |
5 | const Logout: NextPage = () => {
6 | const [logoutSuccessful, setLogoutSuccessful] = useState(
7 | undefined
8 | );
9 | const auth = useAuth();
10 |
11 | const logout = async () => {
12 | auth.clearUser();
13 | setLogoutSuccessful(true);
14 | };
15 |
16 | useEffect(() => {
17 | auth.loginCallback((isAuthed, account) => {
18 | console.log("callback", isAuthed);
19 | if (!isAuthed) {
20 | setLogoutSuccessful(false);
21 | } else {
22 | logout();
23 | }
24 | });
25 | }, []);
26 |
27 | return (
28 | <>
29 | {logoutSuccessful === undefined ? (
30 | Logging out...
31 | ) : logoutSuccessful ? (
32 | Logout successful!
33 | ) : (
34 | Logout failed!
35 | )}
36 | >
37 | );
38 | };
39 |
40 | export default Logout;
41 |
--------------------------------------------------------------------------------
/components/page/events/SidebarEventCard.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import PartialEvent from "../../../lib/api-models/partialEvent";
3 | import { styled } from "../../../stitches.config";
4 | import Panel from "../../util/Panel";
5 | import Text from "../../typography/Text";
6 |
7 | interface SidebarEventCardProps {
8 | event: PartialEvent;
9 | }
10 |
11 | const Header = styled("header", {
12 | fontWeight: "700",
13 | fontSize: 20,
14 | marginBottom: "0.25rem",
15 | });
16 |
17 | const Image = styled("img", {
18 | width: "100%",
19 | });
20 |
21 | const Padding = styled("div", {
22 | padding: 12,
23 | });
24 |
25 | const SidebarEventCard: FunctionComponent = ({
26 | event,
27 | }) => {
28 | return (
29 |
30 |
31 |
32 |
33 | {event.description}
34 |
35 |
36 | );
37 | };
38 |
39 | export default SidebarEventCard;
40 |
--------------------------------------------------------------------------------
/public/assets/icons/features.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@discord-docs/ddocs",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "build:ci": "CI=true npm run build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@stitches/react": "1.2.7",
13 | "@types/jsonwebtoken": "8.5.8",
14 | "@types/react-syntax-highlighter": "13.5.2",
15 | "axios": "0.26.1",
16 | "jsonwebtoken": "8.5.1",
17 | "luxon": "2.5.2",
18 | "next": "12.1.2",
19 | "next-themes": "0.0.15",
20 | "nextjs-progressbar": "0.0.14",
21 | "react": "17.0.2",
22 | "react-dom": "17.0.2",
23 | "react-draggable": "4.4.4",
24 | "react-markdown": "8.0.1",
25 | "react-progressive-graceful-image": "0.6.14",
26 | "react-syntax-highlighter": "15.5.0",
27 | "react-tooltip": "4.2.21",
28 | "remark-gfm": "3.0.1",
29 | "sharp": "0.30.3"
30 | },
31 | "devDependencies": {
32 | "@svgr/webpack": "6.2.1",
33 | "@types/luxon": "2.3.1",
34 | "@types/node": "17.0.45",
35 | "@types/react": "17.0.43",
36 | "eslint": "8.11.0",
37 | "eslint-config-next": "12.1.2",
38 | "prettier": "2.6.1",
39 | "typescript": "4.6.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/components/layout/account/AccountHeader.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { CSSProperties, FunctionComponent, useEffect, useState } from "react";
3 | import { styled } from "../../../stitches.config";
4 | import AccountButton from "./AccountButton";
5 | import { useAuth } from "../../context/AuthContext";
6 | import LoginButton from "./LoginButton";
7 |
8 | interface AccountHeaderProps {}
9 |
10 | const Container = styled("div", {
11 | zIndex: 10,
12 | "@mobile": {
13 | position: "fixed",
14 | top: 0,
15 | right: 0,
16 | },
17 | });
18 |
19 | const AccountHeader: FunctionComponent = () => {
20 | const [initialized, setInitialized] = useState(false);
21 |
22 | const auth = useAuth();
23 |
24 | useEffect(() => {
25 | auth.loginCallback(() => {
26 | setInitialized(true);
27 | });
28 | }, []);
29 |
30 | return (
31 |
32 | {initialized ? (
33 | <>
34 | {auth.isAuthenticated ? (
35 |
36 | ) : (
37 |
38 | )}
39 | >
40 | ) : undefined}
41 |
42 | );
43 | };
44 |
45 | export default AccountHeader;
46 |
--------------------------------------------------------------------------------
/components/util/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { keyframes } from "@stitches/react";
2 | import { FunctionComponent } from "react";
3 | import { styled } from "../../stitches.config";
4 |
5 | interface SpinnerProps {}
6 |
7 | const rotate = keyframes({
8 | "100%": {
9 | transform: "rotate(360deg)",
10 | },
11 | });
12 |
13 | const dash = keyframes({
14 | "0%": {
15 | strokeDasharray: "1, 150",
16 | strokeDashoffset: 0,
17 | },
18 | "50%": {
19 | strokeDasharray: "90, 150",
20 | strokeDashoffset: -35,
21 | },
22 | "100%": {
23 | strokeDasharray: "90, 150",
24 | strokeDashoffset: -124,
25 | },
26 | });
27 |
28 | const SVG = styled("svg", {
29 | animation: `${rotate} 2s linear infinite`,
30 | zIndex: "2",
31 | width: "20px",
32 | height: "20px",
33 | position: "absolute",
34 | top: "6px",
35 | right: "0.5rem",
36 | });
37 |
38 | const Circle = styled("circle", {
39 | animation: `${dash} 1.5s ease-in-out infinite`,
40 | stroke: "#484848",
41 | strokeLinecap: "round",
42 | });
43 |
44 | const Spinner: FunctionComponent = () => {
45 | return (
46 |
49 | );
50 | };
51 |
52 | export default Spinner;
53 |
--------------------------------------------------------------------------------
/components/util/ToggleableArrow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import { styled } from "../../stitches.config";
3 |
4 | interface ToggleableArrorProps {
5 | expanded: boolean;
6 | style?: React.CSSProperties;
7 | }
8 |
9 | const ArrowIcon = styled("svg", {
10 | width: "24px",
11 | height: "24px",
12 | fill: "$textNormal",
13 | transition: "all 0.15s ease-in-out",
14 | marginLeft: "auto",
15 | });
16 |
17 | const ToggleableArror: FunctionComponent = ({
18 | expanded,
19 | style,
20 | }) => {
21 | return (
22 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default ToggleableArror;
35 |
--------------------------------------------------------------------------------
/stitches.config.ts:
--------------------------------------------------------------------------------
1 | import { createStitches, CSS as CSSProperties } from "@stitches/react";
2 |
3 | const stitches = createStitches({
4 | theme: {
5 | colors: {
6 | backgroundPrimary: "#36393f",
7 | backgroundSecondary: "#2f3136",
8 | headerPrimary: "#fff",
9 | textNormal: "#dcddde",
10 | brand: "#5865f2",
11 | backgroundSecondaryAlt: "rgb(24, 25, 28)",
12 | backgroundTeritialy: "#222",
13 | backgroundAccent: "#444",
14 | scrollbarTrack: "#2f3136",
15 | scrollbarThumb: "#1f2022",
16 | itemUnactive: "#b9bbbe",
17 | },
18 | },
19 |
20 | media: {
21 | mobile: "(max-width: 892px)",
22 | },
23 |
24 | prefix: "ddocs",
25 | });
26 |
27 | export const { getCssText, styled, globalCss, css, createTheme, config } =
28 | stitches;
29 |
30 | export const lightTheme = createTheme("light-theme", {
31 | colors: {
32 | backgroundPrimary: "white",
33 | backgroundSecondary: "#f2f3f5",
34 | headerPrimary: "#060607",
35 | textNormal: "#2e3338",
36 | brand: "#5865f2",
37 | backgroundSecondaryAlt: "#ebedef",
38 | backgroundTeritialy: "#e3e5e8",
39 | backgroundAccent: "#747f8d",
40 | scrollbarTrack: "#f2f3f5",
41 | scrollbarThumb: "#747f8d",
42 | itemUnactive: "#2e3338",
43 | },
44 | });
45 |
46 | export type CSS = CSSProperties;
47 |
--------------------------------------------------------------------------------
/lib/relativeDate.ts:
--------------------------------------------------------------------------------
1 | const SECOND = 1000,
2 | MINUTE = 60 * SECOND,
3 | HOUR = 60 * MINUTE,
4 | DAY = 24 * HOUR,
5 | WEEK = 7 * DAY,
6 | YEAR = DAY * 365,
7 | MONTH = YEAR / 12;
8 |
9 | const formats = [
10 | [0.7 * MINUTE, "just now"],
11 | [1.5 * MINUTE, "a minute ago"],
12 | [60 * MINUTE, "minutes ago", MINUTE],
13 | [1.5 * HOUR, "an hour ago"],
14 | [DAY, "hours ago", HOUR],
15 | [2 * DAY, "yesterday"],
16 | [7 * DAY, "days ago", DAY],
17 | [1.5 * WEEK, "a week ago"],
18 | [MONTH, "weeks ago", WEEK],
19 | [1.5 * MONTH, "a month ago"],
20 | [YEAR, "months ago", MONTH],
21 | [1.5 * YEAR, "a year ago"],
22 | [Number.MAX_VALUE, "years ago", YEAR],
23 | ];
24 |
25 | export default function relativeDate(
26 | input: Date | number,
27 | reference?: Date | number
28 | ) {
29 | !reference && (reference = new Date().getTime());
30 | reference instanceof Date && (reference = reference.getTime());
31 | input instanceof Date && (input = input.getTime());
32 |
33 | let delta = reference - input,
34 | format,
35 | i,
36 | len;
37 |
38 | for (i = -1, len = formats.length; ++i < len; ) {
39 | format = formats[i];
40 | if (delta < format[0]) {
41 | return format[2] == undefined
42 | ? format[1]
43 | : Math.round(delta / (format[2] as number)) + " " + format[1];
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/components/page/summary/Card.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { css, styled } from "../../../stitches.config";
3 | import Panel from "../../util/Panel";
4 | import Image from "next/image";
5 | import Text from "../../typography/Text";
6 |
7 | interface MetricProps {
8 | title: string;
9 | image?: string;
10 | isLoading?: boolean;
11 | className?: string;
12 | }
13 |
14 | const Header = styled("header", {
15 | fontWeight: "700",
16 | fontSize: 20,
17 |
18 | "@mobile": {
19 | fontSize: 17,
20 | marginBottom: "0.2rem",
21 | },
22 | });
23 |
24 | const Padding = styled("div", {
25 | padding: 12,
26 | });
27 |
28 | const test = css({
29 | lineHeight: "18px",
30 | display: "-webkit-box",
31 | "-webkit-box-orient": "vertical",
32 | overflow: "hidden",
33 |
34 | "@mobile": {
35 | fontSize: "15px",
36 | },
37 | });
38 |
39 | const Card: FC = ({ title, className, children, image }) => {
40 | let lines = image ? 5 : 14; // no image
41 | const lineClamp = css({ "-webkit-line-clamp": lines });
42 | const textStyle = [test(), lineClamp()].join(" ");
43 | return (
44 |
45 | {image && }
46 |
47 |
48 | {children}
49 |
50 |
51 | );
52 | };
53 |
54 | export default Card;
55 |
--------------------------------------------------------------------------------
/components/typography/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FunctionComponent } from "react";
3 | import { css } from "../../stitches.config";
4 |
5 | interface HeaderProps {
6 | variant: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
7 | className?: string;
8 | }
9 |
10 | const styles = {
11 | h1: css({
12 | fontWeight: "bold",
13 | fontSize: "32px",
14 |
15 | "@mobile": {
16 | fontSize: "28px",
17 | },
18 | }),
19 | h2: css({
20 | fontWeight: "bold",
21 | fontSize: "24px",
22 |
23 | "@mobile": {
24 | fontSize: "20px",
25 | },
26 | }),
27 | h3: css({
28 | fontWeight: "normal",
29 | fontSize: "24px",
30 |
31 | "@mobile": {
32 | fontSize: "20px",
33 | },
34 | }),
35 | h4: css({
36 | fontWeight: "bold",
37 | fontSize: "20px",
38 |
39 | "@mobile": {
40 | fontSize: "16px",
41 | },
42 | }),
43 | h5: css({
44 | fontWeight: "normal",
45 | fontSize: "20px",
46 |
47 | "@mobile": {
48 | fontSize: "16px",
49 | },
50 | }),
51 | h6: css({
52 | fontWeight: "bold",
53 | fontSize: "18px",
54 |
55 | "@mobile": {
56 | fontSize: "16px",
57 | },
58 | }),
59 | };
60 |
61 | const Header: FunctionComponent = ({
62 | variant,
63 | children,
64 | className,
65 | }) => {
66 | return React.createElement(
67 | variant,
68 | { className: `${styles[variant]()} ${className ?? ""}` },
69 | children
70 | );
71 | };
72 |
73 | export default Header;
74 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Install dependencies only when needed
2 | FROM node:17-alpine AS deps
3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
4 | RUN apk add --no-cache libc6-compat
5 | RUN npm install -g pnpm
6 | WORKDIR /app
7 | COPY package.json pnpm-lock.yaml ./
8 | RUN pnpm install
9 |
10 | # Rebuild the source code only when needed
11 | FROM node:17-alpine AS builder
12 | WORKDIR /app
13 | COPY --from=deps /app/node_modules ./node_modules
14 | COPY . .
15 | RUN npm run build:ci
16 |
17 | # Production image, copy all the files and run next
18 | FROM node:17-alpine AS runner
19 | WORKDIR /app
20 |
21 | ENV NODE_ENV production
22 |
23 | RUN addgroup -g 1001 -S nodejs
24 | RUN adduser -S nextjs -u 1001
25 |
26 | # You only need to copy next.config.js if you are NOT using the default configuration
27 | COPY --from=builder /app/next.config.js ./
28 | COPY --from=builder /app/public ./public
29 | COPY --from=builder /app/package.json ./package.json
30 |
31 | # Automatically leverage output traces to reduce image size
32 | # https://nextjs.org/docs/advanced-features/output-file-tracing
33 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
34 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
35 |
36 | USER nextjs
37 |
38 | ENV PORT 3000
39 |
40 | # Next.js collects completely anonymous telemetry data about general usage.
41 | # Learn more here: https://nextjs.org/telemetry
42 | # Uncomment the following line in case you want to disable telemetry.
43 | ENV NEXT_TELEMETRY_DISABLED 1
44 |
45 | CMD ["node", "server.js"]
46 |
--------------------------------------------------------------------------------
/components/util/GracefulImage.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, keyframes } from "@stitches/react";
2 | import { FunctionComponent } from "react";
3 | import ProgressiveImage from "react-progressive-graceful-image";
4 | import API, { Routes } from "../../lib/api";
5 | import { styled } from "../../stitches.config";
6 |
7 | interface GracefulImageProps {
8 | hasThumbnail?: boolean;
9 | id: string;
10 | width?: number;
11 | height?: number;
12 | className?: string;
13 | alt?: string;
14 | style?: CSSProperties;
15 | onClick?: () => void;
16 | }
17 | const GracefulImage: FunctionComponent = ({
18 | id,
19 | hasThumbnail,
20 | width,
21 | height,
22 | className,
23 | alt,
24 | style,
25 | onClick,
26 | }) => {
27 | const imageUrl = API.getRoute(Routes.Assets + `/${id}`);
28 | return (
29 | <>
30 | {hasThumbnail && (
31 |
32 | {(src: string, loading: boolean) => (
33 |
46 | )}
47 |
48 | )}
49 | {!hasThumbnail && (
50 |
59 | )}
60 | >
61 | );
62 | };
63 |
64 | export default GracefulImage;
65 |
--------------------------------------------------------------------------------
/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from "next";
2 | import Link from "next/link";
3 | import { styled } from "../stitches.config";
4 | import MissingWumpus from "../public/assets/images/missing-wumpus.svg";
5 |
6 | const PageWrapper = styled("div", {
7 | display: "relative",
8 | });
9 |
10 | const HeadingContainer = styled("div", {
11 | marginLeft: 80,
12 | marginTop: 40,
13 | position: "relative",
14 | zIndex: 1,
15 |
16 | "@mobile": {
17 | marginLeft: 0,
18 | textAlign: "center",
19 | height: "90vh",
20 | },
21 | });
22 |
23 | const MainHeading = styled("h1", {
24 | fontSize: 180,
25 | fontWeight: "bold",
26 | margin: 0,
27 | });
28 |
29 | const NotFoundHeading = styled("h2", {
30 | fontSize: 48,
31 | fontWeight: "bold",
32 | margin: 0,
33 | });
34 |
35 | const LinkHome = styled("a", {
36 | fontSize: 18,
37 | display: "inline-block",
38 | marginTop: 26,
39 | color: "$brand",
40 | textDecoration: "none",
41 | });
42 |
43 | // TODO: handle scaling on smaller screens
44 | const WumpusWrapper = styled("div", {
45 | position: "absolute",
46 | bottom: 0,
47 | right: 40,
48 |
49 | "@mobile": {
50 | bottom: "-66px",
51 | right: 0,
52 | },
53 | });
54 |
55 | const Wumpus = styled(MissingWumpus, {
56 | width: "350px",
57 | });
58 |
59 | const NotFound: NextPage = () => {
60 | return (
61 |
62 |
63 | 404
64 | Not found.
65 |
66 | Return to home.
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export function getStaticProps() {
77 | return {
78 | props: {
79 | title: "Page Not Found",
80 | },
81 | };
82 | }
83 |
84 | export default NotFound;
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DDocs Frontend
2 |
3 | [](https://discord.gg/HzJfeuUM5h)
4 |
5 | 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).
6 |
7 | ## Getting Started
8 |
9 | - `cp .env.example .env`
10 | - Fill out any environment variables in `.env`
11 | - Run the development server:
12 |
13 | ```bash
14 | npm run dev
15 | # or
16 | yarn dev
17 | ```
18 |
19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
20 |
21 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
22 |
23 | [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`.
24 |
25 | 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.
26 |
27 | ## Learn More
28 |
29 | To learn more about Next.js, take a look at the following resources:
30 |
31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
33 |
34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
35 |
36 | ## Deploy on Vercel
37 |
38 | 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.
39 |
40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
41 |
--------------------------------------------------------------------------------
/pages/login.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { GetServerSideProps, NextPage } from "next/types";
3 | import { FunctionComponent, useEffect, useState } from "react";
4 | import { useAuth } from "../components/context/AuthContext";
5 | import jwt from "jsonwebtoken";
6 |
7 | interface LoginProps {
8 | code?: string;
9 | }
10 |
11 | const Login: FunctionComponent = ({ code }) => {
12 | const [hasFailed, setHasFailed] = useState(false);
13 | const [cat, setCat] = useState("");
14 |
15 | const auth = useAuth();
16 | const router = useRouter();
17 |
18 | const login = async () => {
19 | const token = await auth.Api?.login(code!);
20 |
21 | if (!token) {
22 | const result = await fetch("https://aws.random.cat/meow");
23 |
24 | setCat((await result.json()).file);
25 |
26 | setHasFailed(true);
27 |
28 | return;
29 | }
30 |
31 | const claims = jwt.decode(token) as {
32 | uid: string;
33 | };
34 |
35 | auth.setUser(claims.uid, token);
36 |
37 | router.push("/");
38 | };
39 |
40 | useEffect(() => {
41 | if (!code) {
42 | router.replace("/");
43 | return;
44 | }
45 |
46 | login();
47 | }, []);
48 |
49 | return (
50 | <>
51 | {hasFailed ? (
52 |
62 |
Shit broeke lmao
63 |

70 |
71 | ) : undefined}
72 | >
73 | );
74 | };
75 |
76 | export const getServerSideProps: GetServerSideProps = async (
77 | context
78 | ) => {
79 | return {
80 | props: {
81 | code: context.query.code as string,
82 | },
83 | };
84 | };
85 |
86 | export default Login;
87 |
--------------------------------------------------------------------------------
/public/assets/icons/Discord-Logo-White.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icons/site-logo-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/util/Table.tsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { styled } from "../../stitches.config";
3 |
4 | const StyledTable = styled("table", {
5 | borderCollapse: "collapse",
6 | width: "100%",
7 | borderBottomLeftRadius: "6px",
8 | borderBottomRightRadius: "6px",
9 | fontSize: 14,
10 | lineHeight: "22px",
11 | marginTop: "0",
12 | fontWeight: 300,
13 | overflow: "hidden",
14 | wordBreak: "break-word",
15 | });
16 |
17 | StyledTable.displayName = "Table";
18 |
19 | const StyledTableHead = styled("thead", { backgroundColor: "#35383c" });
20 |
21 | StyledTableHead.displayName = "TableHead";
22 |
23 | const StyledTableRow = styled("tr", {
24 | borderBottom: "1px solid transparent",
25 | color: "$textNormal",
26 | });
27 |
28 | StyledTableRow.displayName = "TableRow";
29 |
30 | const StyledTableHeaderCell = styled("th", {
31 | borderBottom: "1px solid #040405",
32 | padding: 8,
33 | fontSize: 12,
34 | lineHeight: "15px",
35 | textTransform: "uppercase",
36 | color: "hsla(0,0%,100%,.8)",
37 | textAlign: "left",
38 | fontWeight: 600,
39 | backgroundColor: "#202225",
40 | });
41 |
42 | StyledTableHeaderCell.displayName = "TableHeaderCell";
43 |
44 | const StyledTableBody = styled("tbody", {
45 | "& tr": {
46 | background: "#18191c",
47 | ":hover": {
48 | background: "#1f2124",
49 | },
50 | },
51 | "& tr:nth-child(even)": {
52 | background: "#121315",
53 | ":hover": {
54 | background: "#1f2124",
55 | },
56 | },
57 | });
58 |
59 | StyledTableBody.displayName = "TableBody";
60 |
61 | const StyledTableDataCell = styled("td", {
62 | verticalAlign: "top",
63 | padding: 8,
64 | fontWeight: 300,
65 | maxWidth: 272,
66 | whiteSpace: "nowrap",
67 | });
68 |
69 | StyledTableDataCell.displayName = "TableDataCell";
70 |
71 | class Table extends PureComponent {
72 | static Head = StyledTableHead;
73 | static Row = StyledTableRow;
74 | static HeaderCell = StyledTableHeaderCell;
75 | static Body = StyledTableBody;
76 | static DataCell = StyledTableDataCell;
77 |
78 | render() {
79 | return {this.props.children};
80 | }
81 | }
82 |
83 | export default Table;
84 |
--------------------------------------------------------------------------------
/components/typography/Link.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import { css } from "../../stitches.config";
3 | import NextLink from "next/link";
4 | import Text from "./Text";
5 |
6 | interface LinkProps {
7 | external?: boolean;
8 | href: string;
9 | decorator?: boolean;
10 | weight?: "bold" | "medium" | "light";
11 | size?: "normal" | "medium" | "large";
12 | color?: "white" | "blue";
13 | darkenHover?: boolean;
14 | }
15 |
16 | const styles = css({
17 | color: "$brand",
18 | transition: "color 0.1s ease-in-out",
19 | variants: {
20 | darkenHover: {
21 | true: {
22 | color: "$itemUnactive !important",
23 |
24 | "&:hover": {
25 | color: "$brand !important",
26 | },
27 | },
28 | },
29 | color: {
30 | blue: {
31 | color: "$brand",
32 | },
33 | white: {
34 | color: "white",
35 | },
36 | },
37 | textDecoration: {
38 | true: {
39 | textDecoration: "underline",
40 | },
41 | false: {
42 | textDecoration: "none",
43 |
44 | "&:hover": {
45 | textDecoration: "underline",
46 | },
47 | },
48 | },
49 | },
50 | });
51 |
52 | const Link: FunctionComponent = ({
53 | external,
54 | href,
55 | decorator,
56 | children,
57 | weight,
58 | size,
59 | color,
60 | darkenHover,
61 | }) => {
62 | return external ? (
63 |
73 |
74 | {children}
75 |
76 |
77 | ) : (
78 |
79 |
91 | {children}
92 |
93 |
94 | );
95 | };
96 |
97 | export default Link;
98 |
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Whitney";
3 | font-weight: 100;
4 | font-style: normal;
5 | src: local("Whitney Light Regular"),
6 | url("../public/assets/fonts/WhitneyLight.woff") format("woff");
7 | }
8 |
9 | @font-face {
10 | font-family: "Whitney";
11 | font-weight: 100;
12 | font-style: italic;
13 | src: local("Whitney Light Italic"),
14 | url("../public/assets/fonts/WhitneyLightItalic.woff") format("woff");
15 | }
16 |
17 | @font-face {
18 | font-family: "Whitney";
19 | font-weight: 300;
20 | font-style: normal;
21 | src: local("Whitney Book Regular"),
22 | url("../public/assets/fonts/WhitneyBook.woff") format("woff");
23 | }
24 |
25 | @font-face {
26 | font-family: "Whitney";
27 | font-weight: 300;
28 | font-style: italic;
29 | src: local("Whitney Book Italic"),
30 | url("../public/assets/fonts/WhitneyBookItalic.woff") format("woff");
31 | }
32 |
33 | @font-face {
34 | font-family: "Whitney";
35 | font-weight: 400;
36 | font-style: normal;
37 | src: local("Whitney Medium"),
38 | url("../public/assets/fonts/WhitneyMedium.woff") format("woff");
39 | }
40 |
41 | @font-face {
42 | font-family: "Whitney";
43 | font-weight: 400;
44 | font-style: italic;
45 | src: local("Whitney Medium Italic"),
46 | url("../public/assets/fonts/WhitneyMediumItalic.woff") format("woff");
47 | }
48 |
49 | @font-face {
50 | font-family: "Whitney";
51 | font-weight: 600;
52 | font-style: normal;
53 | src: local("Whitney Semibold Regular"),
54 | url("../public/assets/fonts/WhitneySemibold.woff") format("woff");
55 | }
56 |
57 | @font-face {
58 | font-family: "Whitney";
59 | font-weight: 600;
60 | font-style: italic;
61 | src: local("Whitney Semibold Italic"),
62 | url("../public/assets/fonts/WhitneySemiboldItalic.woff") format("woff");
63 | }
64 |
65 | @font-face {
66 | font-family: "Whitney";
67 | font-weight: 700;
68 | font-style: normal;
69 | src: local("Whitney Bold"),
70 | url("../public/assets/fonts/WhitneyBold.woff") format("woff");
71 | }
72 |
73 | @font-face {
74 | font-family: "ABC Ginto Nord Regular";
75 | font-weight: 700;
76 | font-style: normal;
77 | src: local("ABC Ginto Nord Regular"),
78 | url("../public/assets/fonts/ABCGintoNordTrial.woff") format("woff");
79 | }
80 |
--------------------------------------------------------------------------------
/public/assets/icons/summaries.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/typography/Text.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "../../stitches.config";
2 |
3 | const Text = styled("div", {
4 | variants: {
5 | color: {
6 | white: {
7 | color: "#fff",
8 | },
9 | white100: {
10 | color: "#fff",
11 | },
12 | white200: {
13 | color: "#fff",
14 | },
15 | white300: {
16 | color: "#fff",
17 | },
18 | white400: {
19 | color: "#fff",
20 | },
21 | white500: {
22 | color: "#fff",
23 | },
24 | white600: {
25 | color: "#adadad",
26 | },
27 | white700: {
28 | color: "#666",
29 | },
30 | white800: {
31 | color: "#4d4d4d",
32 | },
33 | white900: {
34 | color: "#0d0d0d",
35 | },
36 | primary: {
37 | color: "#4f545c",
38 | },
39 | primary100: {
40 | color: "#f6f6f7",
41 | },
42 | primary200: {
43 | color: "#dcddde",
44 | },
45 | primary300: {
46 | color: "#b9bbbe",
47 | },
48 | primary400: {
49 | color: "#72767d",
50 | },
51 | primary500: {
52 | color: "#4f545c",
53 | },
54 | primary600: {
55 | color: "#36393f",
56 | },
57 | primary700: {
58 | color: "#202225",
59 | },
60 | primary800: {
61 | color: "#18191c",
62 | },
63 | primary900: {
64 | color: "#040405",
65 | },
66 | },
67 | weight: {
68 | light: {
69 | fontWeight: "100",
70 | },
71 | medium: {
72 | fontWeight: 200,
73 | },
74 | bold: {
75 | fontWeight: 700,
76 | },
77 | },
78 | size: {
79 | small: {
80 | fontSize: "12px",
81 | lineHeight: 1,
82 | },
83 | normal: {
84 | fontSize: "16px",
85 | lineHeight: 1.2,
86 |
87 | "@mobile": {
88 | fontSize: "14px",
89 | },
90 | },
91 | medium: {
92 | fontSize: 20,
93 |
94 | "@mobile": {
95 | fontSize: "18px",
96 | },
97 | },
98 | large: {
99 | fontSize: 28,
100 | lineHeight: "34px",
101 |
102 | "@mobile": {
103 | fontSize: "24px",
104 | },
105 | },
106 | },
107 | },
108 | });
109 |
110 | Text.displayName = "Text";
111 |
112 | export default Text;
113 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '26 23 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript', 'typescript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/components/util/form/Searchbar.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "../../../stitches.config";
2 | import { FunctionComponent, useEffect, useState } from "react";
3 | import Spinner from "../Spinner";
4 | import { useTheme } from "next-themes";
5 |
6 | interface SearchbarProps {
7 | onSearch?: (query: string) => void;
8 | onChange?: (query: string) => void;
9 | loading?: boolean;
10 | }
11 |
12 | const Input = styled("input", {
13 | borderRadius: "16px",
14 | height: "32px",
15 | width: "100%",
16 | padding: "0 2.25rem 0 1rem",
17 | border: "none",
18 | fontSize: "1rem",
19 | fontWeight: "400",
20 | fontFamily: "Whitney",
21 | "&:focus-visible": {
22 | outline: "none",
23 | },
24 | variants: {
25 | backgroundColor: {
26 | light: {
27 | backgroundColor: "$backgroundSecondary",
28 | },
29 | dark: {
30 | backgroundColor: "$headerPrimary",
31 | },
32 | },
33 | },
34 | });
35 |
36 | const Icon = styled("svg", {
37 | width: "20px",
38 | height: "20px",
39 | position: "absolute",
40 | top: "30%",
41 | transform: "translateY(-50%)",
42 | right: "0.5rem",
43 | cursor: "pointer",
44 | });
45 |
46 | const Container = styled("div", {
47 | position: "sticky",
48 | height: "56px",
49 | paddingBottom: "1.5rem",
50 | width: "100%",
51 | top: "0",
52 | zIndex: "5",
53 | background: "$backgroundPrimary",
54 | });
55 |
56 | const Searchbar: FunctionComponent = ({
57 | onSearch,
58 | onChange,
59 | loading,
60 | }) => {
61 | const [value, setValue] = useState("");
62 | const { theme } = useTheme();
63 |
64 | useEffect(() => {
65 | if (onChange) onChange(value);
66 | }, [value]);
67 |
68 | return (
69 |
70 | setValue(v.currentTarget.value)}
76 | onKeyUp={(e) => {
77 | if (e.key === "Enter") {
78 | e.preventDefault();
79 | if (onSearch) onSearch(value);
80 | }
81 | }}
82 | />
83 | {!loading ? (
84 | {
86 | if (onSearch) onSearch(value);
87 | }}
88 | viewBox="0 0 30.239 30.239"
89 | >
90 |
97 |
98 | ) : (
99 |
100 | )}
101 |
102 | );
103 | };
104 |
105 | export default Searchbar;
106 |
--------------------------------------------------------------------------------
/components/page/summary/DatePicker.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useState } from "react";
2 | import { styled } from "../../../stitches.config";
3 | import Icon from "../../util/Icon";
4 |
5 | const DatePickerContainer = styled("div", {
6 | display: "grid",
7 | gridTemplateColumns: "auto auto 1fr auto auto",
8 | columnGap: 6,
9 | borderRadius: 6,
10 | height: 42,
11 | width: 288,
12 | overflow: "hidden",
13 | userSelect: "none",
14 |
15 | "@mobile": {
16 | width: "70%",
17 | },
18 | });
19 |
20 | DatePickerContainer.displayName = "DatePicker";
21 |
22 | const IconWrapper = styled("div", {
23 | padding: 13,
24 | textAlign: "center",
25 | cursor: "pointer",
26 | backgroundColor: "#4C5AE8",
27 | "&:hover": {
28 | backgroundColor: "#5865F2",
29 | },
30 | });
31 |
32 | IconWrapper.displayName = "IconWrapper";
33 |
34 | const CurrentValue = styled("div", {
35 | padding: 13,
36 | textAlign: "center",
37 | backgroundColor: "#4C5AE8",
38 | });
39 |
40 | interface DatePickerProps {
41 | values: string[];
42 | current: string;
43 | onChange?: (value: string) => void;
44 | }
45 |
46 | const DatePicker: FC = ({ values, current, onChange }) => {
47 | // remove duplicates
48 | const entries = [...new Set(values)];
49 |
50 | const [currentIndex, setCurrentIndex] = useState(
51 | entries.findIndex((v) => v === current)
52 | );
53 |
54 | useEffect(() => {
55 | if (onChange) onChange(entries[currentIndex]);
56 | // eslint-disable-next-line react-hooks/exhaustive-deps
57 | }, [currentIndex]);
58 |
59 | const seekRelative = (offset: number) => {
60 | const incrementing = offset > 0;
61 |
62 | if (incrementing && currentIndex + offset >= entries.length) {
63 | return setCurrentIndex(entries.length - 1);
64 | }
65 |
66 | if (!incrementing && currentIndex + offset < 0) {
67 | return setCurrentIndex(0);
68 | }
69 |
70 | setCurrentIndex(currentIndex + offset);
71 | };
72 |
73 | const seek = (offset: number) => {
74 | const incrementing = offset > 0;
75 |
76 | if (incrementing && offset >= entries.length - 1) {
77 | return setCurrentIndex(entries.length - 1);
78 | }
79 |
80 | if (!incrementing && offset < 0) {
81 | return setCurrentIndex(0);
82 | }
83 |
84 | setCurrentIndex(offset);
85 | };
86 |
87 | return (
88 |
89 | {
91 | seek(0);
92 | }}
93 | >
94 |
95 |
96 | {
98 | seekRelative(-1);
99 | }}
100 | >
101 |
102 |
103 | {entries[currentIndex]}
104 | {
106 | seekRelative(1);
107 | }}
108 | >
109 |
110 |
111 | {
113 | seek(entries.length - 1);
114 | }}
115 | >
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default DatePicker;
123 |
--------------------------------------------------------------------------------
/lib/api.ts:
--------------------------------------------------------------------------------
1 | import AuthenticationContext from "../components/context/AuthContext";
2 | import CurrentUser from "./api-models/currentUser";
3 | import Event from "././api-models/event";
4 | import PartialEvent from "./api-models/partialEvent";
5 |
6 | export const BaseApiURL =
7 | process.env.NODE_ENV === "development"
8 | ? "http://127.0.0.1:8080"
9 | : "https://api.ddocs.io";
10 |
11 | export const Routes = {
12 | Login: "/auth/login",
13 | Logout: "/auth/logout",
14 | Refresh: "/auth/refresh",
15 | CurrentUser: "/users/@me",
16 | Events: "/events",
17 | Assets: "/assets",
18 | };
19 |
20 | export default class API {
21 | private _context: AuthenticationContext;
22 |
23 | constructor(context: AuthenticationContext) {
24 | this._context = context;
25 | }
26 |
27 | public async getCurrentUser(): Promise {
28 | const result = await this._context.makeAuthedRequest(
29 | API.getRoute(Routes.CurrentUser)
30 | );
31 |
32 | switch (result.status) {
33 | case 200:
34 | return (await result.json()) as CurrentUser;
35 | case 401 || 403 || 404:
36 | throw new RequireSignInError();
37 | default:
38 | this.handleUnknownError(result);
39 | }
40 | }
41 |
42 | public async logout(): Promise {
43 | const result = await this._context.makeAuthedRequest(
44 | API.getRoute(Routes.Logout)
45 | );
46 |
47 | if (!result.ok) this.handleUnknownError(result);
48 | }
49 |
50 | public async login(code: string): Promise {
51 | const result = await this._context.makeAuthedRequest(
52 | API.getRoute(Routes.Login) + "?code=" + code
53 | );
54 |
55 | if (!result.ok) {
56 | this.handleUnknownError(result);
57 | return;
58 | }
59 |
60 | return (await result.json()).token;
61 | }
62 |
63 | public async getEvents(year: string): Promise {
64 | const result = await this._context.makeAuthedRequest(
65 | API.getRoute(Routes.Events) + "?year=" + year
66 | );
67 |
68 | if (!result.ok) {
69 | this.handleUnknownError(result);
70 | return [];
71 | }
72 |
73 | return (await result.json()) as PartialEvent[];
74 | }
75 |
76 | public async getEvent(id: string): Promise {
77 | const result = await this._context.makeAuthedRequest(
78 | API.getRoute(Routes.Events) + "/" + id
79 | );
80 |
81 | switch (result.status) {
82 | case 200:
83 | return (await result.json()) as Event;
84 | case 404:
85 | return undefined;
86 | default:
87 | this.handleUnknownError(result);
88 | }
89 | }
90 |
91 | public async searchEvents(query: string): Promise {
92 | const result = await this._context.makeAuthedRequest(
93 | API.getRoute(Routes.Events) + "?search=" + query
94 | );
95 |
96 | if (!result.ok) {
97 | this.handleUnknownError(result);
98 | return [];
99 | }
100 |
101 | return (await result.json()) as PartialEvent[];
102 | }
103 |
104 | public static getRoute(route: string) {
105 | return `${BaseApiURL}${route}`;
106 | }
107 |
108 | private handleUnknownError(response: Response) {
109 | console.error("Got unsuspected status code", response);
110 | }
111 | }
112 |
113 | // required to sign in
114 | export class RequireSignInError extends Error {
115 | constructor(message: string = "You must be signed in to access this route.") {
116 | super(message);
117 |
118 | Object.setPrototypeOf(this, RequireSignInError.prototype);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/components/page/events/EventSidebar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FunctionComponent, useState } from "react";
3 | import PartialEvent from "../../../lib/api-models/partialEvent";
4 | import { styled } from "../../../stitches.config";
5 | import Card from "../summary/Card";
6 | import { useAuth } from "../../context/AuthContext";
7 | import Scrollbar from "../../layout/Scrollbar";
8 | import Searchbar from "../../util/form/Searchbar";
9 | import SidebarEventCard from "./SidebarEventCard";
10 |
11 | interface EventSidebarProps {
12 | initialEvents: PartialEvent[];
13 | }
14 |
15 | const Container = styled("div", {
16 | paddingLeft: "1rem",
17 | marginLeft: "auto",
18 | marginRight: "0.5rem",
19 | paddingRight: "0.5rem",
20 | marginTop: "2rem",
21 | marginBottom: "2rem",
22 | borderLeft: "1px solid #ccc",
23 | display: "flex",
24 | flexDirection: "column",
25 | alignItems: "center",
26 | width: "332px",
27 | position: "fixed",
28 | right: 0,
29 | bottom: 0,
30 | top: 0,
31 | overflowY: "scroll",
32 | });
33 |
34 | const SearchResultContainer = styled("div", {
35 | position: "relative",
36 | display: "grid",
37 | gridTemplateColumns: "repeat(auto-fit, minmax(100%, max-content))",
38 | gap: 20,
39 |
40 | width: "100%",
41 | });
42 |
43 | const NoItemsFound = styled("div", {});
44 |
45 | interface SearchHistyory {
46 | query: string;
47 | events: PartialEvent[];
48 | }
49 |
50 | const EventSidebar: FunctionComponent = ({
51 | initialEvents,
52 | }) => {
53 | const auth = useAuth();
54 | const [events, setEvents] = useState(initialEvents);
55 | const [searching, setSearching] = useState(false);
56 | const [searchHistory, setSearchHistory] = useState([
57 | {
58 | query: "",
59 | events: initialEvents,
60 | },
61 | ]);
62 |
63 | const handleSearch = async (query: string) => {
64 | setSearching(true);
65 | if (searchHistory.some((x) => x.query === query)) {
66 | setEvents(searchHistory.find((x) => x.query === query)!.events);
67 | setSearching(false);
68 | return;
69 | }
70 |
71 | const result = await auth.Api?.searchEvents(query)!;
72 |
73 | const newHistory = searchHistory.concat({ query, events: result });
74 | setSearchHistory(newHistory);
75 | setEvents(result);
76 | setSearching(false);
77 | };
78 |
79 | return (
80 |
81 | {
84 | handleSearch(v);
85 | }}
86 | onChange={(v) => {
87 | if (searchHistory.some((x) => x.query === v)) {
88 | setEvents(searchHistory.find((x) => x.query === v)!.events);
89 | }
90 | }}
91 | />
92 |
93 | {events.length > 0 ? (
94 | <>
95 | {events.map((event) => (
96 |
97 |
103 |
104 |
105 |
106 | ))}
107 | >
108 | ) : (
109 |
110 | {
111 | "After searching around we couldn't find any events matching your query :(. Try searching with a different keyword instead!"
112 | }
113 |
114 | )}
115 |
116 |
117 | );
118 | };
119 |
120 | export default EventSidebar;
121 |
--------------------------------------------------------------------------------
/components/layout/account/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FunctionComponent } from "react";
3 | import { styled } from "../../../stitches.config";
4 |
5 | interface LoginButtonProps {}
6 |
7 | const Container = styled("div", {
8 | "&:hover": {
9 | boxShadow: "0px 4px 5px 2px #0202024f",
10 | },
11 | borderRadius: "8px",
12 | cursor: "pointer",
13 | display: "flex",
14 | justifyContent: "center",
15 | alignItems: "center",
16 | backgroundColor: "#18191C",
17 | color: "white",
18 | fill: "white",
19 | padding: "1rem",
20 | fontSize: "18px",
21 | boxShadow: "0px 3px 5px 1px #0202024f",
22 | transition: "all 0.1s ease-in-out",
23 | userSelect: "none",
24 |
25 | "@mobile": {
26 | color: "$textNormal",
27 | fill: "$textNormal",
28 | backgroundColor: "transparent",
29 | boxShadow: "none !important",
30 | height: "50px",
31 | margin: "0 1rem",
32 | padding: 0,
33 | },
34 | });
35 |
36 | const DiscordSVG = styled("svg", {
37 | paddingRight: "0.5rem",
38 | });
39 |
40 | const LoginButton: FunctionComponent = (props) => {
41 | // TODO: Set correct redirect URL once deploying to prod
42 | return (
43 |
50 |
51 |
58 |
59 |
63 |
64 |
65 | Login
66 |
67 |
68 | );
69 | };
70 |
71 | export default LoginButton;
72 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import type { AppProps } from "next/app";
3 | import Head from "next/head";
4 | import AccountHeader from "../components/layout/account/AccountHeader";
5 | import AuthenticationContextProvider from "../components/context/AuthContext";
6 | import Sidebar from "../components/layout/Sidebar";
7 | import { BuildDetailsTab, DEFAULT_SIDEBAR_ITEMS } from "../lib/constants";
8 | import { globalCss, styled, lightTheme, css } from "../stitches.config";
9 | import "../styles/global.css";
10 | import NextNprogress from "nextjs-progressbar";
11 | import Footer from "../components/layout/footer/Footer";
12 | import ScrollBar from "../components/layout/Scrollbar";
13 | import { ThemeProvider } from "next-themes";
14 |
15 | const Wrapper = styled("div", {
16 | display: "flex",
17 | flexDirection: "row",
18 | minHeight: "100vh",
19 | height: "100%",
20 | backgroundColor: "$backgroundPrimary",
21 | overflow: "hidden",
22 | });
23 |
24 | Wrapper.displayName = "Wrapper";
25 |
26 | const ContentWrapper = styled("main", {
27 | display: "flex",
28 | flex: "1 1 auto",
29 | flexDirection: "column",
30 | minHeight: "min-content",
31 | // padding: "40px 40px 40px 40px",
32 | width: "100%",
33 | position: "relative",
34 |
35 | "@mobile": {
36 | marginTop: "50px",
37 | },
38 | });
39 |
40 | const AccountHeaderContainer = styled("div", {
41 | position: "absolute",
42 | top: "40px",
43 | right: "40px",
44 | zIndex: "25519",
45 | });
46 |
47 | const MainContentWrapper = styled("div", {
48 | display: "flex",
49 | flexDirection: "column",
50 | height: "100vh",
51 | position: "relative",
52 | width: "100%",
53 | });
54 |
55 | ContentWrapper.displayName = "ContentWrapper";
56 |
57 | const globalStyles = globalCss({
58 | body: { fontFamily: "Whitney", margin: 0, color: "$headerPrimary" },
59 | "*": { boxSizing: "border-box" },
60 | });
61 |
62 | const dontRenderSidebarOn = ["/login"];
63 |
64 | const dontRenderLoginButtonOn = ["/events/[id]"];
65 |
66 | const dontRenderFooterOn = ["/events/[id]"];
67 |
68 | const dontOverflowOn = ["/events/[id]"];
69 |
70 | function DiscordDocsApp({ Component, pageProps, router }: AppProps) {
71 | globalStyles();
72 |
73 | const title = `Discord Docs ${
74 | pageProps.title ? " — " + pageProps.title : ""
75 | }`;
76 | const description = pageProps.description || "Discord Docs";
77 |
78 | return (
79 |
80 |
86 |
92 |
93 |
94 | {title}
95 |
96 |
97 |
98 |
99 | {!dontRenderSidebarOn.includes(router.pathname) && (
100 |
106 | )}
107 |
108 |
116 | {!dontRenderLoginButtonOn.includes(router.pathname) && (
117 |
118 |
119 |
120 | )}
121 |
122 |
123 |
124 | {!dontRenderFooterOn.includes(router.pathname) && }
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
132 | export default DiscordDocsApp;
133 |
--------------------------------------------------------------------------------
/components/layout/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "../../typography/Link";
2 | import { FunctionComponent } from "react";
3 | import { styled } from "../../../stitches.config";
4 | import Text from "../../typography/Text";
5 |
6 | interface FooterProps {}
7 |
8 | interface FooterInnerContent {
9 | content: string;
10 | url?: string;
11 | isLocalLink?: boolean;
12 | }
13 |
14 | const sections = [
15 | {
16 | name: "Legal",
17 | items: [
18 | {
19 | content: "Privacy Policy",
20 | url: "/privacy",
21 | isLocalLink: true,
22 | },
23 | {
24 | content: "Data Use",
25 | url: "/data-use",
26 | isLocalLink: true,
27 | },
28 | {
29 | content: "Cookie Policy",
30 | url: "/cookie-policy",
31 | isLocalLink: true,
32 | },
33 | ],
34 | },
35 | {
36 | name: "Developers",
37 | items: [
38 | {
39 | content: "API Documentation",
40 | url: "/api-documentation",
41 | isLocalLink: true,
42 | },
43 | {
44 | content: "Github",
45 | url: "https://github.com/discord-docs",
46 | },
47 | ],
48 | },
49 | {
50 | name: "Disclamer",
51 | items: [
52 | {
53 | content:
54 | "ddocs.io is not affiliated, associated, authorized, endorsed by, or in anyway officially connected with Discord Inc. or any of its subsidiaries or its affiliates.",
55 | },
56 | ],
57 | },
58 | ];
59 |
60 | const Container = styled("div", {
61 | marginTop: "auto",
62 | display: "flex",
63 | backgroundColor: "$backgroundSecondaryAlt",
64 | paddingBottom: "1rem",
65 | flexDirection: "row",
66 |
67 | zIndex: 250,
68 |
69 | "@mobile": {
70 | flexDirection: "column-reverse",
71 | },
72 | });
73 |
74 | const LeftContentContainer = styled("div", {
75 | display: "flex",
76 | margin: "1rem",
77 |
78 | "@mobile": {
79 | flexWrap: "wrap",
80 | margin: "1rem 2rem",
81 | },
82 | });
83 |
84 | const RightContentContainer = styled("div", {
85 | display: "flex",
86 | flexDirection: "column",
87 | marginLeft: "auto",
88 | marginRight: "2rem",
89 | maxWidth: "300px",
90 |
91 | "@mobile": {
92 | margin: "0rem",
93 | padding: "0 2rem 1rem 2rem",
94 | width: "100%",
95 | maxWidth: "100%",
96 | alignSelf: "center",
97 | },
98 | });
99 |
100 | const Heading = styled("h3", {});
101 |
102 | const FooterSection = styled("div", {
103 | maxWidth: "400px",
104 | margin: "0 2rem",
105 | display: "flex",
106 | flexDirection: "column",
107 | gap: "0.5rem",
108 |
109 | "@mobile": {
110 | width: "50%",
111 | margin: 0,
112 | "&:last-child": {
113 | minWidth: "100%",
114 | margin: "2rem 0 0 0",
115 | },
116 | },
117 | });
118 |
119 | const Footer: FunctionComponent = () => {
120 | const renderSection = (title: string, content: FooterInnerContent[]) => {
121 | return (
122 |
123 |
124 | {title}
125 |
126 | {content.map((item) => {
127 | return (
128 |
129 | {item.url ? (
130 |
136 | {item.content}
137 |
138 | ) : (
139 | {item.content}
140 | )}
141 |
142 | );
143 | })}
144 |
145 | );
146 | };
147 |
148 | return (
149 |
150 |
151 | {sections.map((section) => renderSection(section.name, section.items))}
152 |
153 |
154 | ddocs.io
155 |
156 | Where we present to you our shi-we mean ingenious design to
157 | your brittle eyes.
158 |
159 |
160 | {"©️"} 2022 All rights are reserved.
161 |
162 |
163 |
164 | );
165 | };
166 |
167 | export default Footer;
168 |
--------------------------------------------------------------------------------
/components/page/events/EventHeader.tsx:
--------------------------------------------------------------------------------
1 | import { css, styled } from "../../../stitches.config";
2 | import { FunctionComponent, useState } from "react";
3 | import Author from "../../../lib/api-models/author";
4 | import Event from "../../../lib/api-models/event";
5 | import relativeDate from "../../../lib/relativeDate";
6 | import ReactTooltip from "react-tooltip";
7 | import GracefulImage from "../../util/GracefulImage";
8 |
9 | interface EventHeaderProps {
10 | event: Event;
11 | }
12 |
13 | const Container = styled("div", {
14 | display: "flex",
15 | flexDirection: "column",
16 | });
17 |
18 | const TopContent = styled("div", {
19 | display: "flex",
20 | flexDirection: "row",
21 | });
22 |
23 | const BottomContent = styled("div", {
24 | marginTop: "0.25rem",
25 | });
26 |
27 | const AuthorAvatar = styled("img", {
28 | height: "48px",
29 | width: "48px",
30 | borderRadius: "24px",
31 | });
32 |
33 | const ContributorAvatar = styled("img", {
34 | height: "32px",
35 | width: "32px",
36 | borderRadius: "16px",
37 | margin: "0 0.25rem",
38 | });
39 |
40 | const ContributorAvatarContainer = styled("div", {
41 | display: "flex",
42 | justifyContent: "center",
43 | alignItems: "center",
44 | borderRadius: "20px",
45 | backgroundColor: "$backgroundPrimary",
46 | width: "40px",
47 | height: "40px",
48 | transition: "all 0.15s ease-in-out",
49 | });
50 |
51 | const TopTextContainer = styled("div", {
52 | display: "flex",
53 | flexDirection: "column",
54 | fontSize: "16px",
55 | marginLeft: "0.5rem",
56 | fontWeight: "200",
57 | strong: {
58 | marginBottom: "0.15rem",
59 | fontSize: "20px",
60 | },
61 | });
62 |
63 | const AvatarsContainer = styled("div", {
64 | "&::before": {
65 | content: '" "',
66 | display: "block",
67 | position: "absolute",
68 | top: "0",
69 | right: "100%",
70 | bottom: "40%",
71 | left: "-32px",
72 | marginBottom: "2px",
73 | marginLeft: "-1px",
74 | marginRight: "4px",
75 | marginTop: "5px",
76 |
77 | borderBottom: "3px solid #747F8D",
78 | borderRight: "0 solid #747F8D",
79 | borderTop: "0 solid #747F8D",
80 | borderBottomLeftRadius: "6px",
81 | borderLeft: "3px solid #747F8D",
82 | },
83 | position: "relative",
84 | marginLeft: "55px",
85 | display: "flex",
86 | });
87 |
88 | const TooltipStyles = css({
89 | padding: "5px 10px !important",
90 | borderRadius: "5px !important",
91 | background: "$backgroundSecondaryAlt !important",
92 | color: "$textPrimary !important",
93 | fontSize: "1rem !important",
94 | opacity: "1 !important",
95 | });
96 |
97 | const EventHeader: FunctionComponent = ({ event }) => {
98 | const [expanded, setExpanded] = useState(false);
99 |
100 | return (
101 |
102 |
103 |
111 |
112 |
113 | {event.author.username}#{event.author.discriminator}
114 |
115 | Updated {relativeDate(Date.parse(event.lastRevised))}
116 |
117 |
118 | {event.contributors.length > 0 && (
119 |
120 | {
125 | setExpanded(true);
126 | }}
127 | onMouseLeave={() => {
128 | setExpanded(false);
129 | }}
130 | >
131 | {event.contributors
132 | .sort((a, b) => a.username.localeCompare(b.username))
133 | .map((contributor, i) => {
134 | return (
135 |
144 |
153 |
154 | );
155 | })}
156 |
157 |
163 |
164 | )}
165 |
166 | );
167 | };
168 |
169 | export default EventHeader;
170 |
--------------------------------------------------------------------------------
/components/typography/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import ReactMarkdown from "react-markdown";
3 | import { styled } from "../../stitches.config";
4 | import Text from "./Text";
5 | import remarkGfm from "remark-gfm";
6 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
7 | import {
8 | materialDark,
9 | materialLight,
10 | } from "react-syntax-highlighter/dist/cjs/styles/prism";
11 | import Header from "./Header";
12 | import Link from "./Link";
13 | import { useTheme } from "next-themes";
14 |
15 | interface MarkdownProps {
16 | content?: string;
17 | }
18 |
19 | const Image = styled("img", {
20 | width: "-moz-available;width:-webkit-fill-available",
21 | borderRadius: "10px",
22 | objectFit: "contain",
23 | margin: "1rem",
24 | });
25 |
26 | const Quote = styled("blockquote", {
27 | borderLeft: "5px solid $backgroundAccent",
28 | borderRadius: "5px",
29 | margin: "0.5rem 0.25rem",
30 | padding: "0.25rem 0.5rem",
31 | background: "$backgroundTeritialy",
32 | });
33 |
34 | const UnorderedList = styled("ul", {
35 | paddingLeft: "1.5rem",
36 | });
37 |
38 | const OrderedList = styled("ol", {
39 | paddingLeft: "1.5rem",
40 | });
41 |
42 | const HR = styled("hr", {
43 | margin: "1rem 0",
44 | });
45 |
46 | const ListItem = styled("li", {
47 | fontSize: "20px",
48 | fontWeight: "200",
49 | lineHeight: "1.2",
50 | "&::marker": {
51 | unicodeBidi: "isolate",
52 | fontVariantNumeric: "tabular-nums",
53 | textTransform: "none",
54 | textIndent: "0",
55 | textAlign: "start",
56 | textAlignLast: "start",
57 | },
58 | });
59 |
60 | const Markdown: FunctionComponent = ({ content }) => {
61 | const { theme } = useTheme();
62 | return (
63 |
69 | {children}
70 |
71 | );
72 | },
73 | h2({ node, className, children, ...props }) {
74 | return (
75 |
78 | );
79 | },
80 | h3({ node, className, children, ...props }) {
81 | return (
82 |
85 | );
86 | },
87 | h4({ node, className, children, ...props }) {
88 | return (
89 |
92 | );
93 | },
94 | h5({ node, className, children, ...props }) {
95 | return (
96 |
99 | );
100 | },
101 | h6({ node, className, children, ...props }) {
102 | return (
103 |
106 | );
107 | },
108 | p({ node, className, children, ...props }) {
109 | return (
110 |
111 | {children}
112 |
113 | );
114 | },
115 | ul({ node, className, children, ...props }) {
116 | return {children};
117 | },
118 | ol({ node, className, children, ...props }) {
119 | return {children};
120 | },
121 | a({ node, className, children, ...props }) {
122 | return (
123 |
127 | {children}
128 |
129 | );
130 | },
131 | code({ node, inline, className, children, ...props }) {
132 | const match = /language-(\w+)/.exec(className || "");
133 | return (
134 |
141 | );
142 | },
143 | li({ node, className, children, ...props }) {
144 | return {children};
145 | },
146 | blockquote({ node, className, children, ...props }) {
147 | return {children}
;
148 | },
149 | img({ node, className, children, ...props }) {
150 | return ;
151 | },
152 | hr({ node, className, children, ...props }) {
153 | return
;
154 | },
155 | }}
156 | >
157 | {content || ""}
158 |
159 | );
160 | };
161 |
162 | export default Markdown;
163 |
--------------------------------------------------------------------------------
/pages/summaries.tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import Link from "next/link";
3 | import React, { useEffect, useState } from "react";
4 | import { FC } from "react";
5 | import Card from "../components/page/summary/Card";
6 | import CardList from "../components/page/summary/CardList";
7 | import { AuthContext } from "../components/context/AuthContext";
8 | import DatePicker from "../components/page/summary/DatePicker";
9 | import Scrollbar from "../components/layout/Scrollbar";
10 | import API, { Routes } from "../lib/api";
11 | import PartialEvent from "../lib/api-models/partialEvent";
12 | import Summary from "../lib/api-models/summary";
13 | import { css, styled } from "../stitches.config";
14 | import GracefulImage from "../components/util/GracefulImage";
15 |
16 | const Banner = styled("div", {
17 | display: "flex",
18 | height: "35.2vh",
19 | minHeight: "35.2vh",
20 | background:
21 | "url('/assets/images/summaries-banner.svg'), linear-gradient(90deg, #5865F2 0%, #414EDE 100%);",
22 | backgroundPosition: "bottom center",
23 | overflow: "hidden",
24 | backgroundRepeat: "no-repeat",
25 | color: "white",
26 |
27 | "@mobile": {
28 | height: "min-content",
29 | minHeight: "min-content",
30 | padding: "1rem 0",
31 | },
32 | });
33 |
34 | const BannerContainer = styled("div", {
35 | alignSelf: "flex-end",
36 | margin: "auto 2.5% auto 2.5%",
37 | });
38 |
39 | const BannerTitle = styled("h1", {
40 | fontWeight: "bold",
41 | fontSize: "5em",
42 | margin: 0,
43 |
44 | "@mobile": {
45 | fontSize: "3rem",
46 | },
47 | "@media (max-width: 400px)": {
48 | fontSize: "2.5rem",
49 | },
50 |
51 | "@media (max-width: 300px)": {
52 | fontSize: "2rem",
53 | },
54 | });
55 |
56 | const BannerSubtitle = styled("h3", {
57 | fontWeight: "normal",
58 | fontSize: "2em",
59 |
60 | "@mobile": {
61 | fontSize: "1.5em",
62 | },
63 | });
64 |
65 | Banner.displayName = "Banner";
66 |
67 | const Summary = styled(Card, {
68 | height: 290,
69 | width: 250,
70 |
71 | "@mobile": {
72 | width: "auto",
73 | height: 250,
74 | },
75 | });
76 |
77 | const Wrapper = styled("div", {
78 | padding: 40,
79 | overflowY: "auto",
80 |
81 | "@mobile": {
82 | padding: "16px 0",
83 | },
84 | });
85 |
86 | const SummaryWrapper = styled("div", {
87 | userSelect: "none",
88 | cursor: "pointer",
89 |
90 | "@mobile": {
91 | padding: "8px",
92 | },
93 | });
94 |
95 | interface SummaryProps {
96 | description: string;
97 | currentEvents: PartialEvent[];
98 | }
99 |
100 | interface LoadedEvents {
101 | events: PartialEvent[];
102 | year: string;
103 | }
104 |
105 | const Summaries: FC = ({ currentEvents }) => {
106 | const [year, setYear] = useState(`${new Date().getFullYear()}`);
107 | const [events, setEvents] = useState(currentEvents);
108 | const [loadedEvents, setLoadedEvents] = useState([]);
109 |
110 | const auth = React.useContext(AuthContext);
111 |
112 | const getEvents = async () => {
113 | console.log(loadedEvents);
114 | if (loadedEvents?.some((x) => x.year === year)) {
115 | setEvents(loadedEvents.find((x) => x.year === year)!.events);
116 | return;
117 | }
118 |
119 | const events = await auth.Api!.getEvents(year);
120 |
121 | setEvents(events);
122 |
123 | const l = loadedEvents?.concat({ events, year });
124 | setLoadedEvents(l);
125 | };
126 |
127 | useEffect(() => {
128 | getEvents();
129 | }, [year]);
130 |
131 | const SummaryElements = () => (
132 | <>
133 | {events.map((i, idx) => (
134 |
135 |
136 |
137 | {i.description}
138 |
139 |
140 |
141 | ))}
142 | >
143 | );
144 |
145 | return (
146 | <>
147 |
148 |
149 | Stage Summaries
150 |
151 | Every developer stage — all here for you.
152 |
153 | {
155 | setYear(h);
156 | }}
157 | current={year}
158 | values={["2019", "2020", "2021", "2022", "2023", "2024"]}
159 | />
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | >
169 | );
170 | };
171 |
172 | export async function getServerSideProps() {
173 | // TODO: Use api
174 | // const response = await axios.get("https://api.ddocs.io/...");
175 |
176 | // grab years
177 | const yearsAvailable = [2021, 2022];
178 |
179 | // grab items from selected year
180 | const result = await axios.get(
181 | API.getRoute(Routes.Events + `?year=${new Date().getFullYear()}`)
182 | );
183 |
184 | return {
185 | props: {
186 | description: `Stage Summaries`,
187 | currentEvents: result.status === 200 ? result.data : [],
188 | },
189 | };
190 | }
191 |
192 | export default Summaries;
193 |
--------------------------------------------------------------------------------
/components/context/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import jwt from "jsonwebtoken";
3 | import { createContext, useContext } from "react";
4 | import API, { Routes } from "../../lib/api";
5 |
6 | export interface Account {
7 | uid: string;
8 | jwt: string;
9 |
10 | username: string;
11 | discriminator: string;
12 | avatar: string;
13 | isAuthor: boolean;
14 | }
15 |
16 | export type AuthContextType = {
17 | isAuthenticated: boolean;
18 | account?: Account;
19 |
20 | setUser: (uid: string, jwt: string) => Promise;
21 | clearUser: () => void;
22 | hasChecked: boolean;
23 |
24 | loginCallback: (
25 | callback: (isAuthed: boolean, details?: Account) => void
26 | ) => void;
27 | Api?: API;
28 | };
29 |
30 | export const AuthContext = createContext({
31 | setUser: (_: string, __: string, ___: boolean = true) =>
32 | new Promise((_, __) => {}),
33 | isAuthenticated: false,
34 | clearUser: () => {},
35 | loginCallback: (_: (IsAuthed: boolean, details?: Account) => void) => {},
36 | hasChecked: false,
37 | Api: undefined,
38 | });
39 |
40 | export const useAuth = () => useContext(AuthContext);
41 |
42 | interface AuthenticationContextProps {}
43 |
44 | interface AuthenticationContextState {
45 | account?: Account;
46 | hasChecked: boolean;
47 | }
48 |
49 | class AuthenticationContext extends React.Component<
50 | AuthenticationContextProps,
51 | AuthenticationContextState
52 | > {
53 | public Api: API;
54 | private loginCallback: (IsAuthed: boolean, details?: Account) => void =
55 | () => {};
56 | private hasCheckedInternal: boolean = false;
57 | private _jwt: string | undefined;
58 |
59 | async componentDidMount() {
60 | if (!this.isAuthed) {
61 | await this.refreshToken();
62 |
63 | this.loginCallback(this.state.account !== undefined, this.state.account);
64 | }
65 |
66 | this.hasCheckedInternal = true;
67 | this.setState({ hasChecked: true });
68 | }
69 |
70 | public get isAuthed(): boolean {
71 | const isAuthed = this.state.account !== undefined;
72 | return isAuthed;
73 | }
74 |
75 | constructor(props: AuthenticationContextProps) {
76 | super(props);
77 | this.Api = new API(this);
78 | this.state = { hasChecked: false };
79 | }
80 |
81 | public async makeAuthedRequest(
82 | input: RequestInfo,
83 | init?: RequestInit,
84 | returnOn401: boolean = false
85 | ): Promise {
86 | const r = await fetch(input, {
87 | ...init,
88 | credentials: "include",
89 | headers: {
90 | Authorization:
91 | this.state.account?.jwt || this._jwt
92 | ? `Bearer ${this.state.account?.jwt ?? this._jwt}`
93 | : "",
94 | },
95 | });
96 |
97 | if (
98 | r.status == 401 &&
99 | (await r.json())?.reason == "Invalid authorization"
100 | ) {
101 | if (returnOn401) {
102 | return r;
103 | }
104 |
105 | await this.refreshToken();
106 |
107 | return this.makeAuthedRequest(input, init, true);
108 | } else {
109 | return r;
110 | }
111 | }
112 |
113 | private async refreshToken() {
114 | const url = API.getRoute(Routes.Refresh);
115 | const r = await fetch(url, {
116 | credentials: "include",
117 | });
118 |
119 | if (!r.ok) {
120 | return;
121 | }
122 |
123 | const data = (await r.json()) as { token: string };
124 |
125 | const claims = jwt.decode(data.token) as {
126 | uid: string;
127 | exp: number;
128 | iat: number;
129 | };
130 |
131 | await this.setUser(claims.uid, data.token);
132 | }
133 |
134 | private setStateAsync(state: AuthenticationContextState) {
135 | return new Promise((resolve) => {
136 | this.setState(state, resolve);
137 | });
138 | }
139 |
140 | private setUser = async (uid: string, jwt: string) => {
141 | this._jwt = jwt;
142 | const details = await this.Api.getCurrentUser();
143 | if (details) {
144 | await this.setStateAsync({
145 | ...this.state,
146 | account: {
147 | uid,
148 | jwt,
149 | ...details,
150 | },
151 | });
152 | } else {
153 | this._jwt = undefined;
154 | throw new Error("failed to get user details");
155 | }
156 | };
157 |
158 | private clearUser = async () => {
159 | await this.makeAuthedRequest(API.getRoute(Routes.Logout), {
160 | method: "POST",
161 | credentials: "include",
162 | });
163 | this._jwt = undefined;
164 | this.setState({ account: undefined });
165 | };
166 |
167 | public setLoginCallback = (callback: (IsAuthed: boolean) => void) => {
168 | this.loginCallback = callback;
169 | if (this.hasCheckedInternal || this.state.hasChecked)
170 | callback(this.isAuthed);
171 | };
172 |
173 | render() {
174 | return (
175 |
185 | {this.props.children}
186 |
187 | );
188 | }
189 | }
190 |
191 | export default AuthenticationContext;
192 |
--------------------------------------------------------------------------------
/components/page/events/Summary.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "../../../stitches.config";
2 | import { createRef, FunctionComponent, useEffect, useState } from "react";
3 | import Summary from "../../../lib/api-models/summary";
4 | import Markdown from "../../typography/Markdown";
5 | import ToggleableArror from "../../util/ToggleableArrow";
6 |
7 | interface SummaryProps {
8 | summary: Summary;
9 | fullExpanded: boolean;
10 | }
11 |
12 | const Container = styled("div", {
13 | display: "flex",
14 | flexDirection: "column",
15 | overflow: "hidden",
16 | borderRadius: "6px",
17 | });
18 |
19 | const SummaryHeader = styled("summary", {
20 | backgroundColor: "$backgroundSecondary",
21 | padding: "1rem",
22 | fontSize: "20px",
23 | zIndex: 2,
24 | userSelect: "none",
25 | display: "flex",
26 | cursor: "pointer",
27 | });
28 |
29 | const SummaryBody = styled("div", {
30 | backgroundColor: "$backgroundSecondaryAlt",
31 | padding: "1rem 2rem",
32 | borderRadius: "0px 0px 6px 6px",
33 |
34 | "@mobile": {
35 | padding: "1rem 0.75rem",
36 | },
37 | });
38 |
39 | const Details = styled("details", {});
40 |
41 | const Summary: FunctionComponent = ({
42 | summary,
43 | fullExpanded,
44 | }) => {
45 | const [init, setInit] = useState(false);
46 | const [expanded, setExpanded] = useState(false);
47 | const [fixedHeight, setFixedHeight] = useState(false);
48 | const [overflow, setOverflow] = useState(false);
49 | const [isClosing, setIsClosing] = useState(false);
50 | const [isExpanding, setIsExpanding] = useState(false);
51 | const [bodyHeight, setBodyHeight] = useState(0);
52 | const [headerHeight, setHeaderHeight] = useState(0);
53 | const [detailHeight, setDetailHeight] = useState(0);
54 | const [animation, setAnimation] = useState();
55 | const bodyRef = createRef();
56 | const headerRef = createRef();
57 | const detailsRef = createRef();
58 |
59 | useEffect(() => {
60 | if (init) {
61 | if (fullExpanded) open();
62 | else shrink();
63 | }
64 | }, [fullExpanded]);
65 |
66 | useEffect(() => {
67 | if (bodyRef.current) {
68 | if (bodyRef.current.clientHeight > headerHeight) {
69 | setBodyHeight(bodyRef.current.clientHeight - headerHeight);
70 | }
71 |
72 | if (!init) setInit(true);
73 | }
74 | }, [bodyRef]);
75 |
76 | useEffect(() => {
77 | setHeaderHeight(headerRef.current?.clientHeight || 0);
78 | }, [headerRef]);
79 |
80 | useEffect(() => {
81 | setDetailHeight(detailsRef.current?.clientHeight || 0);
82 | }, [detailsRef]);
83 |
84 | const shrink = () => {
85 | const startHeight = detailsRef.current?.offsetHeight!;
86 | const endHeight = headerHeight;
87 |
88 | animation?.cancel();
89 |
90 | const an = detailsRef.current!.animate(
91 | [{ height: `${startHeight}px` }, { height: `${endHeight}px` }],
92 | {
93 | duration: 250,
94 | easing: "ease-in-out",
95 | }
96 | );
97 |
98 | an.onfinish = () => onAnimationComplete(false);
99 | an.oncancel = () => setIsClosing(false);
100 | setAnimation(an);
101 | };
102 |
103 | const open = () => {
104 | expand();
105 | };
106 |
107 | const expand = () => {
108 | const startHeight = detailsRef.current?.offsetHeight!;
109 |
110 | const endHeight = bodyHeight + headerHeight;
111 |
112 | animation?.cancel();
113 |
114 | const an = detailsRef.current!.animate(
115 | [{ height: `${startHeight}px` }, { height: `${endHeight}px` }],
116 | {
117 | duration: 250,
118 | easing: "ease-in-out",
119 | }
120 | );
121 | an.onfinish = () => onAnimationComplete(true);
122 | an.oncancel = () => setIsExpanding(false);
123 |
124 | setAnimation(an);
125 |
126 | setFixedHeight(true);
127 | setExpanded(true);
128 | setIsExpanding(true);
129 | };
130 |
131 | const onAnimationComplete = (open: boolean) => {
132 | setExpanded(open);
133 | setFixedHeight(false);
134 | setOverflow(false);
135 | setIsClosing(false);
136 | setIsExpanding(false);
137 | };
138 |
139 | const handleClick = (e: React.MouseEvent) => {
140 | e.preventDefault();
141 |
142 | if (isClosing || !detailsRef.current?.open) {
143 | open();
144 | } else if (isExpanding || detailsRef.current.open) {
145 | shrink();
146 | }
147 | setOverflow(true);
148 | };
149 |
150 | return (
151 |
152 |
160 |
167 | {summary.title}
168 |
169 |
170 | {init && (
171 |
177 |
178 |
179 | )}
180 |
181 |
182 | {!init && (
183 |
191 |
192 |
193 | )}
194 |
195 | );
196 | };
197 |
198 | export default Summary;
199 |
--------------------------------------------------------------------------------
/pages/events/[id].tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { GetServerSideProps } from "next";
3 | import { FunctionComponent, useState } from "react";
4 | import AccountHeader from "../../components/layout/account/AccountHeader";
5 | import EventHeader from "../../components/page/events/EventHeader";
6 | import EventSidebar from "../../components/page/events/EventSidebar";
7 | import API, { Routes } from "../../lib/api";
8 | import Event from "../../lib/api-models/event";
9 | import relativeDate from "../../lib/relativeDate";
10 | import { styled } from "../../stitches.config";
11 | import Text from "../../components/typography/Text";
12 | import Summary from "../../components/page/events/Summary";
13 | import PartialEvent from "../../lib/api-models/partialEvent";
14 | import ApiSummary from "../../lib/api-models/summary";
15 | import ToggleableArrow from "../../components/util/ToggleableArrow";
16 | import Scrollbar from "../../components/layout/Scrollbar";
17 |
18 | interface EventProps {
19 | event: Event;
20 | related: PartialEvent[];
21 | }
22 |
23 | const Container = styled("div", {
24 | width: "100%",
25 | display: "flex",
26 | flexDirection: "row",
27 | marginBottom: "2rem",
28 | height: "100vh",
29 | });
30 |
31 | const ContentContainer = styled("div", {
32 | width: "100%",
33 | position: "relative",
34 | display: "flex",
35 | flexDirection: "column",
36 | maxWidth: "1100px",
37 | marginTop: "2rem",
38 |
39 | "@mobile": {
40 | height: "min-content",
41 | paddingBottom: "2rem",
42 | },
43 | });
44 |
45 | const PageHeader = styled("h3", {
46 | fontWeight: "700",
47 | fontSize: "50px",
48 | lineHeight: "55px",
49 | margin: "2rem 0",
50 |
51 | "@mobile": {
52 | fontSize: "32px",
53 | lineHeight: "36px",
54 | marginBottom: "1rem",
55 | },
56 | });
57 |
58 | const EventBanner = styled("img", {
59 | width: "100%",
60 | height: "auto",
61 | objectFit: "cover",
62 | borderRadius: "3px",
63 | marginBottom: "1rem",
64 | marginLeft: "auto",
65 | marginRight: "auto",
66 | });
67 |
68 | const EventSidebarContainer = styled("div", {
69 | paddingLeft: "1rem",
70 | marginLeft: "auto",
71 | marginRight: "1rem",
72 | minWidth: "316px",
73 | position: "unset",
74 |
75 | "@mobile": {
76 | display: "none",
77 | },
78 | });
79 |
80 | const SummaryItemContainer = styled("div", {
81 | display: "flex",
82 | flexDirection: "column",
83 | justifyContent: "center",
84 | gap: "0.75rem",
85 | });
86 |
87 | const SummaryHeading = styled("h4", {
88 | fontWeight: "bold",
89 | fontSize: "26px",
90 | marginTop: "1.75rem",
91 | marginBottom: "1rem",
92 | });
93 |
94 | const ScrollableContainer = styled("div", {
95 | width: "100%",
96 | marginRight: "1rem",
97 | paddingRight: "1rem",
98 | marginLeft: "2rem",
99 | marginBottom: "2rem",
100 | display: "flex",
101 | justifyContent: "center",
102 | overflowY: "auto",
103 |
104 | "@mobile": {
105 | marginRight: "0",
106 | marginLeft: "0.75rem",
107 | paddingRight: "0.75rem",
108 | },
109 | });
110 |
111 | const AccountHeaderContainer = styled("div", {
112 | position: "absolute",
113 | top: "0",
114 | right: "0",
115 | zIndex: "25519",
116 | });
117 |
118 | const Event: FunctionComponent = ({ event, related }) => {
119 | const [whatsNewExpanded, setWhatsNewExpanded] = useState(false);
120 | const [whatsChangedExpanded, setWhatsChangedExpanded] = useState(false);
121 | const [qnaEpanded, setQnaExpanded] = useState(false);
122 |
123 | const getWhatsNew = () => {
124 | return event.summaries.filter((x) => x.isNew);
125 | };
126 |
127 | const getWhatsChanged = () => {
128 | return event.summaries.filter((x) => x.type === "feature");
129 | };
130 |
131 | const getQNA = () => {
132 | return event.summaries.filter((x) => x.type === "qnaanswer");
133 | };
134 |
135 | const renderSection = (
136 | summaries: ApiSummary[],
137 | title: string,
138 | fullExpanded: boolean,
139 | setExpanded: (expanded: boolean) => void
140 | ) => {
141 | return (
142 | <>
143 | setExpanded(!fullExpanded)}
146 | >
147 | {title}
148 |
149 |
150 | {summaries.map((x) => (
151 |
152 | ))}
153 |
154 | >
155 | );
156 | };
157 |
158 | return (
159 |
160 |
163 |
164 |
165 |
166 |
167 |
168 | {event.title}
169 | {event.thumbnail && }
170 |
171 | {event.description}
172 |
173 | {renderSection(
174 | getWhatsNew(),
175 | "What's New",
176 | whatsNewExpanded,
177 | setWhatsNewExpanded
178 | )}
179 | {renderSection(
180 | getWhatsChanged(),
181 | "What's Changed",
182 | whatsChangedExpanded,
183 | setWhatsChangedExpanded
184 | )}
185 | {renderSection(
186 | getQNA(),
187 | "Questions and Answers",
188 | qnaEpanded,
189 | setQnaExpanded
190 | )}
191 |
192 |
193 |
194 |
195 |
196 |
197 | );
198 | };
199 |
200 | export const getServerSideProps: GetServerSideProps = async (
201 | context
202 | ) => {
203 | const { id } = context.query;
204 |
205 | const event = await axios.get(API.getRoute(Routes.Events + `/${id}`));
206 | const related = await axios.get(
207 | API.getRoute(Routes.Events + `/${id}/related`)
208 | );
209 | return {
210 | props: {
211 | event: event.data as Event,
212 | related: related.data as PartialEvent[],
213 | },
214 | };
215 | };
216 |
217 | export default Event;
218 |
--------------------------------------------------------------------------------
/components/layout/account/AccountButton.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import {
3 | createRef,
4 | FunctionComponent,
5 | useEffect,
6 | useRef,
7 | useState,
8 | } from "react";
9 | import { lightTheme, styled } from "../../../stitches.config";
10 | import { Account, AuthContextType } from "../../context/AuthContext";
11 | import Text from "../../typography/Text";
12 | import GracefulImage from "../../util/GracefulImage";
13 |
14 | interface AccountButtonProps {
15 | account: Account;
16 | }
17 |
18 | const DesktopLayoutContainer = styled("div", {
19 | display: "flex",
20 | alignItems: "center",
21 | justifyContent: "center",
22 | height: "64px",
23 | position: "relative",
24 | borderRadius: "12px",
25 | backgroundColor: "#18191C",
26 | transition: "all 0.25s ease-in-out",
27 | overflow: "hidden",
28 | flexDirection: "row",
29 | color: "white",
30 |
31 | "@mobile": {
32 | display: "none",
33 | },
34 | });
35 |
36 | const OuterContentContainer = styled("div", {
37 | height: "64px",
38 | marginBottom: "auto",
39 | display: "flex",
40 | alignItems: "center",
41 | justifyContent: "center",
42 | backgroundColor: "#18191C",
43 | zIndex: 5,
44 | strong: {
45 | marginRight: "0.25rem",
46 | fontSize: "20px",
47 | },
48 | });
49 |
50 | const InnerContentContainer = styled("div", {
51 | overflow: "hidden",
52 | transition: "all 0.25s ease-in-out",
53 | position: "absolute",
54 | display: "flex",
55 | justifyContent: "center",
56 | bottom: "0px",
57 | width: "100%",
58 | });
59 |
60 | const LogoutButton = styled("div", {
61 | color: "#ED4245",
62 | fontSize: "20px",
63 | margin: "0.25rem",
64 | cursor: "pointer",
65 | userSelect: "none",
66 | fontWeight: "200",
67 | });
68 |
69 | const DropdownItem = styled("div", {
70 | width: "100%",
71 | display: "flex",
72 | justifyContent: "center",
73 | marginBottom: "0.75rem",
74 | });
75 |
76 | const ArrowIcon = styled("svg", {
77 | width: "24px",
78 | height: "16px",
79 | fill: "white",
80 | transition: "all 0.25s ease-in-out",
81 | marginRight: "0.75rem",
82 | });
83 |
84 | const MobileLayoutContainer = styled("div", {
85 | display: "none",
86 | height: "50px",
87 | marginRight: "1rem",
88 | alignItems: "center",
89 |
90 | "@mobile": {
91 | display: "flex",
92 | },
93 | });
94 |
95 | const MobileDropdownContainer = styled("div", {
96 | backgroundColor: "$backgroundTeritialy",
97 | borderRadius: "5px",
98 | transition: "all 0.15s ease-in-out",
99 | right: "10px",
100 | padding: "0.75rem",
101 | position: "fixed",
102 | display: "flex",
103 | maxWidth: "220px",
104 | width: "100%",
105 | });
106 |
107 | const MobileLeftContentContainer = styled("div", {
108 | display: "flex",
109 | flexDirection: "column",
110 | flex: "1",
111 | });
112 |
113 | const AccountButton: FunctionComponent = ({ account }) => {
114 | const [expanded, setExpanded] = useState(false);
115 | const [height, setHeight] = useState(0);
116 | const contentRef = createRef();
117 |
118 | useEffect(() => {
119 | setHeight(contentRef.current?.clientHeight ?? 0);
120 | }, [contentRef]);
121 |
122 | return (
123 | <>
124 |
125 | setExpanded(!expanded)}
127 | id={account.avatar}
128 | width={32}
129 | height={32}
130 | style={{
131 | borderRadius: "32px",
132 | cursor: "pointer",
133 | }}
134 | />
135 |
141 |
142 |
148 | Logged in as
149 |
150 | {`${account.username}#${account.discriminator}`}
155 |
156 |
157 |
166 | Logout
167 |
168 |
169 |
170 |
171 | setExpanded(true)}
173 | onMouseLeave={() => setExpanded(false)}
174 | style={{
175 | height: expanded ? `${64 + height}px` : "64px",
176 | }}
177 | >
178 |
179 |
188 | {`${account.username}#${account.discriminator}`}
189 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 | Logout
203 |
204 |
205 |
206 |
207 | >
208 | );
209 | };
210 |
211 | export default AccountButton;
212 |
--------------------------------------------------------------------------------
/public/assets/images/missing-wumpus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/layout/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactChild, useEffect, useState } from "react";
2 | import Link from "next/link";
3 |
4 | import { css, styled, config } from "../../stitches.config";
5 | import Icon from "../util/Icon";
6 | import { useRouter } from "next/dist/client/router";
7 | import ThemeToggle from "../util/ThemeToggle";
8 | import Hamburger from "../../public/assets/icons/hamburger.svg";
9 | import Draggable from "react-draggable";
10 | import Logo from "../util/Logo";
11 |
12 | const StyledSidebar = styled("aside", {
13 | "@mobile": {
14 | position: "fixed",
15 | top: 0,
16 | bottom: 0,
17 | zIndex: 25,
18 | width: "100%",
19 | maxWidth: "66%",
20 | padding: "66px 10px 16px",
21 | right: "100vw",
22 | backgroundColor: "$backgroundSecondary",
23 | minWidth: "250px",
24 | },
25 | });
26 |
27 | const SidebarContainer = styled("div", {
28 | flexBasis: 350,
29 | backgroundColor: "$backgroundSecondary",
30 | padding: 16,
31 |
32 | "@mobile": {
33 | position: "fixed",
34 | left: 0,
35 | top: 0,
36 | bottom: 0,
37 | right: 0,
38 | width: "100%",
39 | height: "100%",
40 | transition: "background-color 0.15s ease-in-out",
41 | zIndex: 251,
42 | },
43 | });
44 |
45 | StyledSidebar.displayName = "Sidebar";
46 |
47 | const StyledSidebarHeader = styled("header", {
48 | display: "flex",
49 | flexDirection: "row",
50 | columnGap: 12,
51 | marginLeft: 2,
52 | marginTop: 13,
53 | marginBottom: 26,
54 | cursor: "pointer",
55 | userSelect: "none",
56 |
57 | "@mobile": {
58 | margin: 0,
59 | height: "50px",
60 | position: "fixed",
61 | top: 0,
62 | left: 0,
63 | right: 0,
64 | zIndex: 100,
65 | backgroundColor: "$backgroundTeritialy",
66 | display: "flex",
67 | alignItems: "center",
68 | padding: "16px",
69 | cursor: "default",
70 | },
71 | });
72 |
73 | StyledSidebarHeader.displayName = "SidebarHeader";
74 |
75 | const StyledSidebarNavBar = styled("nav", {
76 | display: "flex",
77 | flexDirection: "column",
78 | height: "100%",
79 |
80 | "@mobile": {
81 | paddingLeft: "8px",
82 | },
83 | });
84 |
85 | StyledSidebarNavBar.displayName = "SidebarNavBar";
86 |
87 | const StyledSidebarNavBarItem = styled("a", {
88 | display: "flex",
89 | alignItems: "center",
90 | gap: 12,
91 | fontSize: 16,
92 | lineHeight: "20px",
93 | textDecoration: "none",
94 | color: "$itemUnactive",
95 | transition: "color .125s, background .125s",
96 | "&:hover": {
97 | color: "$headerPrimary",
98 | },
99 | variants: {
100 | active: {
101 | true: {
102 | color: "#eee",
103 | backgroundColor: "$brand",
104 | borderRadius: 3,
105 | "&:hover": {
106 | color: "white",
107 | },
108 | },
109 | },
110 | size: {
111 | medium: {
112 | padding: 8,
113 | marginBottom: 8,
114 | },
115 | },
116 | },
117 | defaultVariants: {
118 | size: "medium",
119 | active: false,
120 | },
121 | });
122 |
123 | StyledSidebarNavBarItem.displayName = "SidebarNavBarItem";
124 |
125 | const StyledSidebarSubheading = styled("h3", {
126 | fontSize: 16,
127 | fontWeight: 600,
128 | lineHeight: "20px",
129 | marginBottom: 8,
130 | marginTop: 16,
131 | });
132 |
133 | StyledSidebarSubheading.displayName = "SidebarSubheading";
134 |
135 | const HamburgerWrapper = styled("div", {
136 | display: "none",
137 | color: "$itemUnactive",
138 | marginLeft: "-5px",
139 | transition: "color .125s ease-in-out",
140 | cursor: "pointer",
141 |
142 | "@mobile": {
143 | display: "flex",
144 | },
145 |
146 | "&:hover": {
147 | color: "$headerPrimary",
148 | },
149 | });
150 |
151 | const LogoWrapper = styled("div", {
152 | color: "$textNormal",
153 |
154 | marginTop: "-6px",
155 |
156 | "@mobile": {
157 | marginTop: "4px",
158 | },
159 | });
160 |
161 | const HamburgerStyles = css({
162 | width: "32px",
163 | height: "32px",
164 | });
165 |
166 | const LinkWrapper = styled("section", {
167 | display: "flex",
168 |
169 | "@mobile": {
170 | alignItems: "center",
171 | },
172 | });
173 |
174 | export interface SidebarItem {
175 | icon: string;
176 | label: string;
177 | href: string;
178 | onClick?: () => void;
179 | active?: boolean;
180 | }
181 |
182 | export interface SidebarSubheading {
183 | title: string;
184 | }
185 |
186 | interface SidebarProps {
187 | items: (SidebarItem | SidebarSubheading | ReactChild)[];
188 | }
189 |
190 | const Sidebar: FC = ({ items }) => {
191 | const [sidebarOpen, setSidebarOpen] = React.useState(false);
192 | const sidebarInnerRef = React.createRef();
193 | const [sidebarWidth, setSidebarWidth] = React.useState(0);
194 | const [isDragging, setIsDragging] = React.useState(false);
195 | const [isMobile, setIsMobile] = React.useState(false);
196 |
197 | const router = useRouter();
198 |
199 | const handleMediaCheck = (m: MediaQueryListEvent) => {
200 | setIsMobile(m.matches);
201 | };
202 |
203 | useEffect(() => {
204 | setSidebarOpen(false);
205 | }, [router.asPath]);
206 |
207 | useEffect(() => {
208 | if (sidebarInnerRef.current) {
209 | setSidebarWidth(sidebarInnerRef.current.clientWidth);
210 |
211 | window.onresize = (e) => {
212 | if (sidebarInnerRef.current) {
213 | setSidebarWidth(sidebarInnerRef.current.clientWidth);
214 | }
215 | };
216 | }
217 | }, [sidebarInnerRef]);
218 |
219 | useEffect(() => {
220 | const matchMedia = window.matchMedia(config.media.mobile);
221 | setIsMobile(matchMedia.matches);
222 |
223 | matchMedia.addEventListener("change", handleMediaCheck);
224 |
225 | return () => {
226 | matchMedia.removeEventListener("change", handleMediaCheck);
227 | };
228 | }, []);
229 |
230 | return (
231 | {
239 | const rect = sidebarInnerRef.current?.getBoundingClientRect();
240 |
241 | // check if we clicked anywhere but the sidebar
242 | if (
243 | rect &&
244 | sidebarOpen &&
245 | !(
246 | rect.top <= e.clientY &&
247 | e.clientY <= rect.bottom &&
248 | rect.left <= e.clientX &&
249 | e.clientX <= rect.right
250 | )
251 | ) {
252 | setSidebarOpen(false);
253 | }
254 | }}
255 | >
256 |
257 | setSidebarOpen(!sidebarOpen)}>
258 |
259 |
260 | router.push("/")}
265 | >
266 |
267 |
268 |
269 | ddocs.io
270 |
271 |
279 |
280 | setIsDragging(true)}
285 | onStop={(_, d) => {
286 | setIsDragging(false);
287 | if (d.x < sidebarWidth / 2) {
288 | setSidebarOpen(false);
289 | }
290 | }}
291 | axis="x"
292 | position={{
293 | x: isMobile ? (sidebarOpen ? sidebarWidth : 0) : 0,
294 | y: 0,
295 | }}
296 | defaultPosition={isMobile ? { x: 0, y: 0 } : { x: 0, y: 0 }}
297 | bounds={{
298 | right: sidebarWidth,
299 | left: 0,
300 | }}
301 | >
302 |
309 |
310 | {items.map((item) => {
311 | if (!item.hasOwnProperty("title")) {
312 | const { href, label, icon, onClick, active } =
313 | item as SidebarItem;
314 |
315 | const link = (
316 |
321 |
322 | {label}
323 |
324 | );
325 |
326 | if (href) {
327 | return (
328 |
329 | {link}
330 |
331 | );
332 | }
333 |
334 | return link;
335 | } else if (React.isValidElement(item)) {
336 | return item;
337 | } else {
338 | const { title } = item as SidebarSubheading;
339 |
340 | return (
341 |
342 | {title}
343 |
344 | );
345 | }
346 | })}
347 |
348 |
349 |
350 |
351 | );
352 | };
353 |
354 | export default Sidebar;
355 |
--------------------------------------------------------------------------------
/public/assets/images/clyde-triangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/public/assets/images/summaries-banner.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 | COmmunity ran docs for Discord.
633 | Copyright (C) 2022 ddocs.
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------