Compare commits

...

10 Commits

Author SHA1 Message Date
Chris Grimmett
94c10d07e1 re-add link2cid as NOT a submodule 2024-03-13 23:30:49 -08:00
Chris Grimmett
e8a223f8f9 remove blog 2024-02-27 13:29:49 -08:00
Chris Grimmett
abd7873ec7 debug msg 2024-02-27 11:53:57 -08:00
Chris Grimmett
cf674f7cc6 use docker build args 2024-02-27 10:49:43 -08:00
Chris Grimmett
09ffd5588c detect undefined env vars 2024-02-27 09:57:40 -08:00
Chris Grimmett
54991cd386 failsafe when contributors cant be fetched 2024-02-27 09:11:16 -08:00
Chris Grimmett
bd85718107 remove things stoppping a build 2024-02-27 07:52:43 -08:00
Chris Grimmett
4dd1531a9c switch back to dokku 2024-02-26 14:00:23 -08:00
Chris Grimmett
b56b694270 2024-02-05 deploy attempt 1 2024-02-05 14:13:02 -08:00
Chris Grimmett
59da569c96 use separate dockerfiles for each container 2024-02-02 18:31:57 -08:00
49 changed files with 2426 additions and 1024 deletions

3
.dokku/README.md Normal file
View File

@ -0,0 +1,3 @@
https://dokku.com/docs/advanced-usage/deployment-tasks/?h=monorepo#changing-the-appjson-location
https://dokku.com/docs/deployment/builders/dockerfiles/

8
.dokku/next.app.json Normal file
View File

@ -0,0 +1,8 @@
{
"scripts": {
"dokku": {
"predeploy": "echo hello-world-predeploy",
"postdeploy": "echo hello-world-postdeploy"
}
}
}

10
ARCHITECHTURE.md Normal file
View File

@ -0,0 +1,10 @@
git monorepo.
pnpm required for workspaces.
Yarn required for packages/strapi
Development uses docker compose with dotenv.
Production uses dokku.

View File

@ -1,19 +0,0 @@
FROM node:20-slim AS base
ENV NEXT_TELEMETRY_DISABLED 1
RUN corepack enable
FROM base AS build
WORKDIR /usr/src/fp-monorepo
RUN mkdir /usr/src/next
COPY ./pnpm-lock.yaml ./
COPY ./pnpm-workspace.yaml ./
COPY ./packages/next/package.json ./packages/next/
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store pnpm install
COPY . .
RUN pnpm deploy --filter=fp-next /usr/src/next
FROM base AS dev
WORKDIR /app
COPY --from=build /usr/src/next /app
CMD ["pnpm", "run", "dev"]

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# futureporn-monorepo

View File

@ -1,103 +0,0 @@
version: '3.4'
services:
link2cid:
container_name: fp-link2cid
image: insanity54/link2cid:latest
ports:
- "3939:3939"
environment:
API_KEY: ${LINK2CID_API_KEY}
IPFS_URL: "http://ipfs0:5001"
ipfs0:
container_name: fp-ipfs0
image: ipfs/kubo:release
ports:
- "5001:5001"
volumes:
- ./packages/ipfs0:/data/ipfs
cluster0:
container_name: fp-cluster0
image: ipfs/ipfs-cluster:latest
depends_on:
- ipfs0
environment:
CLUSTER_PEERNAME: cluster0
CLUSTER_SECRET: ${CLUSTER_SECRET} # From shell variable if set
CLUSTER_IPFSHTTP_NODEMULTIADDRESS: /dns4/ipfs0/tcp/5001
CLUSTER_CRDT_TRUSTEDPEERS: '*' # Trust all peers in Cluster
CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS: /ip4/0.0.0.0/tcp/9094 # Expose API
CLUSTER_RESTAPI_BASICAUTHCREDENTIALS: ${CLUSTER_RESTAPI_BASICAUTHCREDENTIALS}
CLUSTER_MONITORPINGINTERVAL: 2s # Speed up peer discovery
ports:
- "127.0.0.1:9094:9094"
volumes:
- ./packages/cluster0:/data/ipfs-cluster
strapi:
container_name: fp-strapi
image: elestio/strapi-development
depends_on:
- db
environment:
ADMIN_PASSWORD: ${STRAPI_ADMIN_PASSWORD}
ADMIN_EMAIL: ${STRAPI_ADMIN_EMAIL}
BASE_URL: ${STRAPI_BASE_URL}
SMTP_HOST: 172.17.0.1
SMTP_PORT: 25
SMTP_AUTH_STRATEGY: NONE
SMTP_FROM_EMAIL: sender@email.com
DATABASE_CLIENT: postgres
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
JWT_SECRET: ${STRAPI_JWT_SECRET}
ADMIN_JWT_SECRET: ${STRAPI_ADMIN_JWT_SECRET}
APP_KEYS: ${STRAPI_APP_KEYS}
NODE_ENV: development
DATABASE_HOST: db
API_TOKEN_SALT: ${STRAPI_API_TOKEN_SALT}
TRANSFER_TOKEN_SALT: ${STRAPI_TRANSFER_TOKEN_SALT}
ports:
- "1337:1337"
volumes:
- ./packages/strapi/config:/opt/app/config
- ./packages/strapi/src:/opt/app/src
# - ./packages/strapi/package.json:/opt/package.json
# - ./packages/strapi/yarn.lock:/opt/yarn.lock
- ./packages/strapi/.env:/opt/app/.env
- ./packages/strapi/public/uploads:/opt/app/public/uploads
# - ./packages/strapi/entrypoint.sh:/opt/app/entrypoint.sh
next:
container_name: fp-next
build:
context: ./packages/next
dockerfile: Dockerfile
environment:
REVALIDATION_TOKEN: ${NEXT_REVALIDATION_TOKEN}
NODE_ENV: production
ports:
- "3000:3000"
volumes:
- ./packages/next/
db:
container_name: fp-db
image: postgres:latest
restart: always
environment:
POSTGRES_DB: ${DATABASE_NAME}
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
PGDATA: /var/lib/postgresql/data
volumes:
- ./packages/db/pgdata:/var/lib/postgresql/data
ports:
- "5433:5432"

View File

@ -14,12 +14,18 @@ services:
link2cid: link2cid:
container_name: fp-link2cid container_name: fp-link2cid
restart: on-failure restart: on-failure
image: insanity54/link2cid:latest build:
context: ./packages/link2cid
dockerfile: Dockerfile
target: dev
ports: ports:
- "3939:3939" - "3939:3939"
environment: environment:
API_KEY: ${LINK2CID_API_KEY} API_KEY: ${LINK2CID_API_KEY}
IPFS_URL: "http://ipfs0:5001" IPFS_URL: "http://ipfs0:5001"
PORT: 3939
volumes:
- ./packages/link2cid/index.js:/app/index.js
ipfs0: ipfs0:
container_name: fp-ipfs0 container_name: fp-ipfs0

32
next.Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM node:20-slim AS base
# Install dependencies only when needed
FROM base AS deps
RUN corepack enable
FROM deps AS build
ARG NEXT_PUBLIC_SITE_URL=foo
ARG NEXT_PUBLIC_STRAPI_URL=foo
ARG NEXT_PUBLIC_UPPY_COMPANION_URL=foo
ENV NEXT_PUBLIC_SITE_URL ${NEXT_PUBLIC_SITE_URL}
ENV NEXT_PUBLIC_STRAPI_URL ${NEXT_PUBLIC_STRAPI_URL}
ENV NEXT_PUBLIC_UPPY_COMPANION_URL ${NEXT_PUBLIC_UPPY_COMPANION_URL}
WORKDIR /usr/src/app
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store pnpm install
RUN pnpm run -r build
RUN pnpm deploy --filter=fp-next /app
FROM deps AS dev
WORKDIR /app
COPY --from=build /usr/src/app /app
CMD ["pnpm", "run", "dev"]
FROM deps AS next
WORKDIR /app
COPY --from=build /usr/src/app /app
CMD ["pnpm", "start"]

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "futureporn",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"kompose": "kompose convert --file compose.yml -c --out ./charts",
"deploy:bot": "echo @todo",
"deploy:next": "git push origin:next main",
"deploy:link2cid": "echo @todo",
"deploy:strapi": "echo @todo",
"deploy:uppy": "echo @todo",
"deploy": "echo @todo"
},
"keywords": [],
"author": "@cj_clippy",
"license": "CC0-1.0"
}

1
packages/bot/.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/iron

19
packages/bot/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app
RUN corepack enable
FROM base AS build
COPY ./packages/bot/package.json ./
COPY ./packages/bot/src ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install
FROM build AS dev
ENTRYPOINT ["pnpm"]
CMD ["run", "dev"]
FROM build AS run
ENTRYPOINT ["pnpm"]
CMD ["start"]

106
packages/bot/components.js Normal file
View File

@ -0,0 +1,106 @@
const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
if(msg.content.includes("!component")) {
await client.rest.channels.createMessage(msg.channelID, {
content: `Here's some buttons for you, ${msg.author.mention}.`,
components: [
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
customID: "some-string-you-will-see-later",
label: "Click!",
disabled: false, // If the button is disabled, false by default.
},
{
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY,
customID: "some-other-string",
label: "This Is Disabled",
disabled: true
},
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.LINK,
label: "Open Link",
url: "https://docs.oceanic.ws"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.STRING_SELECT,
customID: "string-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
options: [
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
{
default: true, // If this option is selected by default
description: "The description of the option", // Optional description
label: "Option One",
value: "value-1"
},
{
label: "Option Two",
value: "option-2"
}
],
placeholder: "Some Placeholder Text"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.CHANNEL_SELECT,
channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
customID: "channel-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
placeholder: "Some Placeholder Text"
}
]
}
]
});
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

94
packages/bot/embeds.js Normal file
View File

@ -0,0 +1,94 @@
const { Client } = require("oceanic.js");
const { readFileSync } = require("fs");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
if(msg.content.includes("!embed")) {
console.log(`'!embeds' was seen in chat!`)
console.log(msg)
await client.rest.channels.createMessage(msg.channelID, {
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
// Up to 10 in one message
embeds: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedAuthorOptions.html
author: {
name: "Author Name",
// An image url, or attachment://filename.ext
iconURL: "https://i.furry.cool/DonPride.png", // Optional
url: "https://docs.oceanic.ws" // Optional
},
// Array of https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedField.html
// Up to 25 in one message
fields: [
{
name: "Field One",
value: "Field One Value",
inline: true // If this field should be displayed inline (default: false)
},
{
name: "Field Two",
value: "Field Two Value",
inline: false
}
],
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedFooterOptions.html
footer: {
text: "Footer Text",
// An image url, or attachment://filename.ext
iconURL: "https://i.furry.cool/DonPride.png" // Optional
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedImageOptions.html
image: {
// An image url, or attachment://filename.ext
url: "https://i.furry.cool/DonPride.png"
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedThumbnailOptions.html
thumbnail: {
// An image url, or attachment://filename.ext
url: "https://i.furry.cool/DonPride.png"
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
color: 0xFFA500, // Base-10 color (0x prefix can be used for hex codes)
description: "My Cool Embed",
timestamp: new Date().toISOString(), // The current time - ISO 8601 format
title: "My Amazing Embed",
url: "https://docs.oceanic.ws"
}
]
});
} else if(msg.content.includes("!file")) {
await client.rest.channels.createMessage(msg.channelID, {
embeds: [
{
image: {
// This can also be used for author & footer images
url: "attachment://image.png"
}
}
],
files: [
{
name: "image.png",
contents: readFileSync(`${__dirname}/image.png`)
}
]
});
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

261
packages/bot/futurebutt.js Normal file
View File

@ -0,0 +1,261 @@
const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
console.log(msg.content)
if(msg.content.includes("!test")) {
await client.rest.channels.createMessage(msg.channelID, {
content: `HGERE IZ BUTTN'z 5 u, ${msg.author.mention}.`,
components: [
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
customID: "some-string-you-will-see-later",
label: "Click!",
disabled: false, // If the button is disabled, false by default.
},
{
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY,
customID: "some-other-string",
label: "This Is Disabled",
disabled: true
},
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.LINK,
label: "Open Link",
url: "https://docs.oceanic.ws"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.STRING_SELECT,
customID: "string-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
options: [
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
{
default: true, // If this option is selected by default
description: "The description of the option", // Optional description
label: "Option One",
value: "value-1"
},
{
label: "Option Two",
value: "option-2"
}
],
placeholder: "Some Placeholder Text"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.CHANNEL_SELECT,
channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
customID: "channel-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
placeholder: "Some Placeholder Text"
}
]
}
]
});
}
});
client.on("interactionCreate", async(interaction) => {
console.log(`interaction!@`)
console.log(interaction)
switch(interaction.type) {
// https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
case InteractionTypes.APPLICATION_COMMAND: {
// defer interactions as soon as possible, you have three seconds to send any initial response
// if you wait too long, the interaction may be invalidated
await interaction.defer();
// If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
// await interaction.defer(MessageFlags.EPHEMERAL);
// data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
switch(interaction.data.type) {
// Chat Input commands are what you use in the chat, i.e. slash commands
case ApplicationCommandTypes.CHAT_INPUT: {
if(interaction.data.name === "greet") {
// assume we have two options, user (called user) then string (called greeting) - first is required, second is not
// Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
// Setting the second parameter to true will throw an error if the option is not present
const user = interaction.data.options.getUser("user", true);
const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
// since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
// we can only have one initial response, so we use createFollowup
await interaction.createFollowup({
content: `${greeting} ${user.mention}!`,
allowedMentions: {
users: [user.id]
}
});
}
// Chat Input application command interactions also have a set of resolved data, which is structured as so:
// https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
// the options wrapper pulls values out of resolved automatically, if you use the right method
break;
}
// User application commands are shown in the context menu when right-clicking on users
// `data` will have a target (and targetID) property with the user that the command was executed on
// These don't have options
case ApplicationCommandTypes.USER: {
if(interaction.data.name === "ping") {
await interaction.createFollowup({
content: `Pong! ${interaction.data.target.mention}`,
allowedMentions: {
users: [interaction.data.target.id]
}
});
}
break;
}
// Message application commands are shown in the context menu when right-clicking on messages
// `data` will have a target (and targetID) property with the message that the command was executed on
// Same as user commands, these don't have options
case ApplicationCommandTypes.MESSAGE: {
if(interaction.data.name === "author") {
await interaction.createFollowup({
content: `${interaction.data.target.author.mention} is the author of that message!`,
allowedMentions: {
users: [interaction.data.target.author.id]
}
});
}
break;
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
case InteractionTypes.MESSAGE_COMPONENT: {
// same spiel as above
await interaction.defer();
// when you create a message with components, this will correspond with what you provided as the customID there
if(interaction.data.componentType === ComponentTypes.BUTTON) {
if(interaction.data.customID === "edit-message") {
// Edits the original message. This has an initial response variant: editParent
await interaction.editOriginal({
content: `This message was edited by ${interaction.user.mention}!`,
allowedMentions: {
users: [interaction.user.id]
}
});
} else if(interaction.data.customID === "my-amazing-button") {
await interaction.createFollowup({
content: "You clicked an amazing button!"
});
}
} else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
// The `values` property under data contains all the selected values
await interaction.createFollowup({
content: `You selected: **${interaction.data.values.join("**, **")}**`
});
}
break;
}
// https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
// Autocomplete Interactions cannot be deferred
switch(interaction.data.name) {
case "test-autocomplete": {
// Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
// along with one at the end, which will have focused
// Setting the first parameter to true will throw an error if no focused option is present
const option = interaction.data.options.getFocused(true);
switch(option.name) {
case "test-option": {
return interaction.result([
{
name: "Choice 1",
nameLocalizations: {
"es-ES": "Opción 1"
},
value: "choice-1"
},
{
name: "Choice 2",
nameLocalizations: {
"es-ES": "Opción 2"
},
value: "choice-2"
}
]);
break;
}
}
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
case InteractionTypes.MODAL_SUBMIT: {
// this will correspond with the customID you provided when creating the modal
switch(interaction.data.customID) {
case "test-modal": {
// the `components` property under data contains all the components that were submitted
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
console.log(interaction.data.components);
break;
}
}
break;
}
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

View File

@ -0,0 +1,171 @@
const { Client, InteractionTypes, MessageFlags, ComponentTypes, ApplicationCommandTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: 0 // No intents are needed if you are only using interactions
}
});
client.on("ready", async() => {
console.log("Ready as", client.user.tag);
});
client.on("interactionCreate", async(interaction) => {
switch(interaction.type) {
// https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
case InteractionTypes.APPLICATION_COMMAND: {
// defer interactions as soon as possible, you have three seconds to send any initial response
// if you wait too long, the interaction may be invalidated
await interaction.defer();
// If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
// await interaction.defer(MessageFlags.EPHEMERAL);
// data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
switch(interaction.data.type) {
// Chat Input commands are what you use in the chat, i.e. slash commands
case ApplicationCommandTypes.CHAT_INPUT: {
if(interaction.data.name === "greet") {
// assume we have two options, user (called user) then string (called greeting) - first is required, second is not
// Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
// Setting the second parameter to true will throw an error if the option is not present
const user = interaction.data.options.getUser("user", true);
const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
// since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
// we can only have one initial response, so we use createFollowup
await interaction.createFollowup({
content: `${greeting} ${user.mention}!`,
allowedMentions: {
users: [user.id]
}
});
}
// Chat Input application command interactions also have a set of resolved data, which is structured as so:
// https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
// the options wrapper pulls values out of resolved automatically, if you use the right method
break;
}
// User application commands are shown in the context menu when right-clicking on users
// `data` will have a target (and targetID) property with the user that the command was executed on
// These don't have options
case ApplicationCommandTypes.USER: {
if(interaction.data.name === "ping") {
await interaction.createFollowup({
content: `Pong! ${interaction.data.target.mention}`,
allowedMentions: {
users: [interaction.data.target.id]
}
});
}
break;
}
// Message application commands are shown in the context menu when right-clicking on messages
// `data` will have a target (and targetID) property with the message that the command was executed on
// Same as user commands, these don't have options
case ApplicationCommandTypes.MESSAGE: {
if(interaction.data.name === "author") {
await interaction.createFollowup({
content: `${interaction.data.target.author.mention} is the author of that message!`,
allowedMentions: {
users: [interaction.data.target.author.id]
}
});
}
break;
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
case InteractionTypes.MESSAGE_COMPONENT: {
// same spiel as above
await interaction.defer();
// when you create a message with components, this will correspond with what you provided as the customID there
if(interaction.data.componentType === ComponentTypes.BUTTON) {
if(interaction.data.customID === "edit-message") {
// Edits the original message. This has an initial response variant: editParent
await interaction.editOriginal({
content: `This message was edited by ${interaction.user.mention}!`,
allowedMentions: {
users: [interaction.user.id]
}
});
} else if(interaction.data.customID === "my-amazing-button") {
await interaction.createFollowup({
content: "You clicked an amazing button!"
});
}
} else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
// The `values` property under data contains all the selected values
await interaction.createFollowup({
content: `You selected: **${interaction.data.values.join("**, **")}**`
});
}
break;
}
// https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
// Autocomplete Interactions cannot be deferred
switch(interaction.data.name) {
case "test-autocomplete": {
// Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
// along with one at the end, which will have focused
// Setting the first parameter to true will throw an error if no focused option is present
const option = interaction.data.options.getFocused(true);
switch(option.name) {
case "test-option": {
return interaction.result([
{
name: "Choice 1",
nameLocalizations: {
"es-ES": "Opción 1"
},
value: "choice-1"
},
{
name: "Choice 2",
nameLocalizations: {
"es-ES": "Opción 2"
},
value: "choice-2"
}
]);
break;
}
}
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
case InteractionTypes.MODAL_SUBMIT: {
// this will correspond with the customID you provided when creating the modal
switch(interaction.data.customID) {
case "test-modal": {
// the `components` property under data contains all the components that were submitted
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
console.log(interaction.data.components);
break;
}
}
break;
}
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

27
packages/bot/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "fp-bot",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node --import=tsx --watch ./src/index.ts"
},
"keywords": [],
"author": "",
"license": "CC0-1.0",
"engines": {
"node": "v20.x.x",
"npm": ">=6.x.x"
},
"devDependencies": {
"tsx": "^4.7.0"
},
"dependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"discordeno": "^18.0.1",
"express": "^4.18.2",
"oceanic.js": "^1.9.0"
}
}

View File

@ -0,0 +1,28 @@
import { getBotIdFromToken, Intents } from 'discordeno';
/** The bot id, derived from the bot token. */
export const BOT_ID = getBotIdFromToken(process.env.DISCORD_TOKEN as string);
export const EVENT_HANDLER_URL = `http://${process.env.EVENT_HANDLER_HOST}:${process.env.EVENT_HANDLER_PORT}`;
export const REST_URL = `http://${process.env.REST_HOST}:${process.env.REST_PORT}`;
export const GATEWAY_URL = `http://${process.env.GATEWAY_HOST}:${process.env.GATEWAY_PORT}`;
// Gateway Proxy Configurations
/** The gateway intents you would like to use. */
export const INTENTS: Intents =
// SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
Intents.DirectMessageReactions |
Intents.DirectMessageTyping |
Intents.DirectMessages |
Intents.GuildBans |
Intents.GuildEmojis |
Intents.GuildIntegrations |
Intents.GuildInvites |
Intents.GuildMembers |
Intents.GuildMessageReactions |
Intents.GuildMessageTyping |
Intents.GuildMessages |
Intents.GuildPresences |
Intents.GuildVoiceStates |
Intents.GuildWebhooks |
Intents.Guilds;

58
packages/bot/src/index.ts Normal file
View File

@ -0,0 +1,58 @@
import { BASE_URL, createRestManager } from 'discordeno';
import express, { Request, Response } from 'express';
// import { setupAnalyticsHooks } from '../analytics.js';
import { REST_URL } from './configs.js';
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
const REST_PORT = process.env.REST_PORT as string;
const rest = createRestManager({
token: DISCORD_TOKEN,
secretKey: REST_AUTHORIZATION,
customUrl: REST_URL,
debug: console.log,
});
// Add send fetching analytics hook to rest
// setupAnalyticsHooks(rest);
// @ts-expect-error
rest.convertRestError = (errorStack, data) => {
if (!data) return { message: errorStack.message };
return { ...data, message: errorStack.message };
};
const app = express();
app.use(
express.urlencoded({
extended: true,
}),
);
app.use(express.json());
app.all('/*', async (req: Request, res: Response) => {
if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
return res.status(401).json({ error: 'Invalid authorization key.' });
}
try {
const result = await rest.runMethod(rest, req.method, `${BASE_URL}${req.url}`, req.body);
if (result) {
res.status(200).json(result);
} else {
res.status(204).json();
}
} catch (error: any) {
console.log(error);
res.status(500).json(error);
}
});
app.listen(REST_PORT, () => {
console.log(`REST listening at ${REST_URL}`);
});

View File

@ -0,0 +1,9 @@
# REST Proxy
This folder will contain the code for our REST proxy. This is going to become the single source that all of our bot will
use to communciate to the Discord API.
## Further Steps
- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
bloated framework. Feel free to optimize to a better framework.

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"importHelpers": true,
"allowUnusedLabels": false,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"stripInternal": true,
"noFallthroughCasesInSwitch": true,
"useUnknownInCatchVariables": false,
"allowUnreachableCode": false,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["./src/**/*", ".env"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node",
"swc": true
}
}

144
packages/link2cid/.gitignore vendored Normal file
View File

@ -0,0 +1,144 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node

View File

@ -0,0 +1,20 @@
# Reference-- https://pnpm.io/docker
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY ./package.json /app
EXPOSE 3939
FROM base AS dev
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install
CMD ["pnpm", "run", "dev"]
FROM base
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod
COPY ./index.js /app
ENTRYPOINT ["pnpm"]
CMD ["start"]

View File

@ -0,0 +1,90 @@
# link2cid
## Motivation
I wish I could give [kubo](https://github.com/ipfs/kubo) or [IPFS cluster](https://ipfscluster.io/) a URI to a file and then they would download the file and add to ipfs, returning me a [CID](https://docs.ipfs.tech/concepts/glossary/#cid).
However, neither [kubo](https://github.com/ipfs/kubo) nor [IPFS cluster](https://ipfscluster.io/) can do this.
link2cid solves this issue with a REST API for adding a file at `url` to IPFS.
## Usage
Configure environment
Create a `.env` file. See `.env.example` for an example. Important environment variables are `API_KEY`, `PORT`, and `IPFS_URL`.
Install and run
```bash
pnpm install
pnpm start
```
Make a GET REST request to `/add` with `url` as a query parameter. Expect a [SSE](https://wikipedia.org/wiki/Server-sent_events) response.
## dokku
dokku builder-dockerfile:set link2cid dockerfile-path link2cid.Dockerfile
### Examples
#### [HTTPIE](https://httpie.io)
```bash
http -A bearer -a $API_KEY --stream 'http://localhost:3939/add?url=https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png' Accept:text/event-stream
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream; charset=utf-8
Date: Thu, 21 Dec 2023 11:20:24 GMT
Transfer-Encoding: identity
X-Powered-By: Express
:ok
event: dlProgress
data: {
"percent": 100
}
event: addProgress
data: {
"percent": 100
}
event: end
data: {
"cid": "bafkreidj3jo7efguloaixz6vgivljlmowagagjtqv4yanyqgty2hrvg6km"
}
```
#### Javascript
@todo this is incomplete/untested
```js
await fetch('http://localhost:3939/add?url=https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png', {
headers: {
'accept': 'text/event-stream',
'authorization': `Bearer ${API_KEY}`
}
});
```
## Dev notes
### Generate API_KEY
```js
require('crypto').randomBytes(64).toString('hex')
```
### `TypeError: data.split is not a function`
If you see this error, make sure data in SSE event payload is a string, not a number.

284
packages/link2cid/index.js Normal file
View File

@ -0,0 +1,284 @@
'use strict';
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const fs = require('fs');
const fsp = require('fs/promises');
const { openAsBlob } = require('node:fs');
const { rm, stat } = require('fs/promises');
const os = require('os');
const path = require('path');
const SseStream = require('ssestream').default;
const { Transform, Readable } = require('node:stream');
const { pipeline } = require('node:stream/promises');
const { differenceInSeconds } = require('date-fns');
const cidRegex = /Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}/;
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// environment variables
const port = process.env.PORT || 3000;
const ipfsUrl = process.env.IPFS_URL || 'http://localhost:5001';
if (!process.env.API_KEY) throw new Error('API_KEY was missing in env');
// greetz https://stackoverflow.com/a/51302466/1004931
async function downloadFile(url, filePath, sse) {
console.log(`downloading url=${url} to filePath=${filePath}`);
const res = await fetch(url);
const fileSize = res.headers.get('content-length');
const fileStream = fs.createWriteStream(filePath, { flags: 'wx' });
let downloadedBytes = 0;
const logInterval = 1 * 1024 * 1024; // 1MB in bytes
const progressLogger = new Transform({
transform(chunk, encoding, callback) {
downloadedBytes += chunk.length;
if (downloadedBytes % logInterval < chunk.length) {
console.log(`${downloadedBytes / (1024 * 1024)} MB processed`);
const progress = (downloadedBytes / fileSize) * 100;
console.log(`Download Progress: ${progress.toFixed(2)}%`);
sse.write({
event: 'dlProgress',
data: `${Math.floor(progress)}`
});
}
this.push(chunk);
callback();
}
});
await pipeline(
res.body,
progressLogger,
fileStream
)
console.log('download finished');
// verify the file
// If we don't, we get text error messages sent to kubo which gets added and it's a bad time.
console.log(`fileSize=${fileSize}. downloadedBytes=${downloadedBytes}`);
if (fileSize != downloadedBytes) throw new Error('downloadedBytes did not match fileSize');
}
async function healthRes(_, res) {
const version = await getPackageVersion();
res.json({ error: false, message: `*link2cid ${version} pisses on the floor*` });
}
async function getPackageVersion() {
const packageJsonFile = await fsp.readFile(path.join(__dirname, 'package.json'), { encoding: 'utf-8' });
const json = JSON.parse(packageJsonFile);
return json.version;
}
/**
*
* We use this to upload files and get progress notifications
*
*/
async function streamingPostFetch(
url,
formData,
basename,
sse,
filesize
) {
console.log(`streamingPostFetch with url=${url}, formData=${formData.get('file')}, basename=${basename}, sse=${sse}, filesize=${filesize}`);
try {
const res = await fetch(url, {
method: 'POST',
body: formData
});
if (!res.ok) {
throw new Error(`HTTP error! Status-- ${res.status}`);
}
const reader = res.body?.getReader();
if (!reader) {
throw new Error('Failed to get reader from response body');
}
while (true) {
const { done, value } = await reader.read();
const chunk = new TextDecoder().decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
const trimmedLine = line.trim()
if (!!trimmedLine) {
console.log(trimmedLine);
const json = JSON.parse(trimmedLine);
// console.log(`comparing json.Name=${json.Name} with basename=${basename}`);
sse.write({
event: 'addProgress',
data: `${Math.floor(json?.Size / filesize * 100)}`
})
if (json.Name === basename && json.Hash && json.Size) {
// this is the last chunk
return json;
}
}
}
if (done) {
throw new Error('Response reader finished before receiving a CID which indicates a failiure.');
}
}
} catch (error) {
console.error('An error occurred:', error);
throw error;
}
}
function authenticate(req, res, next) {
const apiKey = req.query?.token;
if (!apiKey) {
const msg = `authorization 'token' was missing from query`;
console.error(msg);
return res.status(401).json({ error: true, message: msg });
}
if (apiKey !== process.env.API_KEY) {
const msg = 'INCORRECT API_KEY (wrong token)';
console.error(msg);
return res.status(403).json({ error: true, message: msg });
} else {
next();
}
}
async function getFormStuff(filePath) {
const url = `${ipfsUrl}/api/v0/add?progress=false&cid-version=1&pin=true`;
const blob = await openAsBlob(filePath);
const basename = path.basename(filePath);
const filesize = (await stat(filePath)).size;
const formData = new FormData();
return {
url,
blob,
basename,
filesize,
formData
}
}
/**
* Add a file from URL to IPFS.
*
* uses SSE to send progress reports as the script
* downloads the file to disk and then does `ipfs add`
* finally returning a CID
*
* events:
* - heartbeat
* - dlProgress
* - addProgress
* - end
*/
async function addHandler(req, res) {
console.log(`/add`)
let url;
const urlStr = req.query.url;
if (!urlStr) return res.status(400).json({
error: 'url was missing from query'
});
try {
url = new URL(urlStr);
} catch (e) {
return res.status(400).json({
error: e?.message
})
}
const timestamp = new Date().valueOf();
const fileName = `${timestamp}-${url.pathname.split('/').at(-1)}`;
const destinationFilePath = path.join(os.tmpdir(), fileName);
console.log(`fileName=${fileName}, destinationFilePath=${destinationFilePath}`);
const sse = new SseStream(req);
sse.pipe(res);
let hbStartTime = new Date();
const heartbeat = setInterval(() => {
sse.write({
event: 'heartbeat',
data: `${differenceInSeconds(new Date(), hbStartTime)}`
});
}, 15000);
res.on('close', () => {
console.log('Connection closed.');
clearTimeout(heartbeat);
sse.unpipe(res);
});
console.log(`Downloading '${urlStr}' to destinationFilePath=${destinationFilePath}`);
await downloadFile(urlStr, destinationFilePath, sse);
sse.write({
event: 'dlProgress',
data: '100'
})
console.log(`'ipfs add' the file ${destinationFilePath}`);
const { url: kuboUrl, blob, basename, filesize, formData } = await getFormStuff(destinationFilePath);
formData.append('file', blob, basename);
let cid;
try {
const output = await streamingPostFetch(kuboUrl, formData, basename, sse, filesize);
console.log(`streamingPostFetch output as follows.`);
console.log(output);
if (!output?.Hash) throw new Error('No CID was received from remote IPFS node.');
if (!output?.Size) throw new Error(`'ipfs add' was missing Size in its output.`);
// if (output.Size !== filesize) throw new Error(`input and output sizes did not match. Expected output.Size ${output.Size} to equal ${filesize}.`);
// console.log(`filesize=${filesize} output.Size=${output.Size}`);
cid = output.Hash;
console.log('cleanup');
await rm(destinationFilePath);
console.log('end SSE');
clearTimeout(heartbeat);
} catch (e) {
return sse.end({
event: 'end',
error: true,
message: e
})
}
return sse.end({
event: 'end',
data: cid
})
}
app.get('/', authenticate, healthRes);
app.get('/health', healthRes);
app.get('/add', authenticate, addHandler);
app.listen(port, async () => {
const version = await getPackageVersion();
console.log(`link2cid ${version} listening on port ${port}`);
});

View File

@ -0,0 +1,30 @@
{
"name": "link2cid",
"version": "3.2.0",
"description": "REST API for adding files to IPFS",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "pnpm nodemon ./index.js",
"start": "node index.js"
},
"engines": {
"node": ">=20.0.0"
},
"keywords": [],
"author": "",
"license": "Unlicense",
"dependencies": {
"@types/express": "^4.17.21",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"date-fns": "^3.0.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"ssestream": "^1.1.0"
},
"devDependencies": {
"nodemon": "^3.0.3"
}
}

691
packages/link2cid/pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,691 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
body-parser:
specifier: ^1.20.2
version: 1.20.2
cors:
specifier: ^2.8.5
version: 2.8.5
date-fns:
specifier: ^3.0.5
version: 3.0.5
dotenv:
specifier: ^16.3.1
version: 16.3.1
express:
specifier: ^4.18.2
version: 4.18.2
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
ssestream:
specifier: ^1.1.0
version: 1.1.0
packages:
/@types/body-parser@1.19.5:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:
'@types/connect': 3.4.38
'@types/node': 20.10.5
dev: false
/@types/connect@3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
dependencies:
'@types/node': 20.10.5
dev: false
/@types/express-serve-static-core@4.17.41:
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
dependencies:
'@types/node': 20.10.5
'@types/qs': 6.9.10
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
dev: false
/@types/express@4.17.21:
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 4.17.41
'@types/qs': 6.9.10
'@types/serve-static': 1.15.5
dev: false
/@types/http-errors@2.0.4:
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
dev: false
/@types/mime@1.3.5:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
dev: false
/@types/mime@3.0.4:
resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==}
dev: false
/@types/node@20.10.5:
resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==}
dependencies:
undici-types: 5.26.5
dev: false
/@types/qs@6.9.10:
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
dev: false
/@types/range-parser@1.2.7:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
dev: false
/@types/send@0.17.4:
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
dependencies:
'@types/mime': 1.3.5
'@types/node': 20.10.5
dev: false
/@types/serve-static@1.15.5:
resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==}
dependencies:
'@types/http-errors': 2.0.4
'@types/mime': 3.0.4
'@types/node': 20.10.5
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
dev: false
/array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.1
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/body-parser@1.20.2:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: false
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/call-bind@1.0.5:
resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
dependencies:
function-bind: 1.1.2
get-intrinsic: 1.2.2
set-function-length: 1.1.1
dev: false
/content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: false
/content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
dev: false
/cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
dependencies:
object-assign: 4.1.1
vary: 1.1.2
dev: false
/date-fns@3.0.5:
resolution: {integrity: sha512-Q4Tq5c5s/Zl/zbgdWf6pejn9ru7UwdIlLfvEEg1hVsQNQ7LKt76qIduagIT9OPK7+JCv1mAKherdU6bOqGYDnw==}
dev: false
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: false
/define-data-property@1.1.1:
resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
engines: {node: '>= 0.4'}
dependencies:
get-intrinsic: 1.2.2
gopd: 1.0.1
has-property-descriptors: 1.0.1
dev: false
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: false
/destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/dotenv@16.3.1:
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
engines: {node: '>=12'}
dev: false
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
dev: false
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
/encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: false
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: false
/express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.1
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.5.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
proxy-addr: 2.0.7
qs: 6.11.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: false
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: false
/get-intrinsic@1.2.2:
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
dependencies:
function-bind: 1.1.2
has-proto: 1.0.1
has-symbols: 1.0.3
hasown: 2.0.0
dev: false
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
get-intrinsic: 1.2.2
dev: false
/has-property-descriptors@1.0.1:
resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==}
dependencies:
get-intrinsic: 1.2.2
dev: false
/has-proto@1.0.1:
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
engines: {node: '>= 0.4'}
dev: false
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/hasown@2.0.0:
resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
engines: {node: '>= 0.4'}
dependencies:
function-bind: 1.1.2
dev: false
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
dev: false
/jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.5.4
dev: false
/jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
dev: false
/jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
dev: false
/lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false
/lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
dev: false
/lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
dev: false
/lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
dev: false
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: false
/lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
dev: false
/lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
dev: false
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: false
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: false
/merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: false
/methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: false
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: false
/object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: false
/on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: false
/proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
dev: false
/qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: false
/raw-body@2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 1.0.2
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
dev: false
/set-function-length@1.1.1:
resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
engines: {node: '>= 0.4'}
dependencies:
define-data-property: 1.1.1
get-intrinsic: 1.2.2
gopd: 1.0.1
has-property-descriptors: 1.0.1
dev: false
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.5
get-intrinsic: 1.2.2
object-inspect: 1.13.1
dev: false
/ssestream@1.1.0:
resolution: {integrity: sha512-UOS3JTuGqGEOH89mfHFwVOJNH2+JX9ebIWuw6WBQXpkVOxbdoY3RMliSHzshL4XVYJJrcul5NkuvDFCzgYu1Lw==}
dev: false
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: false
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: false
/utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: false
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: false
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false

View File

@ -1,54 +0,0 @@
import Link from "next/link"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"
export default async function Page() {
return (
<div className="content">
<div className="box">
<div className="block">
<h1>The Story of Futureporn</h1>
<p>2020 was a busy time for me. I started a small business, attended lots of support group meetings, and rode my bicycle more than ever before. I often found myself away from home during times when Melody was streaming on Chaturbate.</p>
<p>You probably know that unlike other video streaming platforms, Chaturbate doesnt store any VODs. When I missed a stream, I felt sad. I felt like I had missed out and theres no way Id ever find out what happened.</p>
<p>Im pretty handy with computer software. Creating programs and websites has been my biggest passion for my entire life. In order to never miss a ProjektMelody livestream again, I resolved to create some software that would automatically record Melodys Chaturbate streams.</p>
<p>I put the project on hold for a few months, because I didnt think I could make a website that could handle the traffic that the Science Team would generate.</p>
<p>I couldnt shake the idea, though. I wanted Futureporn to exist no matter what!</p>
<p>Ive been working on this project off and on for about a year and a half. Its gone through several iterations, and each iteration has taught me something new. Right now, the website is usable for finding and downloading ProjektMelody Chaturbate VODs. Every VOD has a link to Melodys tweet which originally announced the stream, and a title/description derived from said tweet. I have archived all of her known Chaturbate streams.</p>
<p>The project has evolved over time. Originally, I wanted to have a place to go when I missed one of Melodys livestreams. Now, the project is becoming a sort of a time capsule. Weve all seen how Melody has been de-platformed a half dozen times, and Ive taken this to heart. Platforms are a problem for data preservation! This is one of the reasons for why I chose to use the Inter-Planetary File System (<Link target="_blank" href="https://ipfs.io/">IPFS<FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>.)</p>
<p>IPFS can end 404s through pinning, a way of mirroring a file across several different computers. Its a way for computers to work together to serve content instead of working independently, thus gaining redundancy and performance benefits. I see a future where pinning files on IPFS becomes as easy as pinning a photo on Pinterest. Fans of ProjektMelody can pin the VODs on Futureporn, increasing that VODs replication and servability to future viewers.</p>
<p>But wait, theres more! I have been thinking about a bunch of other stuff that could be done with past VODs. I think the most exciting thing would be to use computer vision to parse Melodys vibrator activity from the video, and export to a data file. This data file could be used to send good vibes to a viewers vibrator in-sync with VOD playback. Feel what Melody feels! Very exciting, very sexy! This is a long-term goal for Futureporn.</p>
<p>I have several goals for Futureporn, as listed on the <Link href="/goals">Goals page</Link>. A bunch of them have to do with increasing video playback performance, user interface design, but theres a few that are pretty eccentric Serving ProjektMelody VODs to Mars, for example!</p>
<p>I hope this site is useful to all the Science Team!</p>
<article className="mt-5 message is-success">
<div className="message-body">
<p>Futureporn needs financial support to continue improving. If you enjoy this website, please consider <Link target="_blank" href="https://patreon.com/CJ_Clippy">becoming a patron<FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></Link>.</p>
</div>
</article>
</div>
</div>
</div>
)
}

View File

@ -1,35 +0,0 @@
import Link from 'next/link';
import { siteUrl } from '@/lib/constants';
import { IBlogPost } from '@/lib/blog';
export default async function PostsPage() {
const res = await fetch(`${siteUrl}/api/blogs`);
const posts: IBlogPost[] = [
{
id: 1,
slug: '2021-10-29-the-story-of-futureporn',
title: 'The Story Of Futureporn'
}
]
return (
<div className="container mb-5">
<div className="content mb-5">
<h1>All Blog Posts</h1>
<hr style={{ width: '220px' }} />
<div style={{ paddingTop: '40px' }}>
{posts.map((post: IBlogPost) => (
<article key={post.slug}>
<Link href={`/blog/${post.slug}`}>
<h2>&gt; {post.title}</h2>
</Link>
</article>
))}
</div>
</div>
</div>
);
}

View File

@ -6,18 +6,19 @@ export interface IArchiveProgressProps {
} }
export default async function ArchiveProgress ({ vtuber }: IArchiveProgressProps) { export default async function ArchiveProgress ({ vtuber }: IArchiveProgressProps) {
const streams = await getAllStreamsForVtuber(vtuber.id); // const streams = await getAllStreamsForVtuber(vtuber.id);
const goodStreams = await getAllStreamsForVtuber(vtuber.id, ['good']); // const goodStreams = await getAllStreamsForVtuber(vtuber.id, ['good']);
const issueStreams = await getAllStreamsForVtuber(vtuber.id, ['issue']); // const issueStreams = await getAllStreamsForVtuber(vtuber.id, ['issue']);
const totalStreams = streams.length; // const totalStreams = streams.length;
const eligibleStreams = issueStreams.length+goodStreams.length; // const eligibleStreams = issueStreams.length+goodStreams.length;
// Check if totalStreams is not zero before calculating completedPercentage // // Check if totalStreams is not zero before calculating completedPercentage
const completedPercentage = (totalStreams !== 0) ? Math.round(eligibleStreams / totalStreams * 100) : 0; // const completedPercentage = (totalStreams !== 0) ? Math.round(eligibleStreams / totalStreams * 100) : 0;
return ( // return (
<div> // <div>
<p className="heading">{eligibleStreams}/{totalStreams} Streams Archived ({completedPercentage}%)</p> // <p className="heading">{eligibleStreams}/{totalStreams} Streams Archived ({completedPercentage}%)</p>
<progress className="progress is-success" value={eligibleStreams} max={totalStreams}>{completedPercentage}%</progress> // <progress className="progress is-success" value={eligibleStreams} max={totalStreams}>{completedPercentage}%</progress>
</div> // </div>
) // )
return "@todo"
} }

View File

@ -1,125 +0,0 @@
'use client';
// greets https://github.com/wa0x6e/cal-heatmap-react-starter/blob/main/src/components/cal-heatmap.tsx
import CalHeatmap from 'cal-heatmap';
// @ts-ignore cal-heatmap is jenk
import Legend from 'cal-heatmap/plugins/Legend';
// @ts-ignore cal-heatmap is jenk
import Tooltip from 'cal-heatmap/plugins/Tooltip';
import { DataRecord } from 'cal-heatmap/src/options/Options';
import 'cal-heatmap/cal-heatmap.css';
import dayjs from 'dayjs';
import { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { getSafeDate } from '@/lib/dates';
export interface ICalProps {
data: DataRecord[];
slug: string;
}
export function Cal({ data, slug }: ICalProps) {
const router = useRouter();
const [cellSize, setCellSize] = useState(13);
const [targetElementId, setTargetElementId] = useState('');
const generateUniqueId = () => {
return `cal-${Math.random().toString(36).substring(2, 9)}`;
};
useEffect(() => {
const updateCellSize = () => {
const windowWidth = window.innerWidth;
if (windowWidth > 1400) {
setCellSize(15); // Adjust the cell size for width > 1400px
} else if (windowWidth > 730) {
setCellSize(10); // Adjust the cell size for width > 730px
} else {
setCellSize(3); // Adjust the cell size for width <= 730px
}
}
updateCellSize();
// Event listener to update cell size on window resize
window.addEventListener('resize', updateCellSize);
return () => {
window.removeEventListener('resize', updateCellSize);
};
}, [])
useEffect(() => {
setTargetElementId(generateUniqueId());
}, []);
useEffect(() => {
if (!targetElementId) return;
const cal = new CalHeatmap();
// @ts-ignore
cal.on('click', (
event: string,
timestamp: number,
value: number
) => {
router.push(`/vt/${slug}/stream/${getSafeDate(new Date(timestamp))}`);
// console.log(`slug=${slug} safeDate=${getSafeDate(new Date(timestamp))}`);
});
cal.paint(
{
itemSelector: `#${targetElementId}`,
scale: {
color: {
// @ts-ignore this shit is straight from the example website
domain: ['missing', 'issue', 'good'],
type: 'ordinal',
range: ['red', 'yellow', 'green']
}
},
theme: 'dark',
verticalOrientation: false,
data: {
source: data,
x: 'date',
y: 'value',
// @ts-ignore this shit is straight from the example website
groupY: d => d[0]
},
range: 12,
date: { start: data[0].date },
domain: {
type: 'month',
gutter: 4,
label: { text: 'MMM', textAlign: 'start', position: 'top' }
},
subDomain: {
type: 'ghDay',
radius: 2,
width: cellSize,
height: cellSize,
gutter: 4,
}
}, [
[
Tooltip,
{
text: ((ts: number, value: string, dayjsDate: dayjs.Dayjs) => {
return `${!!value ? value+' - '+dayjsDate.toString() : dayjsDate.toString() }`;
})
}
]
]);
}, [targetElementId, data, cellSize, router, slug]);
return (
<>
<div id={targetElementId}></div>
</>
)
}

View File

@ -5,6 +5,7 @@ import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default async function Contributors() { export default async function Contributors() {
try {
const contributors = await getContributors(); const contributors = await getContributors();
if (!contributors || contributors.length < 1) return ( if (!contributors || contributors.length < 1) return (
<SkeletonTheme baseColor="#000" highlightColor="#000" width="25%"> <SkeletonTheme baseColor="#000" highlightColor="#000" width="25%">
@ -30,4 +31,10 @@ export default async function Contributors() {
return ( return (
<>{contributorList}</> <>{contributorList}</>
) )
} catch (e) {
if (e instanceof Error) {
console.error(e)
}
return <p>Failed to fetch contributor list</p>
}
} }

View File

@ -25,7 +25,6 @@ export default function Footer() {
<li><Link href="/tags">Tags</Link></li> <li><Link href="/tags">Tags</Link></li>
<li><Link href="/feed">RSS Feed</Link></li> <li><Link href="/feed">RSS Feed</Link></li>
<li><Link href="/api">API</Link></li> <li><Link href="/api">API</Link></li>
<li><Link href="/blog">Blog</Link></li>
<li><Link href="https://status.futureporn.net/" target="_blank"><span className="mr-1">Status</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fab fa-external-link-alt"></FontAwesomeIcon></Link></li> <li><Link href="https://status.futureporn.net/" target="_blank"><span className="mr-1">Status</span><FontAwesomeIcon icon={faExternalLinkAlt} className="fab fa-external-link-alt"></FontAwesomeIcon></Link></li>
<li><Link href="/upload">Upload</Link></li> <li><Link href="/upload">Upload</Link></li>
<li><Link href="/profile">Profile</Link></li> <li><Link href="/profile">Profile</Link></li>

View File

@ -11,7 +11,7 @@ export default function Pager({ baseUrl, page, pageCount }: IPagerProps): React.
const getPagePath = (page: any): string => { const getPagePath = (page: any): string => {
const pageNumber = parseInt(page); const pageNumber = parseInt(page);
console.log(`pageNumber=${pageNumber}`) console.log(`pageNumber=${pageNumber}`);
return `${baseUrl}/${pageNumber}`; return `${baseUrl}/${pageNumber}`;
}; };

View File

@ -83,7 +83,7 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
if (authData?.accessToken) { if (authData?.accessToken) {
setIsAuthed(true); setIsAuthed(true);
} }
}, [isAuthed]); }, [isAuthed, authData]);
async function getRandomSuggestions() { async function getRandomSuggestions() {

View File

@ -253,7 +253,7 @@ export default function UploadForm({ vtubers }: IUploadFormProps) {
{...register('vtuber')} {...register('vtuber')}
> >
{vtubers.map((vtuber: IVtuber) => ( {vtubers.map((vtuber: IVtuber) => (
<option value={vtuber.id}>{vtuber.attributes.displayName}</option> <option key={vtuber.id} value={vtuber.id}>{vtuber.attributes.displayName}</option>
))} ))}
</select> </select>
</div> </div>

View File

@ -10,8 +10,8 @@ import LinkableHeading from './linkable-heading';
export function getVodTitle(vod: IVod): string { export function getVodTitle(vod: IVod): string {
console.log('lets getVodTitle, ey?') // console.log('lets getVodTitle, ey?')
console.log(JSON.stringify(vod, null, 2)) // console.log(JSON.stringify(vod, null, 2))
return vod.attributes.title || vod.attributes.announceTitle || (vod.attributes?.date2 && vod.attributes?.vtuber?.data?.attributes?.displayName) ? `${vod.attributes.vtuber.data.attributes.displayName} ${vod.attributes.date2}` : `VOD ${vod.id}`; return vod.attributes.title || vod.attributes.announceTitle || (vod.attributes?.date2 && vod.attributes?.vtuber?.data?.attributes?.displayName) ? `${vod.attributes.vtuber.data.attributes.displayName} ${vod.attributes.date2}` : `VOD ${vod.id}`;
} }

View File

@ -1,11 +1,9 @@
import Tes from '@/assets/svg/tes';
export default async function Page() { export default async function Page() {
return ( return (
<div className="content"> <div className="content">
<div className="box"> <div className="box">
<h2>Healthy!</h2> <h2>Healthy!</h2>
<Tes></Tes>
</div> </div>
</div> </div>
) )

View File

@ -1,5 +0,0 @@
export interface IBlogPost {
slug: string;
title: string;
id: number;
}

View File

@ -1,7 +1,10 @@
// export const strapiUrl = (process.env.NODE_ENV === 'production') ? 'https://portal.futureporn.net' : 'https://chisel.sbtp:1337' if (!process.env.NEXT_PUBLIC_SITE_URL) throw new Error('NEXT_PUBLIC_SITE_URL was missing in env');
// export const siteUrl = (process.env.NODE_ENV === 'production') ? 'https://futureporn.net' : 'http://localhost:3000' if (!process.env.NEXT_PUBLIC_STRAPI_URL) throw new Error('NEXT_PUBLIC_STRAPI_URL was missing in env');
export const siteUrl = process.env.NEXT_PUBLIC_SITE_URL if (!process.env.NEXT_PUBLIC_UPPY_COMPANION_URL) throw new Error('NEXT_PUBLIC_UPPY_COMPANION_URL undefined in env');
export const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_URL
export const companionUrl = ''+process.env.NEXT_PUBLIC_UPPY_COMPANION_URL
export const siteUrl = ''+process.env.NEXT_PUBLIC_SITE_URL
export const strapiUrl = ''+process.env.NEXT_PUBLIC_STRAPI_URL
export const patreonSupporterBenefitId: string = '4760169' export const patreonSupporterBenefitId: string = '4760169'
export const patreonQuantumSupporterId: string = '10663202' export const patreonQuantumSupporterId: string = '10663202'
export const patreonVideoAccessBenefitId: string = '13462019' export const patreonVideoAccessBenefitId: string = '13462019'

View File

@ -80,8 +80,8 @@ export async function getGoals(pledgeSum: number): Promise<IGoals | null> {
const funded = filterAndSortGoals(openData.concat(closedData), true); const funded = filterAndSortGoals(openData.concat(closedData), true);
const unfunded = filterAndSortGoals(openData.concat(closedData), false); const unfunded = filterAndSortGoals(openData.concat(closedData), false);
console.log('the following are unfunded goals') // console.log('the following are unfunded goals')
console.log(unfunded) // console.log(unfunded)
return { return {
complete: closedData, complete: closedData,

View File

@ -275,7 +275,7 @@ export async function getAllStreamsForVtuber(vtuberId: number, archiveStatuses =
}); });
console.log(`strapiUrl=${strapiUrl}`) console.log(`strapiUrl=${strapiUrl}`)
const response = await fetch(`${strapiUrl}/api/streams?${query}`, fetchStreamsOptions); const response = await fetch(`${strapiUrl}/api/streams?${query}`, fetchStreamsOptions)
if (response.status !== 200) { if (response.status !== 200) {
// If the response status is not 200 (OK), consider it a network failure // If the response status is not 200 (OK), consider it a network failure

View File

@ -120,7 +120,7 @@ export async function getAllVtubers(): Promise<IVtuber[] | null> {
}); });
try { try {
console.log(`Getting /api/vtubers page=${currentPage}`); console.log(`Getting ${strapiUrl}/api/vtubers page=${currentPage}`);
const response = await fetch(`${strapiUrl}/api/vtubers?${query}`, fetchVtubersOptions); const response = await fetch(`${strapiUrl}/api/vtubers?${query}`, fetchVtubersOptions);
if (!response.ok) { if (!response.ok) {

View File

@ -5,8 +5,8 @@ import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3'; import AwsS3 from '@uppy/aws-s3';
import RemoteSources from '@uppy/remote-sources'; import RemoteSources from '@uppy/remote-sources';
import { useAuth } from './components/auth'; import { useAuth } from './components/auth';
import { companionUrl } from '@/lib/constants';
const companionUrl = process.env.NEXT_PUBLIC_UPPY_COMPANION_URL!
export const UppyContext = createContext(new Uppy()); export const UppyContext = createContext(new Uppy());

View File

@ -1,70 +0,0 @@
import { getVtuberBySlug } from '@/lib/vtubers';
import { getAllStreamsForVtuber } from '@/lib/streams';
import NotFound from '../not-found';
import { DataRecord } from 'cal-heatmap/src/options/Options';
import { Cal } from '@/components/cal';
interface IPageProps {
params: {
slug: string;
};
}
function getArchiveStatusValue(archiveStatus: string): number {
if (archiveStatus === 'good') return 2;
if (archiveStatus === 'issue') return 1;
else return 0 // missing
}
function sortDataRecordsByDate(records: DataRecord[]) {
return records.sort((a, b) => {
if (typeof a.date === 'string' && typeof b.date === 'string') {
return a.date.localeCompare(b.date);
} else {
// Handle comparison when date is not a string (e.g., when it's a number)
// For instance, you might want to convert numbers to strings or use a different comparison logic.
// Example assuming number to string conversion:
return String(a.date).localeCompare(String(b.date));
}
});
}
export default async function Page({ params: { slug } }: IPageProps) {
const vtuber = await getVtuberBySlug(slug);
if (!vtuber) return <NotFound></NotFound>
const streams = await getAllStreamsForVtuber(vtuber.id);
const streamsByYear: { [year: string]: DataRecord[] } = {};
streams.forEach((stream) => {
const date = new Date(stream.attributes.date);
const year = date.getFullYear();
if (!streamsByYear[year]) {
streamsByYear[year] = [];
}
streamsByYear[year].push({
date: new Date(stream.attributes.date).toISOString(),
value: stream.attributes.archiveStatus,
});
});
// Sort the data records within each year's array
for (const year in streamsByYear) {
streamsByYear[year] = sortDataRecordsByDate(streamsByYear[year]);
}
return (
<div>
{Object.keys(streamsByYear).map((year) => {
return (
<div key={year} className='section'>
<h2 className='title'>{year}</h2>
{/* <pre><code>{JSON.stringify(streamsByYear[year], null, 2)}</code></pre> */}
<Cal slug={slug} data={streamsByYear[year]} />
</div>
)
})}
</div>
)
}

View File

@ -30,217 +30,218 @@ export default async function Page({ params }: { params: { slug: string } }) {
const vods = await getVodsForVtuber(vtuber.id, 1, 9); const vods = await getVodsForVtuber(vtuber.id, 1, 9);
if (!vods) notFound(); if (!vods) notFound();
const missingStreams = await getAllStreamsForVtuber(vtuber.id, ['missing']); return '@todo'
const issueStreams = await getAllStreamsForVtuber(vtuber.id, ['issue']); // const missingStreams = await getAllStreamsForVtuber(vtuber.id, ['missing']);
const goodStreams = await getAllStreamsForVtuber(vtuber.id, ['good']); // const issueStreams = await getAllStreamsForVtuber(vtuber.id, ['issue']);
// const goodStreams = await getAllStreamsForVtuber(vtuber.id, ['good']);
// return ( // // return (
// <> // // <>
// <p>hi mom!</p> // // <p>hi mom!</p>
// <pre> // // <pre>
// <code> // // <code>
// {JSON.stringify(missingStreams, null, 2)} // // {JSON.stringify(missingStreams, null, 2)}
// </code> // // </code>
// </pre> // // </pre>
// </> // // </>
// ) // // )
return ( // return (
<> // <>
{vtuber && ( // {vtuber && (
<> // <>
<div className="box"> // <div className="box">
<div className="columns is-multiline"> // <div className="columns is-multiline">
<div className="column is-full"> // <div className="column is-full">
<h1 className="title is-2">{vtuber.attributes.displayName}</h1> // <h1 className="title is-2">{vtuber.attributes.displayName}</h1>
</div> // </div>
<div className="column is-one-quarter"> // <div className="column is-one-quarter">
<figure className="image is-rounded is-1by1"> // <figure className="image is-rounded is-1by1">
<Image // <Image
className="is-rounded" // className="is-rounded"
alt={vtuber.attributes.displayName} // alt={vtuber.attributes.displayName}
src={vtuber.attributes.image} // src={vtuber.attributes.image}
fill={true} // fill={true}
placeholder='blur' // placeholder='blur'
blurDataURL={vtuber.attributes.imageBlur} // blurDataURL={vtuber.attributes.imageBlur}
/> // />
</figure> // </figure>
</div> // </div>
<div className="column is-three-quarters"> // <div className="column is-three-quarters">
<p className="is-size-5 mb-3">{vtuber.attributes.description1}</p> // <p className="is-size-5 mb-3">{vtuber.attributes.description1}</p>
<p className="is-size-5">{vtuber.attributes.description2}</p> // <p className="is-size-5">{vtuber.attributes.description2}</p>
</div> // </div>
</div> // </div>
<h2 id="socials" className="title is-3"> // <h2 id="socials" className="title is-3">
<Link href="#socials">Socials</Link> // <Link href="#socials">Socials</Link>
</h2> // </h2>
<div className="column is-full mb-5"> // <div className="column is-full mb-5">
<div className="columns has-text-centered is-multiline is-centered"> // <div className="columns has-text-centered is-multiline is-centered">
{vtuber.attributes.patreon && ( // {vtuber.attributes.patreon && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link href={vtuber.attributes.patreon} className="subtitle is-5"> // <Link href={vtuber.attributes.patreon} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faPatreon} className="fab fa-patreon" /></span><span className="mr-2">Patreon</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faPatreon} className="fab fa-patreon" /></span><span className="mr-2">Patreon</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.twitter && ( // {vtuber.attributes.twitter && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.twitter} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.twitter} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faXTwitter} className="fab fa-x-twitter" /></span><span className="mr-2">Twitter</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faXTwitter} className="fab fa-x-twitter" /></span><span className="mr-2">Twitter</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.youtube && ( // {vtuber.attributes.youtube && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.youtube} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.youtube} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faYoutube} className="fab fa-youtube" /></span><span className="mr-2">YouTube</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faYoutube} className="fab fa-youtube" /></span><span className="mr-2">YouTube</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.twitch && ( // {vtuber.attributes.twitch && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.twitch} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.twitch} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faTwitch} className="fab fa-twitch" /></span><span className="mr-2">Twitch</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faTwitch} className="fab fa-twitch" /></span><span className="mr-2">Twitch</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.tiktok && ( // {vtuber.attributes.tiktok && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.tiktok} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.tiktok} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faTiktok} className="fab fa-tiktok" /></span><span className="mr-2">TikTok</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faTiktok} className="fab fa-tiktok" /></span><span className="mr-2">TikTok</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.fansly && ( // {vtuber.attributes.fansly && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.fansly} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.fansly} className='subtitle is-5'>
<span className='mr-2'><FanslyIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Fansly</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><FanslyIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Fansly</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.onlyfans && ( // {vtuber.attributes.onlyfans && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.onlyfans} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.onlyfans} className='subtitle is-5'>
<span className='mr-2'> // <span className='mr-2'>
<OnlyfansIcon width={20} height={20} className={styles.icon}></OnlyfansIcon> // <OnlyfansIcon width={20} height={20} className={styles.icon}></OnlyfansIcon>
</span><span className="mr-2">OnlyFans</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // </span><span className="mr-2">OnlyFans</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.pornhub && ( // {vtuber.attributes.pornhub && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.pornhub} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.pornhub} className='subtitle is-5'>
<span className='mr-2'><PornhubIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Pornhub</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><PornhubIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Pornhub</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.reddit && ( // {vtuber.attributes.reddit && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.reddit} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.reddit} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faReddit} className="fab fa-reddit" /></span><span className="mr-2">Reddit</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faReddit} className="fab fa-reddit" /></span><span className="mr-2">Reddit</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.discord && ( // {vtuber.attributes.discord && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.discord} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.discord} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faDiscord} className="fab fa-discord" /></span><span className="mr-2">Discord</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faDiscord} className="fab fa-discord" /></span><span className="mr-2">Discord</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.instagram && ( // {vtuber.attributes.instagram && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.instagram} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.instagram} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faInstagram} className="fab fa-instagram" /></span><span className="mr-2">Instagram</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faInstagram} className="fab fa-instagram" /></span><span className="mr-2">Instagram</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.facebook && ( // {vtuber.attributes.facebook && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.facebook} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.facebook} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faFacebook} className="fab fa-facebook" /></span><span className="mr-2">Facebook</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faFacebook} className="fab fa-facebook" /></span><span className="mr-2">Facebook</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.merch && ( // {vtuber.attributes.merch && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.merch} className="subtitle is-5"> // <Link target="_blank" href={vtuber.attributes.merch} className="subtitle is-5">
<span className="mr-2"><FontAwesomeIcon icon={faBagShopping} className="fas fa-bag-shopping" /></span><span className="mr-2">Merch</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className="mr-2"><FontAwesomeIcon icon={faBagShopping} className="fas fa-bag-shopping" /></span><span className="mr-2">Merch</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.chaturbate && ( // {vtuber.attributes.chaturbate && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.chaturbate} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.chaturbate} className='subtitle is-5'>
<span className='mr-2'><ChaturbateIcon width={20} className={styles.icon}/></span><span className="mr-2">Chaturbate</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><ChaturbateIcon width={20} className={styles.icon}/></span><span className="mr-2">Chaturbate</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.throne && ( // {vtuber.attributes.throne && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.throne} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.throne} className='subtitle is-5'>
<span className='mr-2'><ThroneIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Throne</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><ThroneIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Throne</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.linktree && ( // {vtuber.attributes.linktree && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.linktree} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.linktree} className='subtitle is-5'>
<span className='mr-2'><LinktreeIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Linktree</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><LinktreeIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Linktree</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
{vtuber.attributes.carrd && ( // {vtuber.attributes.carrd && (
<div className="column is-3 is-narrow"> // <div className="column is-3 is-narrow">
<Link target="_blank" href={vtuber.attributes.carrd} className='subtitle is-5'> // <Link target="_blank" href={vtuber.attributes.carrd} className='subtitle is-5'>
<span className='mr-2'><CarrdIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Carrd</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span> // <span className='mr-2'><CarrdIcon width={20} height={20} className={styles.icon}/></span><span className="mr-2">Carrd</span><span><FontAwesomeIcon icon={faExternalLinkAlt} className="fas fa-external-link-alt" /></span>
</Link> // </Link>
</div> // </div>
)} // )}
</div> // </div>
</div> // </div>
{/* <h2 id="toys" className="title is-3"> // {/* <h2 id="toys" className="title is-3">
<Link href="#toys">Toys</Link> // <Link href="#toys">Toys</Link>
</h2> // </h2>
<> // <>
<ToysList vtuber={vtuber} toys={toys} page={1} pageSize={toySampleCount} /> // <ToysList vtuber={vtuber} toys={toys} page={1} pageSize={toySampleCount} />
{(toys.pagination.total > toySampleCount) && <Link href={`/vt/${vtuber.slug}/toys/1`} className='button mb-5'>See all of {vtuber.displayName}'s toys</Link>} // {(toys.pagination.total > toySampleCount) && <Link href={`/vt/${vtuber.slug}/toys/1`} className='button mb-5'>See all of {vtuber.displayName}'s toys</Link>}
</> */} // </> */}
<h2 id="vods" className="title is-3"> // <h2 id="vods" className="title is-3">
<Link href="#vods">Vods</Link> // <Link href="#vods">Vods</Link>
</h2> // </h2>
<VodsList vtuber={vtuber} vods={vods.data} page={1} pageSize={9} /> // <VodsList vtuber={vtuber} vods={vods.data} page={1} pageSize={9} />
{ // {
(vtuber.attributes.vods) ? ( // (vtuber.attributes.vods) ? (
<Link className='button mb-5' href={`/vt/${vtuber.attributes.slug}/vods/1`}>See all {vtuber.attributes.displayName} vods</Link> // <Link className='button mb-5' href={`/vt/${vtuber.attributes.slug}/vods/1`}>See all {vtuber.attributes.displayName} vods</Link>
) : (<p className='section'><i>No VODs have been added, yet.</i></p>) // ) : (<p className='section'><i>No VODs have been added, yet.</i></p>)
} // }
<h2 id="streams" className='title is-3'> // <h2 id="streams" className='title is-3'>
<Link href="#streams">Streams</Link> // <Link href="#streams">Streams</Link>
</h2> // </h2>
<StreamsCalendar missingStreams={missingStreams} issueStreams={issueStreams} goodStreams={goodStreams} /> // <StreamsCalendar missingStreams={missingStreams} issueStreams={issueStreams} goodStreams={goodStreams} />
{/* // {/*
<h2 id="progress" className='title is-3'> // <h2 id="progress" className='title is-3'>
<Link href="#progress">Archive Progress</Link> // <Link href="#progress">Archive Progress</Link>
</h2> // </h2>
<ArchiveProgress vtuber={vtuber} /> */} // <ArchiveProgress vtuber={vtuber} /> */}
</div> // </div>
</> // </>
)} // )}
</> // </>
); // );
} }

View File

@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react"
const SvgComponent = (props) => ( const SvgComponent = (props: any) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve" xmlSpace="preserve"

View File

@ -43,7 +43,6 @@
"@uppy/remote-sources": "^1.1.0", "@uppy/remote-sources": "^1.1.0",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"bulma-prefers-dark": "0.1.0-beta.1", "bulma-prefers-dark": "0.1.0-beta.1",
"cal-heatmap": "^4.2.4",
"date-fns": "^2.0.0", "date-fns": "^2.0.0",
"date-fns-tz": "^2.0.0", "date-fns-tz": "^2.0.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",

View File

@ -41,8 +41,7 @@
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"dist/types/**/*.ts", "dist/types/**/*.ts",
".next/types/**/*.ts", ".next/types/**/*.ts"
"assets/svg/tes.jsx"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"

373
pnpm-lock.yaml generated
View File

@ -28,28 +28,6 @@ importers:
specifier: ^4.7.0 specifier: ^4.7.0
version: 4.7.0 version: 4.7.0
packages/futurebot:
dependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/node':
specifier: ^20.11.0
version: 20.11.5
discordeno:
specifier: ^18.0.1
version: 18.0.1
dotenv:
specifier: ^16.3.1
version: 16.3.1
express:
specifier: ^4.18.2
version: 4.18.2
devDependencies:
tsx:
specifier: ^4.7.0
version: 4.7.0
packages/next: packages/next:
dependencies: dependencies:
'@fortawesome/fontawesome-free': '@fortawesome/fontawesome-free':
@ -151,9 +129,6 @@ importers:
bulma-prefers-dark: bulma-prefers-dark:
specifier: 0.1.0-beta.1 specifier: 0.1.0-beta.1
version: 0.1.0-beta.1 version: 0.1.0-beta.1
cal-heatmap:
specifier: ^4.2.4
version: 4.2.4
date-fns: date-fns:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.30.0 version: 2.30.0
@ -1211,7 +1186,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
espree: 9.6.1 espree: 9.6.1
globals: 13.24.0 globals: 13.24.0
ignore: 5.3.0 ignore: 5.3.0
@ -1348,7 +1323,7 @@ packages:
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
dependencies: dependencies:
'@humanwhocodes/object-schema': 2.0.2 '@humanwhocodes/object-schema': 2.0.2
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
minimatch: 3.1.2 minimatch: 3.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1721,25 +1696,12 @@ packages:
fastq: 1.16.0 fastq: 1.16.0
dev: true dev: true
/@observablehq/plot@0.6.13:
resolution: {integrity: sha512-ebQS4ENodOy+O3WUjhqv9jNPZENAZRQMIdO3ziOlAKfUzSf69+gaFAqqc04SGrQA6JwJjPYnbfeN3YIpNsCF/A==}
engines: {node: '>=12'}
dependencies:
d3: 7.8.5
interval-tree-1d: 1.0.4
isoformat: 0.2.1
dev: false
/@paralleldrive/cuid2@2.2.2: /@paralleldrive/cuid2@2.2.2:
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
dependencies: dependencies:
'@noble/hashes': 1.3.3 '@noble/hashes': 1.3.3
dev: false dev: false
/@popperjs/core@2.11.8:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@react-hookz/deep-equal@1.0.4: /@react-hookz/deep-equal@1.0.4:
resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==} resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==}
dev: false dev: false
@ -2478,7 +2440,7 @@ packages:
'@typescript-eslint/types': 6.18.1 '@typescript-eslint/types': 6.18.1
'@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.18.1 '@typescript-eslint/visitor-keys': 6.18.1
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
eslint: 8.56.0 eslint: 8.56.0
typescript: 5.3.3 typescript: 5.3.3
transitivePeerDependencies: transitivePeerDependencies:
@ -2509,7 +2471,7 @@ packages:
dependencies: dependencies:
'@typescript-eslint/types': 6.18.1 '@typescript-eslint/types': 6.18.1
'@typescript-eslint/visitor-keys': 6.18.1 '@typescript-eslint/visitor-keys': 6.18.1
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.3 minimatch: 9.0.3
@ -3159,10 +3121,6 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'} engines: {node: '>=8'}
/binary-search-bounds@2.0.5:
resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==}
dev: false
/bintrees@1.0.2: /bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
dev: false dev: false
@ -3334,20 +3292,6 @@ packages:
responselike: 2.0.1 responselike: 2.0.1
dev: false dev: false
/cal-heatmap@4.2.4:
resolution: {integrity: sha512-TTNoQTRxHXrttOEbkraKv9vy2VpfQIwVLQJTlAfcBusQK9qrBL/UBO+WloAxv2yrR+P8URA2cuXEdc5iztER9g==}
dependencies:
'@observablehq/plot': 0.6.13
'@popperjs/core': 2.11.8
d3-color: 3.1.0
d3-fetch: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dayjs: 1.11.10
eventemitter3: 5.0.1
lodash-es: 4.17.21
dev: false
/call-bind@1.0.5: /call-bind@1.0.5:
resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
dependencies: dependencies:
@ -3462,11 +3406,6 @@ packages:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
dev: false dev: false
/commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
dev: false
/common-tags@1.8.2: /common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
@ -3577,254 +3516,6 @@ packages:
resolution: {integrity: sha512-tI+NjVRS485QlSxHeM3JIjdEZSJMLOZVp41/vvWukDmIkZSoYG9gLYl9GFZGBpY42UbRVo1eMlF7XkI/IiDHzQ==} resolution: {integrity: sha512-tI+NjVRS485QlSxHeM3JIjdEZSJMLOZVp41/vvWukDmIkZSoYG9gLYl9GFZGBpY42UbRVo1eMlF7XkI/IiDHzQ==}
dev: false dev: false
/d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-axis@3.0.0:
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
engines: {node: '>=12'}
dev: false
/d3-brush@3.0.0:
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dev: false
/d3-chord@3.0.1:
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-contour@4.0.2:
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-delaunay@6.0.4:
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
engines: {node: '>=12'}
dependencies:
delaunator: 5.0.0
dev: false
/d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
dev: false
/d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
dev: false
/d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'}
hasBin: true
dependencies:
commander: 7.2.0
iconv-lite: 0.6.3
rw: 1.3.3
dev: false
/d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-fetch@3.0.1:
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
engines: {node: '>=12'}
dependencies:
d3-dsv: 3.0.1
dev: false
/d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
dev: false
/d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
dev: false
/d3-geo@3.1.0:
resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-hierarchy@3.1.2:
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-polygon@3.0.1:
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
engines: {node: '>=12'}
dev: false
/d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
dev: false
/d3-random@3.0.1:
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
engines: {node: '>=12'}
dev: false
/d3-scale-chromatic@3.0.0:
resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
dev: false
/d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
dev: false
/d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/d3-transition@3.0.1(d3-selection@3.0.0):
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
dev: false
/d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dev: false
/d3@7.8.5:
resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-axis: 3.0.0
d3-brush: 3.0.0
d3-chord: 3.0.1
d3-color: 3.1.0
d3-contour: 4.0.2
d3-delaunay: 6.0.4
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-dsv: 3.0.1
d3-ease: 3.0.1
d3-fetch: 3.0.1
d3-force: 3.0.0
d3-format: 3.1.0
d3-geo: 3.1.0
d3-hierarchy: 3.1.2
d3-interpolate: 3.0.1
d3-path: 3.1.0
d3-polygon: 3.0.1
d3-quadtree: 3.0.1
d3-random: 3.0.1
d3-scale: 4.0.2
d3-scale-chromatic: 3.0.0
d3-selection: 3.0.0
d3-shape: 3.2.0
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-timer: 3.0.1
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
dev: false
/damerau-levenshtein@1.0.8: /damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true dev: true
@ -3870,6 +3561,18 @@ packages:
ms: 2.1.3 ms: 2.1.3
dev: true dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: true
/debug@4.3.4(supports-color@5.5.0): /debug@4.3.4(supports-color@5.5.0):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -3919,12 +3622,6 @@ packages:
has-property-descriptors: 1.0.1 has-property-descriptors: 1.0.1
object-keys: 1.1.1 object-keys: 1.1.1
/delaunator@5.0.0:
resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
dependencies:
robust-predicates: 3.0.2
dev: false
/delayed-stream@1.0.0: /delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@ -4215,7 +3912,7 @@ packages:
eslint: '*' eslint: '*'
eslint-plugin-import: '*' eslint-plugin-import: '*'
dependencies: dependencies:
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
enhanced-resolve: 5.15.0 enhanced-resolve: 5.15.0
eslint: 8.56.0 eslint: 8.56.0
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
@ -4384,7 +4081,7 @@ packages:
ajv: 6.12.6 ajv: 6.12.6
chalk: 4.1.2 chalk: 4.1.2
cross-spawn: 7.0.3 cross-spawn: 7.0.3
debug: 4.3.4(supports-color@5.5.0) debug: 4.3.4
doctrine: 3.0.0 doctrine: 3.0.0
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 7.2.2 eslint-scope: 7.2.2
@ -5008,13 +4705,6 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: false dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ieee754@1.2.1: /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false dev: false
@ -5068,17 +4758,6 @@ packages:
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/interval-tree-1d@1.0.4:
resolution: {integrity: sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==}
dependencies:
binary-search-bounds: 2.0.5
dev: false
/ipaddr.js@1.9.1: /ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@ -5284,10 +4963,6 @@ packages:
/isexe@2.0.0: /isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
/isoformat@0.2.1:
resolution: {integrity: sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==}
dev: false
/iterator.prototype@1.1.2: /iterator.prototype@1.1.2:
resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
dependencies: dependencies:
@ -5458,10 +5133,6 @@ packages:
p-locate: 5.0.0 p-locate: 5.0.0
dev: true dev: true
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash._baseiteratee@4.7.0: /lodash._baseiteratee@4.7.0:
resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==}
dependencies: dependencies:
@ -6517,20 +6188,12 @@ packages:
glob: 7.2.3 glob: 7.2.3
dev: true dev: true
/robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
dev: false
/run-parallel@1.2.0: /run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
dev: true dev: true
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
dev: false
/safe-array-concat@1.0.1: /safe-array-concat@1.0.1:
resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}