diff --git a/.gitignore b/.gitignore index e86fe25..6a2ce6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +users.json + # Logs logs *.log diff --git a/README.md b/README.md index ab607ee..c99da5f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ - + +# TPH Website + The official website for [The Programmer's Hangout](https://discord.gg/programming). ## Contributing @@ -40,7 +42,7 @@ Gatsby will take care of most of this process by creating a new page and positio 5. Run through the [setup steps](#setup) if possible to make sure your changes look ok. 6. Create a pull request. -#### Frontmatter +#### Frontmatter Frontmatter the optional metadata attached to every markdown file, like the list of authors or the creation date of the file. It is essentially just yaml syntax inside `---` delimiters within a markdown file. If you're not familiar with yaml you can find some refreshers [here](https://learnxinyminutes.com/docs/yaml/) @@ -49,7 +51,7 @@ An example frontmatter might look like this ```md --- authors: - - "@Xetera" + - "Xetera#0001" title: "Working with Lists in Elixir" created_at: 2020/01/30 external_resources: @@ -62,4 +64,3 @@ some content here ``` You can look at other resources like [this one](/src/content/docs/javascript/promises/intro.md) for reference when creating your own. We use [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) as the date format to confuse both Americans and Europeans an equal amount. - diff --git a/gatsby-node.js b/gatsby-node.js index 0978f39..307e7e4 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,28 +1,40 @@ const { graphql } = require("gatsby") const { createFilePath } = require("gatsby-source-filesystem") +const helper = require("./scripts/buildHelpers") const path = require(`path`) +const fs = require("fs") -const usersEndpoint = id => `https://discordapp.com/api/guilds/${id}/members` +const requiredArticleFrontmatter = ["authors", "date"] -const fetchUsers = () => { - // Fetch user data from discord using bot token here +let users = [] +try { + users = JSON.parse(fs.readFileSync("./users.json", "utf-8")) + console.log("(◕◡◕✿) Users list loaded!") +} catch (e) { + console.log("(◕‸ ◕✿) Users list not present, skipping user resolution...") +} + +const validateResourceArticle = node => { + const missingField = requiredArticleFrontmatter.find( + required => node.frontmatter[required] + ) + if (missingField) { + console.error( + `Article ${node.fileAbsolutePath} is missing field '${missingField}'` + ) + process.exit(1) + } } const createDocs = async ({ createPage, graphql }) => { const languageDocs = path.resolve(`src/templates/languagePost.tsx`) const result = await graphql(` - { + query FetchDocs { allFile(filter: { sourceInstanceName: { eq: "docs" } }) { edges { node { relativePath - childMarkdownRemark { - frontmatter { - author - date - } - } } } } @@ -47,11 +59,19 @@ const createDocs = async ({ createPage, graphql }) => { exports.onCreateNode = async ({ node, getNode, actions }) => { const { createPage, createNodeField } = actions if (node.internal.type === "MarkdownRemark") { - const slug = createFilePath({ node, getNode, basePath: "src/content/docs" }) + const { frontmatter } = node + const isDoc = Boolean(!frontmatter.path) + + if (!isDoc) { + return + } + + const authors = helper.resolveAuthors(users, frontmatter.authors) + createNodeField({ node, - name: "slug", - value: slug, + name: "authors", + value: authors, }) } } diff --git a/package-lock.json b/package-lock.json index 9b1622e..3903ddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5323,6 +5323,25 @@ } } }, + "discord.js": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.5.1.tgz", + "integrity": "sha512-tGhV5xaZXE3Z+4uXJb3hYM6gQ1NmnSxp9PClcsSAYFVRzH6AJH74040mO3afPDMWEAlj8XsoPXXTJHTxesqcGw==", + "requires": { + "long": "^4.0.0", + "prism-media": "^0.0.3", + "snekfetch": "^3.6.4", + "tweetnacl": "^1.0.0", + "ws": "^6.0.0" + }, + "dependencies": { + "tweetnacl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", + "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==" + } + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -10942,6 +10961,11 @@ "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.7.tgz", "integrity": "sha512-2SqUV6JH4f15Z5/7LVsyadSUwHhZppxhujgy/VhVqiRYMGt5oaocb7fV/3JGjHJ6rTuEIajnpTLGRz9cJW/c3g==" }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -13580,6 +13604,11 @@ "utila": "~0.4" } }, + "prism-media": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.3.tgz", + "integrity": "sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ==" + }, "prismjs": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.17.1.tgz", @@ -15428,6 +15457,11 @@ } } }, + "snekfetch": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz", + "integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw==" + }, "socket.io": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", diff --git a/package.json b/package.json index 26ddca5..fb8d4c7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@types/ramda": "^0.26.18", "axios": "^0.19.0", "babel-plugin-styled-components": "^1.10.6", + "discord.js": "^11.5.1", "gatsby": "^2.13.39", "gatsby-image": "^2.2.6", "gatsby-plugin-manifest": "^2.2.4", @@ -58,7 +59,8 @@ "start": "npm run develop", "serve": "gatsby serve", "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\"", - "codegen": "graphql-codegen --config codegen.yml" + "codegen": "graphql-codegen --config codegen.yml", + "fetch-users": "node ./scripts/fetchUsers.js" }, "repository": { "type": "git", diff --git a/scripts/buildHelpers.js b/scripts/buildHelpers.js new file mode 100644 index 0000000..6f86bfd --- /dev/null +++ b/scripts/buildHelpers.js @@ -0,0 +1,32 @@ +const DEFAULT_AVATAR_MODULO = 5 +module.exports = { + splitHash(identifier) { + const hashLocation = identifier.lastIndexOf("#") + + if (hashLocation === -1) { + throw Error(`Invalid name format ${identifier}`) + } + + const name = identifier.slice(0, hashLocation) + const hash = identifier.slice(hashLocation + 1) + return [name, hash] + }, + getDefaultAvatar(hash) { + const avatar = hash % DEFAULT_AVATAR_MODULO + return `https://cdn.discordapp.com/embed/avatars/${avatar}.png` + }, + resolveAuthor(users, author) { + const target = users.find(user => user.identifier === author) + const [name, hash] = this.splitHash(author) + const avatar = target ? target.avatar : this.getDefaultAvatar(hash) + + return { + name, + hash, + avatar, + } + }, + resolveAuthors(users, authors) { + return authors.map(author => this.resolveAuthor(users, author)) + }, +} diff --git a/scripts/fetchUsers.js b/scripts/fetchUsers.js new file mode 100644 index 0000000..dd725a3 --- /dev/null +++ b/scripts/fetchUsers.js @@ -0,0 +1,44 @@ +// This script will be ran periodically through CI +// don't touch it +const Discord = require("discord.js") +const fs = require("fs") +const { Readable, Writable } = require("stream") + +const client = new Discord.Client() + +const { BOT_TOKEN } = process.env +const TPH = "244230771232079873" +const DESTINATION = "./users.json" + +if (!BOT_TOKEN) { + throw Error("Missing BOT_TOKEN environment variable") +} + +const writeS = (content, dest) => { + const source = new Readable() + source.push(JSON.stringify(content)) + source.push(null) + return source.pipe(dest) +} + +client.once("ready", async () => { + console.log("Bot ready, fetching user list...") + const tph = client.guilds.get(TPH) + if (!tph) { + throw Error("Bot is not in TPH, cannot fetch users") + } + const guild = await tph.fetchMembers() + const memberInfo = guild.members.map(member => ({ + identifier: `${member.user.username}#${member.user.discriminator}`, + avatar: member.user.displayAvatarURL, + })) + const wrs = fs.createWriteStream(DESTINATION) + console.log(`Fetched ${memberInfo.length} users, writing to ${DESTINATION}`) + writeS(memberInfo, wrs).on("finish", () => { + console.log("Finished writing list") + process.exit(0) + }) +}) + +console.log("Attempting to log in...") +client.login(process.env.BOT_TOKEN) diff --git a/src/components/DiscordButton/index.tsx b/src/components/DiscordButton/index.tsx new file mode 100644 index 0000000..5a748e8 --- /dev/null +++ b/src/components/DiscordButton/index.tsx @@ -0,0 +1,14 @@ +import React from "react" + +function DiscordButton({ + lined = false, + children, +}: React.PropsWithChildren<{ lined: boolean }>) { + return ( + + ) +} + +export default DiscordButton diff --git a/src/components/DocsSidebar/index.tsx b/src/components/DocsSidebar/index.tsx index 81d7478..7dcab99 100644 --- a/src/components/DocsSidebar/index.tsx +++ b/src/components/DocsSidebar/index.tsx @@ -45,7 +45,8 @@ const ALL_DOCS = graphql` relativePath childMarkdownRemark { frontmatter { - author + authors + title date } } diff --git a/src/content/docs/javascript/callbacks/intro.md b/src/content/docs/javascript/callbacks/intro.md new file mode 100644 index 0000000..c490815 --- /dev/null +++ b/src/content/docs/javascript/callbacks/intro.md @@ -0,0 +1,10 @@ +--- +authors: + - "Xetera#0001" +created_at: 2019/07/27 +title: Callbacks +--- + +# Callbacks are everywhere + +If you've been doing javascript for any amount of time you'll have noticed that callbacks appear in just about every single piece of code. diff --git a/src/content/docs/javascript/iterative-vs-functional.md b/src/content/docs/javascript/iterative-vs-functional.md index 3aa35d5..a515781 100644 --- a/src/content/docs/javascript/iterative-vs-functional.md +++ b/src/content/docs/javascript/iterative-vs-functional.md @@ -1,6 +1,6 @@ --- authors: - - "@veksen" + - "veksen#1060" created_at: "2019/07/27" --- diff --git a/src/content/docs/javascript/promises/intro.md b/src/content/docs/javascript/promises/intro.md index 64b94db..a57e13e 100644 --- a/src/content/docs/javascript/promises/intro.md +++ b/src/content/docs/javascript/promises/intro.md @@ -1,6 +1,6 @@ --- authors: - - "@Xetera" + - "Xetera#0001" created_at: 2019/07/26 updated_at: 2019/07/26 title: Introduction to Promises diff --git a/src/content/docs/javascript/promises/simplifying-promises.md b/src/content/docs/javascript/promises/simplifying-promises.md index 261d1fb..ce4eff4 100644 --- a/src/content/docs/javascript/promises/simplifying-promises.md +++ b/src/content/docs/javascript/promises/simplifying-promises.md @@ -1,6 +1,6 @@ --- authors: - - "@Xetera" + - "Xetera#0001" created_at: "2019/07/26" --- diff --git a/src/content/docs/php/design-patterns/singleton.md b/src/content/docs/php/design-patterns/singleton.md index 1295460..1ee26a0 100644 --- a/src/content/docs/php/design-patterns/singleton.md +++ b/src/content/docs/php/design-patterns/singleton.md @@ -1,6 +1,6 @@ --- authors: - - "@supergrecko" + - "supergrecko#3434" created_at: "2019/07/27" --- diff --git a/src/content/docs/python/basics.md b/src/content/docs/python/basics.md index 8f768f1..4a9fb27 100644 --- a/src/content/docs/python/basics.md +++ b/src/content/docs/python/basics.md @@ -1,5 +1,6 @@ --- -author: "@Xetera" +authors: + - "Xetera#0001" date: 2019/06/26 --- diff --git a/src/templates/languagePost.tsx b/src/templates/languagePost.tsx index 4f83889..8689af2 100644 --- a/src/templates/languagePost.tsx +++ b/src/templates/languagePost.tsx @@ -22,8 +22,15 @@ export const query = graphql` file(relativePath: { eq: $file }) { post: childMarkdownRemark { html + fields { + authors { + name + hash + avatar + } + } frontmatter { - authors + date title } }