diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 414c79d..8ac823f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,20 +1,16 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.191.0/containers/go/.devcontainer/base.Dockerfile # [Choice] Go version: 1, 1.16, 1.15 -ARG VARIANT="1.16" -# NOTE: Temporary workaround because Microsoft doesn't publish -# ARM images for their devcontainers yet. AMD64 containers -# use way too much CPU on M1 so I've built the image for ARM. -# FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} -FROM evanbuss/devcontainers-go:latest +ARG VARIANT="1.17" +FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT} # [Choice] Node.js version: none, lts, 16, 14, 12, 10 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends tmux # [Optional] Uncomment the next line to use go get to install anything else you need # RUN go get -x diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cbe6ddd..30f92b5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,9 +5,7 @@ "build": { "dockerfile": "Dockerfile", "args": { - // Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.15 - "VARIANT": "1.16", - // Options + "VARIANT": "1.17", "NODE_VERSION": "16" } }, @@ -25,7 +23,10 @@ }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "golang.Go" + "golang.Go", + "eamodio.gitlens", + "bradlc.vscode-tailwindcss", + "esbenp.prettier-vscode" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ diff --git a/.github/home_v2.png b/.github/home_v2.png new file mode 100644 index 0000000..f627503 Binary files /dev/null and b/.github/home_v2.png differ diff --git a/.gitignore b/.gitignore index c090e16..092efd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # start persist openbooks +!openbooks/ *.DS_Store **/dist # end persist diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a7157a8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch OpenBooks Server", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "cmd/openbooks", + "args": [ + "server", + "--log", + "--server", + "localhost" + ], + }, + { + "name": "Launch OpenBooks CLI", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "cmd/openbooks", + "args": [ + "cli", + "download", + "--server", + "localhost", + "!test" + ], + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f3b33e2..166ee54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,12 @@ COPY --from=web /web/ . ENV CGO_ENABLED=0 RUN go get -d -v ./... RUN go install -v ./... +WORKDIR /go/src/cmd/openbooks/ RUN go build FROM gcr.io/distroless/static as app WORKDIR /app -COPY --from=build /go/src/openbooks . +COPY --from=build /go/src/cmd/openbooks/openbooks . EXPOSE 80 VOLUME [ "/books" ] diff --git a/README.md b/README.md index 8a2a846..9aa2d06 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ Openbooks allows you to download ebooks from irc.irchighway.net quickly and easily. -![home](https://raw.githubusercontent.com/evan-buss/openbooks/master/.github/home.png) -![search results](https://raw.githubusercontent.com/evan-buss/openbooks/master/.github/search.png) +![openbooks](https://raw.githubusercontent.com/evan-buss/openbooks/master/.github/home_v2.png) ## Getting Started @@ -22,7 +21,7 @@ Openbooks allows you to download ebooks from irc.irchighway.net quickly and easi - Basic config - `docker run -p 8080:80 evanbuss/openbooks` - Config to perist all eBook files to disk - - `docker run -p 8080:80 -v /home/evan/Downloads/books:/books evanbuss/openbooks --persist` + - `docker run -p 8080:80 -v /home/evan/Downloads/openbooks:/books evanbuss/openbooks --persist` ### Setting the Base Path @@ -33,6 +32,14 @@ OpenBooks server doesn't have to be hosted at the root of your webserver. The ba - Binary - `./openbooks server --basepath /openbooks/` +## Usage + +For a complete list of features use the `--help` flags on all subcommands. +For example `openbooks cli --help or openbooks cli download --help`. There are +two modes; Server or CLI. In CLI mode you interact and download books through +a terminal interface. In server mode the application runs as a web application +that you can visit in your browser. + ## Development ### Install the dependencies @@ -51,6 +58,19 @@ OpenBooks server doesn't have to be hosted at the root of your webserver. The ba - `go build` +### Mock Development Server + +- The mock server allows you to debug responses and requests to simplified IRC / DCC + servers that mimic the responses received from IRC Highway. +- + ```bash + cd cmd/mock_server + go run . + # Another Terminal + cd cmd/openbooks + go run . server --server localhost --log + ``` + ## Why / How - I wrote this as an easier way to search and download books from irchighway.net. It handles all the extraction and data processing for you. You just have to click the book you want. Hopefully you find it much easier than the IRC interface. diff --git a/build.sh b/build.sh index 840bdd5..91b3f1e 100755 --- a/build.sh +++ b/build.sh @@ -4,11 +4,11 @@ echo "Building React App." cd server/app npm install npm run build -cd ../.. +cd ../../cmd/openbooks -echo "Building binaries for various platforms."; -env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o build/openbooks.exe -env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o build/openbooks_mac -env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o build/openbooks_mac_arm -env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/openbooks_linux -env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/openbooks_linux_arm \ No newline at end of file +echo "Building binaries for various platforms." +env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ../../build/openbooks.exe +env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ../../build/openbooks_mac +env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o ../../build/openbooks_mac_arm +env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../../build/openbooks_linux +env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ../../build/openbooks_linux_arm \ No newline at end of file diff --git a/cli/cli.go b/cli/cli.go index d0b23fa..f8d6c72 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,92 +1,94 @@ package cli import ( - "bufio" + "context" "fmt" - "log" - "os" - "os/signal" - "strings" - "syscall" "github.com/evan-buss/openbooks/core" "github.com/evan-buss/openbooks/irc" ) -// Config is used to configure CLI mode settings. type Config struct { UserName string // Username to use when connecting to IRC Log bool // True if IRC messages should be logged + Dir string + Server string + irc *irc.Conn } -// Reader is a way to recieve input from the user -var reader *bufio.Reader - -// IRC is the current IRC connection -var conn *irc.Conn - -// Start instantiates the OpenBooks CLI interface -func Start(config Config) { - conn := irc.New(config.UserName, "OpenBooks CLI") - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - <-c - conn.Disconnect() - os.Exit(1) - }() - +// StartInteractive instantiates the OpenBooks CLI interface +func StartInteractive(config Config) { fmt.Println("=======================================") fmt.Println(" Welcome to OpenBooks ") fmt.Println("=======================================") - core.Join(conn) + conn := instantiate(config) + config.irc = conn - cwd, err := os.Getwd() - if err != nil { - log.Fatalln("Could not get current working directory.", err) + ctx, cancel := context.WithCancel(context.Background()) + registerShutdown(conn, cancel) + + handler := fullHandler(config) + if config.Log { + file := config.setupLogger(handler) + defer file.Close() } - exitSignal := make(chan struct{}) - go core.ReadDaemon(conn, config.Log, Handler{cwd}, exitSignal) + go core.StartReader(ctx, conn, handler) + terminalMenu(conn) - fmt.Println("Connection established...") + <-ctx.Done() +} - fmt.Print("\r") +func StartDownload(config Config, download string) { + conn := instantiate(config) + defer conn.Close() + ctx, cancel := context.WithCancel(context.Background()) - reader = bufio.NewReader(os.Stdin) + handler := core.EventHandler{} + handler[core.BookResult] = func(text string) { + fmt.Printf("%sReceived file response.\n", clearLine) + config.downloadHandler(text) + cancel() + } + if config.Log { + file := config.setupLogger(handler) + defer file.Close() + } + + fmt.Printf("Sending download request.") + go core.StartReader(ctx, conn, handler) + core.DownloadBook(conn, download) + fmt.Printf("%sSent download request.", clearLine) + fmt.Printf("Waiting for file response.") - // Get the first input - // Reader, IRC - menu() - // We make a channel to block forever. We want the reader daemon to run forever - <-exitSignal + registerShutdown(conn, cancel) + <-ctx.Done() } -//reader *bufio.Reader, irc *irc.Conn -func menu() { - fmt.Print("\ns)search\ng)et book\nd)one\n~> ") - - input, _ := reader.ReadString('\n') - input = strings.TrimRight(input, "\n") - input = strings.TrimRight(input, "\r") - - switch input { - case "s": - fmt.Print("@search ") - message, _ := reader.ReadString('\n') - core.SearchBook(conn, message) - case "g": - fmt.Print("Download String: ") - message, _ := reader.ReadString('\n') - core.DownloadBook(conn, message) - case "d": - fmt.Println("Disconnecting.") - conn.Disconnect() - os.Exit(0) - default: - fmt.Println("Invalid Selection.") - menu() +func StartSearch(config Config, query string) { + conn := instantiate(config) + defer conn.Close() + ctx, cancel := context.WithCancel(context.Background()) + + handler := core.EventHandler{} + handler[core.SearchResult] = func(text string) { + fmt.Printf("%sReceived file response.\n", clearLine) + config.searchHandler(text) + cancel() } + handler[core.MatchesFound] = config.matchesFoundHandler + if config.Log { + file := config.setupLogger(handler) + defer file.Close() + } + + fmt.Printf("Sending search request.") + go core.StartReader(ctx, conn, handler) + core.SearchBook(conn, query) + fmt.Printf("%sSent search request.", clearLine) + fmt.Printf("Waiting for file response.") + + registerShutdown(conn, cancel) + <-ctx.Done() } diff --git a/cli/handlers.go b/cli/handlers.go new file mode 100644 index 0000000..aad2fd9 --- /dev/null +++ b/cli/handlers.go @@ -0,0 +1,72 @@ +package cli + +import ( + "fmt" + "log" + + "github.com/evan-buss/openbooks/core" + "github.com/evan-buss/openbooks/dcc" + "github.com/schollz/progressbar/v3" +) + +// DownloadSearchResults downloads the search results +// and sends user a response message +func (c Config) searchHandler(text string) { + download, err := dcc.ParseString(text) + if err != nil { + log.Println(err) + return + } + bar := progressbar.DefaultBytes(download.Size, download.Filename) + + extractedPath, err := core.DownloadExtractDCCString(c.Dir, text, bar) + if err != nil { + fmt.Println(err) + } + fmt.Println("Results location: " + extractedPath) +} + +// DownloadBookFile downloads the search results and sends +// a user a response message +func (c Config) downloadHandler(text string) { + download, err := dcc.ParseString(text) + if err != nil { + log.Println(err) + return + } + bar := progressbar.DefaultBytes(download.Size, download.Filename) + + extractedPath, err := core.DownloadExtractDCCString(c.Dir, text, bar) + if err != nil { + fmt.Println(err) + } + fmt.Println("File location: " + extractedPath) +} + +// NoResults is called when the user searches for something that +// is not available sends a CLI message +func (c Config) noResultsHandler(_ string) { + fmt.Println("No results returned for your search...") +} + +// BadServer is called when the user tries to download a file from a +// server that is not available. +func (c Config) badServerHandler(_ string) { + fmt.Println("That server is not available. Try again...") +} + +// SearchAccepted is called when the search has been accepted but the user +// must wait in the queue for the search to be executed. +func (c Config) searchAcceptedHandler(_ string) { + fmt.Println("Search has been accepted. Please wait.") +} + +// MatchesFound is called when the search returns the number of results +// found. Server sends the client a status update +func (c Config) matchesFoundHandler(num string) { + fmt.Printf("Found %s search results.", num) +} + +func (c Config) pingHandler(_ string) { + c.irc.Pong(c.Server) +} diff --git a/cli/interactive.go b/cli/interactive.go new file mode 100644 index 0000000..0e8598f --- /dev/null +++ b/cli/interactive.go @@ -0,0 +1,68 @@ +package cli + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/evan-buss/openbooks/core" + "github.com/evan-buss/openbooks/irc" +) + +func terminalMenu(irc *irc.Conn) { + fmt.Print("\ns)search\ng)et book\nd)one\n~> ") + + // Trim user input so we don't send 2 messages + clean := func(message string) string { return strings.Trim(message, "\r\n") } + + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = clean(input) + + switch input { + case "s": + fmt.Print("@search ") + message, _ := reader.ReadString('\n') + core.SearchBook(irc, clean(message)) + fmt.Println("\nSent search request.") + case "g": + fmt.Print("Download String: ") + message, _ := reader.ReadString('\n') + core.DownloadBook(irc, clean(message)) + fmt.Println("\nSent download request.") + case "d": + fmt.Println("Disconnecting.") + irc.Disconnect() + os.Exit(0) + default: + fmt.Println("Invalid Selection.") + terminalMenu(irc) + } +} + +func fullHandler(config Config) core.EventHandler { + handler := core.EventHandler{} + + handler[core.BadServer] = func(text string) { + config.badServerHandler(text) + terminalMenu(config.irc) + } + handler[core.BookResult] = func(text string) { + config.downloadHandler(text) + terminalMenu(config.irc) + } + handler[core.SearchResult] = func(text string) { + config.searchHandler(text) + terminalMenu(config.irc) + } + handler[core.SearchAccepted] = config.searchAcceptedHandler + handler[core.NoResults] = func(text string) { + config.noResultsHandler(text) + terminalMenu(config.irc) + } + handler[core.MatchesFound] = config.matchesFoundHandler + handler[core.Ping] = config.pingHandler + + return handler +} diff --git a/cli/irc_handler.go b/cli/irc_handler.go deleted file mode 100644 index 1304969..0000000 --- a/cli/irc_handler.go +++ /dev/null @@ -1,56 +0,0 @@ -package cli - -import ( - "fmt" - - "github.com/evan-buss/openbooks/dcc" -) - -// Handler is the CLI implementation of the EventHandler interface. -type Handler struct { - downloadDir string -} - -// DownloadSearchResults downloads the search results -// and sends user a response message -func (h Handler) DownloadSearchResults(text string) { - fileLocation := make(chan string) - go dcc.NewDownload(text, h.downloadDir, fileLocation) - fmt.Println("Results location: " + <-fileLocation) - menu() -} - -// DownloadBookFile downloads the search results and sends -// a user a response message -func (h Handler) DownloadBookFile(text string) { - fileLocation := make(chan string) - go dcc.NewDownload(text, h.downloadDir, fileLocation) - fmt.Println("File location: " + <-fileLocation) - menu() -} - -// NoResults is called when the user searches for something that -// is not available sends a CLI message -func (h Handler) NoResults() { - fmt.Println("No results returned for that search...") - menu() -} - -// BadServer is called when the user tries to download a file from a -// server that is not available. -func (h Handler) BadServer() { - fmt.Println("That server is not available. Try again...") - menu() -} - -// SearchAccepted is called when the search has been accepted but the user -// must wait in the queue for the search to be executed. -func (h Handler) SearchAccepted() { - fmt.Println("Search has been accepted. Please wait.") -} - -// MatchesFound is called when the search returns the number of results -// found. Server sends the client a status update -func (h Handler) MatchesFound(num string) { - fmt.Println("Your search returned " + num + " matches.") -} diff --git a/cli/util.go b/cli/util.go new file mode 100644 index 0000000..0b81357 --- /dev/null +++ b/cli/util.go @@ -0,0 +1,48 @@ +package cli + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + + "github.com/evan-buss/openbooks/core" + "github.com/evan-buss/openbooks/irc" + "github.com/evan-buss/openbooks/util" +) + +const clearLine = "\r\033[2K" + +func registerShutdown(conn *irc.Conn, cancel context.CancelFunc) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + conn.Disconnect() + cancel() + os.Exit(0) + }() +} + +func instantiate(config Config) *irc.Conn { + fmt.Printf("Connecting to %s.", config.Server) + conn := irc.New(config.UserName, "OpenBooks CLI") + core.Join(conn, config.Server) + fmt.Printf("%sConnected to %s.\n", clearLine, config.Server) + return conn +} + +func (config *Config) setupLogger(handler core.EventHandler) io.Closer { + logger, file, err := util.CreateLogFile(config.UserName, config.Dir) + if err != nil { + log.Fatalf("Error setting up logger: %s\n", err) + } + handler[core.Message] = func(text string) { + logger.Println(text) + } + + return file +} diff --git a/cmd/cli.go b/cmd/cli.go deleted file mode 100644 index 3bff204..0000000 --- a/cmd/cli.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "os/user" - "strings" - - "github.com/evan-buss/openbooks/cli" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(cliCmd) - - user, _ := user.Current() - userName := strings.Split(user.Name, " ")[0] - cliCmd.Flags().StringP("name", "n", userName, "Username to connect to the IRC server as.") - cliCmd.Flags().BoolP("log", "l", false, "Whether or not to log IRC data to an output file.") -} - -var cliCmd = &cobra.Command{ - Use: "cli", - Short: "Run openbooks from the terminal in CLI mode.", - Run: func(cmd *cobra.Command, args []string) { - username, _ := cmd.Flags().GetString("username") - log, _ := cmd.Flags().GetBool("log") - - config := cli.Config{ - UserName: username, - Log: log, - } - - cli.Start(config) - }, -} diff --git a/cmd/mock_server/SearchBot_results_for__the_great_gatsby.txt.zip b/cmd/mock_server/SearchBot_results_for__the_great_gatsby.txt.zip new file mode 100644 index 0000000..4b7ce0f Binary files /dev/null and b/cmd/mock_server/SearchBot_results_for__the_great_gatsby.txt.zip differ diff --git a/cmd/mock_server/great-gatsby.epub b/cmd/mock_server/great-gatsby.epub new file mode 100644 index 0000000..cc4a659 Binary files /dev/null and b/cmd/mock_server/great-gatsby.epub differ diff --git a/cmd/mock_server/main.go b/cmd/mock_server/main.go new file mode 100644 index 0000000..bd64ee4 --- /dev/null +++ b/cmd/mock_server/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/evan-buss/openbooks/mock" +) + +func main() { + ready := make(chan struct{}) + + ircServer := mock.IrcServer{ + Port: ":6667", + } + go ircServer.Start(ready) + <-ready + + greatGatsby, err := os.Open("great-gatsby.epub") + if err != nil { + panic(err) + } + + dccConfig := mock.DccServer{ + Port: ":6669", + Reader: greatGatsby, + } + + go dccConfig.Start(ready) + <-ready + + searchResults, err := os.Open("SearchBot_results_for__the_great_gatsby.txt.zip") + if err != nil { + panic(err) + } + + dccSearch := mock.DccServer{ + Port: ":6668", + Reader: searchResults, + } + + go dccSearch.Start(ready) + <-ready + + fmt.Println("waiting") + time.Sleep(time.Hour * 24) +} diff --git a/cmd/openbooks/cli.go b/cmd/openbooks/cli.go new file mode 100644 index 0000000..993432c --- /dev/null +++ b/cmd/openbooks/cli.go @@ -0,0 +1,65 @@ +package main + +import ( + "errors" + "log" + "os" + "strings" + + "github.com/evan-buss/openbooks/cli" + "github.com/spf13/cobra" +) + +var config cli.Config + +func init() { + rootCmd.AddCommand(cliCmd) + cliCmd.AddCommand(downloadCmd) + cliCmd.AddCommand(searchCmd) + + cwd, err := os.Getwd() + if err != nil { + log.Fatalln("Could not get current working directory.", err) + } + + cliCmd.PersistentFlags().StringVarP(&config.UserName, "name", "n", generateUserName(), "Use a name that isn't randomly generated. One word only.") + cliCmd.PersistentFlags().StringVarP(&config.Dir, "directory", "d", cwd, "Directory where files are downloaded.") + cliCmd.PersistentFlags().BoolVarP(&config.Log, "log", "l", false, "Whether or not to log IRC messages to an output file.") + cliCmd.PersistentFlags().StringVarP(&config.Server, "server", "s", "irc.irchighway.net", "IRC server to connect to.") +} + +var cliCmd = &cobra.Command{ + Use: "cli", + Short: "Run openbooks from the terminal in CLI mode.", + Run: func(cmd *cobra.Command, args []string) { + cli.StartInteractive(config) + }, +} + +var downloadCmd = &cobra.Command{ + Use: "download [flags] identifier", + Short: "Downloads a single file and exits.", + Example: `openbooks cli download '!Oatmeal - F. Scott Fitzgerald - The Great Gatsby.epub'`, + Args: func(cmd *cobra.Command, args []string) error { + err := cobra.ExactArgs(1)(cmd, args) + if err != nil { + return err + } + if !strings.HasPrefix(args[0], "!") { + return errors.New("identifier must begin with '!'") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + cli.StartDownload(config, args[0]) + }, +} + +var searchCmd = &cobra.Command{ + Use: "search", + Short: "Searches for a book and exits.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cli.StartSearch(config, args[0]) + }, +} diff --git a/cmd/root.go b/cmd/openbooks/main.go similarity index 78% rename from cmd/root.go rename to cmd/openbooks/main.go index 27a7ff7..48aef14 100644 --- a/cmd/root.go +++ b/cmd/openbooks/main.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "fmt" @@ -12,8 +12,7 @@ var rootCmd = &cobra.Command{ Short: "Quickly and easily download eBooks from IRCHighway.", } -// Execute starts the root command handler. -func Execute() { +func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/cmd/server.go b/cmd/openbooks/server.go similarity index 77% rename from cmd/server.go rename to cmd/openbooks/server.go index ced64d4..78c8913 100644 --- a/cmd/server.go +++ b/cmd/openbooks/server.go @@ -1,9 +1,10 @@ -package cmd +package main import ( "fmt" "math/rand" "os" + "path" "time" "github.com/brianvoe/gofakeit/v5" @@ -16,13 +17,14 @@ func init() { rootCmd.AddCommand(serverCmd) userName := generateUserName() - serverCmd.Flags().BoolP("log", "l", false, "Save IRC logs to irc_log.txt.") + serverCmd.Flags().BoolP("log", "l", false, "Save raw IRC logs for each client connection.") serverCmd.Flags().BoolP("browser", "b", false, "Open the browser on server start.") serverCmd.Flags().StringP("name", "n", userName, "Use a name that isn't randomly generated. One word only.") serverCmd.Flags().StringP("port", "p", "5228", "Set the local network port for browser mode.") - serverCmd.Flags().StringP("dir", "d", os.TempDir(), "The directory where eBooks are saved when persist enabled.") + serverCmd.Flags().StringP("dir", "d", path.Join(os.TempDir(), "openbooks"), "The directory where eBooks are saved when persist enabled.") serverCmd.Flags().Bool("persist", false, "Persist eBooks in 'dir'. Default is to delete after sending.") serverCmd.Flags().String("basepath", "/", `Base path where the application is accessible. For example "/openbooks/".`) + serverCmd.Flags().StringP("server", "s", "irc.irchighway.net", "IRC server to connect to.") } var serverCmd = &cobra.Command{ @@ -37,6 +39,7 @@ var serverCmd = &cobra.Command{ dir, _ := cmd.Flags().GetString("dir") persist, _ := cmd.Flags().GetBool("persist") basepath, _ := cmd.Flags().GetString("basepath") + url, _ := cmd.Flags().GetString("server") // If cli flag isn't set (default value) check for the presence of an // environment variable and use it if found. @@ -53,7 +56,8 @@ var serverCmd = &cobra.Command{ Port: port, DownloadDir: dir, Persist: persist, - Basepath: basepath, + Basepath: sanitizePath(basepath), + Server: url, } server.Start(config) @@ -67,3 +71,11 @@ func generateUserName() string { gofakeit.Seed(int64(rand.Int())) return fmt.Sprintf("%s-%s", gofakeit.Adjective(), gofakeit.Noun()) } + +func sanitizePath(basepath string) string { + cleaned := path.Clean(basepath) + if cleaned == "/" { + return cleaned + } + return cleaned + "/" +} diff --git a/core/file.go b/core/file.go new file mode 100644 index 0000000..7c6bd01 --- /dev/null +++ b/core/file.go @@ -0,0 +1,46 @@ +package core + +import ( + "io" + "os" + "path" + + "github.com/evan-buss/openbooks/dcc" + "github.com/evan-buss/openbooks/util" +) + +func DownloadExtractDCCString(baseDir, dccStr string, progress io.Writer) (string, error) { + // Download the file and wait until it is completed + download, err := dcc.ParseString(dccStr) + if err != nil { + return "", err + } + + dccPath := path.Join(baseDir, download.Filename) + file, err := os.Create(dccPath) + if err != nil { + return "", err + } + + writer := io.Writer(file) + if progress != nil { + writer = io.MultiWriter(file, progress) + } + + // Download DCC data to the file + err = download.Download(writer) + if err != nil { + return "", err + } + file.Close() + if !util.IsArchive(dccPath) { + return dccPath, nil + } + + extractedPath, err := util.ExtractArchive(dccPath) + if err != nil { + return "", err + } + + return extractedPath, nil +} diff --git a/core/irchighway.go b/core/irchighway.go index cad14cb..72167a6 100644 --- a/core/irchighway.go +++ b/core/irchighway.go @@ -6,14 +6,11 @@ import ( "github.com/evan-buss/openbooks/irc" ) -var ircConn *irc.Conn - // Specific irc.irchighway.net commands // Join connects to the irc.irchighway.net server and joins the #ebooks channel -func Join(irc *irc.Conn) { - ircConn = irc - irc.Connect("irc.irchighway.net") +func Join(irc *irc.Conn, url string) { + irc.Connect(url) // Wait before joining the ebooks room // Often you recieve a private message from the server time.Sleep(time.Second * 2) diff --git a/core/reader.go b/core/reader.go index 7883286..232a122 100644 --- a/core/reader.go +++ b/core/reader.go @@ -2,113 +2,96 @@ package core import ( "bufio" - "fmt" + "context" "log" - "os" "strings" - "time" "github.com/evan-buss/openbooks/irc" ) -// ReaderHandler handles and responds to different IRC events -// Both the CLI and Server versions implement this interface -type ReaderHandler interface { - DownloadSearchResults(text string) - DownloadBookFile(text string) - NoResults() - BadServer() - SearchAccepted() - MatchesFound(num string) -} +type event int -// Possible messages that are sent by the server. We respond accordingly const ( - pingMessage = "PING" - sendMessage = "DCC SEND" - noticeMessage = "NOTICE" - noResults = "Sorry" - serverUnavailable = "try another server" - searchAccepted = "has been accepted" - numMatches = "matches" - beginUserList = "353" - endUserList = "366" + noOp = event(0) + Message = event(1) + SearchResult = event(2) + BookResult = event(3) + NoResults = event(4) + BadServer = event(5) + SearchAccepted = event(6) + MatchesFound = event(7) + ServerList = event(8) + Ping = event(9) ) -// Servers contains the cache of available download servers. -var serverCache ServerCache +// Unique identifiers found in the message for various different events. +const ( + pingMessage = "PING" + sendMessage = "DCC SEND" + noticeMessage = "NOTICE" + noResults = "Sorry" + serverUnavailable = "try another server" + searchAccepted = "has been accepted" + searchResultIdentifier = "_results_for" + numMatches = "matches" + beginUserList = "353" + endUserList = "366" +) -// ReadDaemon is designed to be launched as a goroutine. Listens for -// specific messages and dispatches appropriate handler functions -// Params: irc - IRC connection -// handler - domain specific handler that responds to IRC events -func ReadDaemon(irc *irc.Conn, logIrc bool, handler ReaderHandler, disconnect <-chan struct{}) { +type HandlerFunc func(text string) +type EventHandler map[event]HandlerFunc - var logFile *os.File - serverCache = ServerCache{Servers: []string{}, Time: time.Now()} - var users strings.Builder // Accumulate list of users and then flush +func StartReader(ctx context.Context, irc *irc.Conn, handler EventHandler) { + var users strings.Builder scanner := bufio.NewScanner(irc) - if logIrc { - var err error - logFile, err = os.OpenFile("irc_log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Fatal("Error Opening Log File.", err) - } - defer logFile.Close() - - _, err = logFile.WriteString("\n==================== NEW LOG ======================\n") - if err != nil { - log.Println(err) - } - } - for scanner.Scan() { select { - case <-disconnect: - fmt.Println("breaking out of scanner due to disconnect") + case <-ctx.Done(): return - default: - text := scanner.Text() if err := scanner.Err(); err != nil { - log.Println("Scanner errror: ", err) + log.Println(err) } - if logIrc { - _, err := logFile.WriteString(text + "\n") - if err != nil { - log.Println(err) - } + // Send raw message if they want to recieve it (logging purposes) + if invoke, ok := handler[Message]; ok { + invoke(text) } - // Respond to Direct Client-to-Client downloads + event := noOp if strings.Contains(text, sendMessage) { - if strings.Contains(text, "_results_for") { - go handler.DownloadSearchResults(text) + if strings.Contains(text, searchResultIdentifier) { + event = SearchResult } else { - go handler.DownloadBookFile(text) + event = BookResult } } else if strings.Contains(text, noticeMessage) { if strings.Contains(text, noResults) { - handler.NoResults() + event = NoResults } else if strings.Contains(text, serverUnavailable) { - handler.BadServer() + event = BadServer } else if strings.Contains(text, searchAccepted) { - handler.SearchAccepted() + event = SearchAccepted } else if strings.Contains(text, numMatches) { start := strings.LastIndex(text, "returned") + 9 end := strings.LastIndex(text, "matches") - 1 - handler.MatchesFound(text[start:end]) + text = text[start:end] + event = MatchesFound } } else if strings.Contains(text, beginUserList) { - users.WriteString(text) // Accumulate the user list + users.WriteString(text) } else if strings.Contains(text, endUserList) { - serverCache.ParseServers(users.String()) + event = ServerList + text = users.String() users.Reset() } else if strings.Contains(text, pingMessage) { - irc.PONG("irc.irchighway.net") + event = Ping + } + + if invoke, ok := handler[event]; ok { + go invoke(text) } } } diff --git a/core/search_parser.go b/core/search_parser.go index b07bab7..bfd9e81 100644 --- a/core/search_parser.go +++ b/core/search_parser.go @@ -3,6 +3,8 @@ package core import ( "bufio" "errors" + "fmt" + "io" "log" "os" "sort" @@ -32,32 +34,46 @@ type BookDetail struct { Full string `json:"full"` } +type ParseError struct { + Line string + Error error +} + +func (p ParseError) String() string { + return fmt.Sprintf("Error: %s. Line: %s.", p.Error, p.Line) +} + // ParseSearchFile converts a single search file into an array of BookDetail -func ParseSearchFile(filePath string) []BookDetail { +func ParseSearchFile(filePath string) ([]BookDetail, []ParseError) { file, err := os.Open(filePath) if err != nil { log.Fatal(err) } + defer file.Close() + + return ParseSearch(file) +} - scanner := bufio.NewScanner(file) +func ParseSearch(reader io.Reader) ([]BookDetail, []ParseError) { var books []BookDetail + var errors []ParseError - counter := 0 + scanner := bufio.NewScanner(reader) for scanner.Scan() { - if counter > 3 { - dat, err := parseLine(scanner.Text()) - if err == nil { + line := scanner.Text() + if strings.HasPrefix(line, "!") { + dat, err := parseLine(line) + if err != nil { + errors = append(errors, ParseError{Line: line, Error: err}) + } else { books = append(books, dat) } } - counter++ } - file.Close() - sort.Slice(books, func(i, j int) bool { return books[i].Server < books[j].Server }) - return books + return books, errors } // Parse line extracts data from a single line @@ -65,7 +81,7 @@ func parseLine(line string) (BookDetail, error) { //First check if it follows the correct format. Some servers don't include file info... if !strings.Contains(line, "::INFO::") { - return BookDetail{}, errors.New("invalid line format. No file info ") + return BookDetail{}, errors.New("invalid line format. ::INFO:: not found") } var book BookDetail @@ -112,7 +128,13 @@ func parseLine(line string) (BookDetail, error) { if tmp = strings.Index(line, "::INFO:: "); tmp == -1 { return BookDetail{}, errors.New("could not parse size") } - book.Size = line[tmp+9:] + + line = strings.TrimSpace(line) + splits := strings.Split(line, " ") + + if len(splits) >= 2 { + book.Size = splits[1] + } return book, nil } diff --git a/core/search_parser_test.go b/core/search_parser_test.go new file mode 100644 index 0000000..ca196ea --- /dev/null +++ b/core/search_parser_test.go @@ -0,0 +1,57 @@ +package core + +import ( + "strings" + "testing" +) + +func TestSearchParser(t *testing.T) { + reader := strings.NewReader(sampleData) + results, errors := ParseSearch(reader) + + if len(errors) > 0 { + t.Errorf("Expected 0 errors but got %d\n", len(errors)) + } + + if len(results) != 27 { + t.Errorf("Expected 27 results but got %d\n", len(results)) + } +} + +var sampleData = `Search results from SearchBot v3.00.07 by Ook, searching dll written by Iczelion, Based on Searchbot v2.22 by Dukelupus +Searched 20 lists for "the great gatsby" , found 27 matches. Enjoy! +This list includes results from ALL the lists SearchBot v3.00.07 currently has, some of these servers may be offline. +Always check to be sure the server you want to make a request from is actually in the channel, otherwise your request will have no effect. +For easier searching, use sbClient script (also very fast local searches). You can get that script by typing @sbClient in the channel. + + + + +!dragnbreaker Fitzgerald, F Scott - Novel 03 - The Great Gatsby (retail).epub ::INFO:: 1.7MB +!DV8 F. Scott Fitzgerald - The Great Gatsby (Epub).rar ::INFO:: 394.7KB +!Horla F Scott Fitzgerald - The Great Gatsby (retail) (epub).epub +!Horla F. Scott Fitzgerald - The Great Gatsby (V1.5 RTF).rtf +!Horla Sarah Churchwell - Careless People- Murder, Mayhem, and the Invention of the Great Gatsby (epub).epub +!JimBob420 F. Scott Fitzgerald - The Great Gatsby (V1.5 RTF).rar ::INFO:: 272.23KB +!JimBob420 F Scott Fitzgerald - The Great Gatsby (epub).rar ::INFO:: 204.54KB +!JimBob420 F Scott Fitzgerald - The Great Gatsby (retail) (epub).rar ::INFO:: 1.65MB +!JimBob420 Sarah Churchwell - Careless People- Murder, Mayhem, and the Invention of the Great Gatsby (epub).rar ::INFO:: 8.44MB +!MusicWench F Scott Fitzgerald - The Great Gatsby.epub ::INFO:: 332.7KB +!MusicWench F Scott Fitzgerald - The Great Gatsby.mobi ::INFO:: 376.6KB +!Oatmeal F. Scott Fitzgerald - The Great Gatsby (V1.5 RTF).rar ::INFO:: 272.23KB +!Oatmeal F Scott Fitzgerald - The Great Gatsby (epub).rar ::INFO:: 204.55KB +!Oatmeal F Scott Fitzgerald - The Great Gatsby (retail) (epub).rar ::INFO:: 1.65MB +!Oatmeal Sarah Churchwell - Careless People- Murder, Mayhem, and the Invention of the Great Gatsby (epub).rar ::INFO:: 8.44MB +!Ook So we Read on -How the Great Gatsby came to be and why it Endures (2014) - Maureen Corrigan.epub ::INFO:: 5MB ::HASH:: dde55317998f25aa +!Ook Sarah Churchwell - Careless People- Murder, Mayhem, and the Invention of the Great Gatsby (epub).rar ::INFO:: 8MB ::HASH:: 348c62174a5c5c29 +!Ook F Scott Fitzgerald - The Great Gatsby (retail) (epub).rar ::INFO:: 1MB ::HASH:: 8d860602f0f43789 +!peapod F Scott Fitzgerald - Great Gatsby, The.azw3 ::INFO:: 260.46KB +!peapod F Scott Fitzgerald - Great Gatsby, The.epub ::INFO:: 373.54KB +!peapod F Scott Fitzgerald - Great Gatsby, The.mobi ::INFO:: 368.87KB +!peapod Sarah Churchwell - Careless People- Murder, Mayhem, and the Invention of the Great Gatsby (epub).rar ::INFO:: 8.44MB +!peapod The Great Gatsby.pdf ::INFO:: 254.73KB +!peapod The Great Gatsby - F Scott Fitzgerald.mobi ::INFO:: 246.10KB +!phoomphy Fitzgerald, F. Scott - The Great Gatsby (1925).epub ::INFO:: 205.10 KiB +!phoomphy Fitzgerald, F. Scott - The Great Gatsby.pdf ::INFO:: 775.69 KiB +!phoomphy Call of Cthulhu - Gatsby and the Great Race (monograph #0324).pdf ::INFO:: 20.23 MiB +` diff --git a/core/server_parser.go b/core/server_parser.go index e5a1eda..c3c612f 100644 --- a/core/server_parser.go +++ b/core/server_parser.go @@ -3,52 +3,49 @@ package core import ( "sort" "strings" - "time" ) -var prefixes = [...]string{ - "~", - "&", - "@", - "%", - "+", +var prefixes = map[byte]struct{}{ + '~': {}, + '&': {}, + '@': {}, + '%': {}, + '+': {}, } -// ServerCache maintains a list of download servers that are available -// Time ensures that the cache is never too far out of date -type ServerCache struct { - Servers []string - Time time.Time +type IrcServers struct { + ElevatedUsers []string `json:"elevatedUsers"` + RegularUsers []string `json:"regularUsers"` } -// ParseServers parses the complete list of IRC users to get the elevates users which in +// ParseServers parses the complete list of IRC users to get the elevated users which in // this case are the download servers -func (s *ServerCache) ParseServers(data string) { - servers := strings.Split(data, " ") - output := make([]string, 0) - - for _, user := range servers { - for _, prefix := range prefixes { - if strings.Contains(user, prefix) && strings.Index(user, prefix) == 0 && len(user) > 1 { - output = append(output, user[1:]) +func ParseServers(rawString string) IrcServers { + allServers := strings.Split(rawString, " ") + + servers := IrcServers{ + ElevatedUsers: make([]string, 0), + RegularUsers: make([]string, 0), + } + + for _, name := range allServers { + if len(name) > 1 { + if _, exists := prefixes[name[0]]; exists { + servers.ElevatedUsers = append(servers.ElevatedUsers, name[1:]) + } else { + servers.RegularUsers = append(servers.RegularUsers, name) } } } - sort.Strings(output) - s.Servers = output - s.Time = time.Now() + + sort.Slice(servers.ElevatedUsers, ignoreCaseSort(servers.ElevatedUsers)) + sort.Slice(servers.RegularUsers, ignoreCaseSort(servers.RegularUsers)) + + return servers } -// GetServers returns the IRC book servers that are online -// TODO: Look into a cleaner way of doing this -func GetServers(servers chan<- []string) { - cacheIsOld := time.Now().Sub(serverCache.Time) > (time.Minute * 2) - if len(serverCache.Servers) == 0 || cacheIsOld { - ircConn.GetUsers("ebooks") - oldTime := serverCache.Time - for serverCache.Time.Equal(oldTime) { - time.Sleep(time.Millisecond * 500) - } +func ignoreCaseSort(items []string) func(i, j int) bool { + return func(i, j int) bool { + return strings.ToLower(items[i]) < strings.ToLower(items[j]) } - servers <- serverCache.Servers } diff --git a/core/server_parser_test.go b/core/server_parser_test.go new file mode 100644 index 0000000..edc0c4a --- /dev/null +++ b/core/server_parser_test.go @@ -0,0 +1,38 @@ +package core + +import ( + "reflect" + "testing" +) + +func TestCaseInsensitiveSort(t *testing.T) { + cases := []struct { + input string + want IrcServers + }{ + { + "+FWServer ~Oatmeal +LawdyServer +fwServer evan", + IrcServers{ + ElevatedUsers: []string{"FWServer", "fwServer", "LawdyServer", "Oatmeal"}, + RegularUsers: []string{"evan"}, + }, + }, + {"", + IrcServers{ + ElevatedUsers: []string{}, + RegularUsers: []string{}, + }, + }, + } + + for _, v := range cases { + result := ParseServers(v.input) + if !reflect.DeepEqual(result.ElevatedUsers, v.want.ElevatedUsers) { + t.Errorf("got %#v, want %#v", result.ElevatedUsers, v.want.ElevatedUsers) + } + + if !reflect.DeepEqual(result.RegularUsers, v.want.RegularUsers) { + t.Errorf("got %#v, want %#v", result.RegularUsers, v.want.RegularUsers) + } + } +} diff --git a/dcc/dcc.go b/dcc/dcc.go index 53fcaed..209954c 100644 --- a/dcc/dcc.go +++ b/dcc/dcc.go @@ -6,141 +6,105 @@ import ( "io" "log" "net" - "os" - "path/filepath" "regexp" "strconv" - - "github.com/mholt/archiver" ) // There are two types of DCC strings this program accepts. -// Search Results -// - A text file containing a list of search results returned from a search -// query. -// Book Files -// - The actual book file itself. You get the download string from the search -// results, enter it, then the book is sent to you. - -// Data contains all of the necessary DCC info parsed from the DCC SEND string -type Data struct { - filename string - ip string - port string - size int -} +// Download contains all of the necessary DCC info parsed from the DCC SEND string -// NewDownload parses the DCC SEND string and downloads the file -func NewDownload(text string, downloadDir string, doneChan chan<- string) { - dcc := Data{} - err := dcc.ParseDCC(text) - if err != nil { - log.Fatal("ParseDCC Error: ", err) - } +var ( + ErrInvalidDCCString = errors.New("invalid dcc send string") + ErrInvalidIP = errors.New("unable to convert int IP to string") + ErrMissingBytes = errors.New("download size didn't match dcc file size. data could be missing") +) - dcc.filename = filepath.Join(downloadDir, dcc.filename) +var dccRegex = regexp.MustCompile(`DCC SEND "?(.+[^"])"?\s(\d+)\s+(\d+)\s+(\d+)\s*`) - downloadDCC(dcc, doneChan) +type Download struct { + Filename string + IP string + Port string + Size int64 } -// downloadDCC downloads the data contained in the Data object -func downloadDCC(dcc Data, doneChan chan<- string) { +func ParseString(text string) (*Download, error) { + groups := dccRegex.FindStringSubmatch(text) - file, err := os.OpenFile(dcc.filename, os.O_CREATE|os.O_WRONLY, 0644) + if len(groups) == 0 { + return nil, ErrInvalidDCCString + } + + ip, err := stringToIP(groups[2]) if err != nil { - log.Fatal(err) + return nil, err } - conn, err := net.Dial("tcp", dcc.ip+":"+dcc.port) + size, err := strconv.ParseInt(groups[4], 10, 64) if err != nil { - log.Fatal(err) + return nil, err + } + + return &Download{ + Filename: groups[1], + IP: ip, + Port: groups[3], + Size: size, + }, nil +} + +// Download writes the data contained in the DCC Download +func (download Download) Download(writer io.Writer) error { + // TODO: Maybe specify deadline? + conn, err := net.Dial("tcp", download.IP+":"+download.Port) + if err != nil { + return err } defer conn.Close() - // Download the file - // NOTE: I tried the io.Copy utility but it EASILY took about 100x - // the amount of time as the manual method... Not sure why (windows) + // NOTE: Not using the idiomatic io.Copy or io.CopyBuffer because they are + // much slower in real world tests than the manual way. I suspect it has to + // do with the way the DCC server is sending data. I don't think it ever sends + // an EOF like the io.* methods expect. + + // Benchmark: 2.36MB File + // CopyBuffer - 4096 - 2m32s, 2m18s, 2m32s + // Copy - 2m35s + // Custom - 1024 - 35s + // Custom - 4096 - 46s, 14s received := 0 - bytes := make([]byte, 1024) - for received < dcc.size { + bytes := make([]byte, 4096) + for int64(received) < download.Size { n, err := conn.Read(bytes) + if err != nil { log.Fatal("Error Downloading Data", err) } - _, err = file.Write(bytes[:n]) + _, err = writer.Write(bytes[:n]) if err != nil { log.Println(err) } received += n } - file.Close() - - var newPath string - ext := filepath.Ext(dcc.filename) - if ext == ".rar" || ext == ".zip" { - err = archiver.Walk(dcc.filename, func(f archiver.File) error { - newPath = filepath.Join(filepath.Dir(dcc.filename), f.Name()) - - out, err := os.OpenFile(newPath, os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - return err - } - - _, err = io.Copy(out, f) - if err != nil { - return err - } - - err = out.Close() - if err != nil { - log.Println("Error closing the archive output file", err) - } - - return nil - }) - if err != nil { - log.Println(err) - } - } - - if newPath != "" { // If we extracted a file, send that file and remove the zip file - doneChan <- newPath - err = os.Remove(dcc.filename) - if err != nil { - log.Println("remove error", err) - } - } else { - doneChan <- dcc.filename - } -} - -// ParseDCC parses the important data of a DCC SEND string -func (dcc *Data) ParseDCC(text string) error { - re := regexp.MustCompile(`DCC SEND "?(.+[^"])"?\s(\d+)\s+(\d+)\s+(\d+)\s*`) - groups := re.FindStringSubmatch(text) - - if len(groups) == 0 { - return errors.New("no match in string") + if int64(received) != download.Size { + return ErrMissingBytes } - dcc.filename = groups[1] - dcc.ip = stringToIP(groups[2]) - dcc.port = groups[3] - dcc.size, _ = strconv.Atoi(groups[4]) return nil } +// ParseDCC parses the important data of a DCC SEND string // Convert a given 32 bit IP integer to an IP string // Ex) 2907707975 -> 192.168.1.1 -func stringToIP(nn string) string { +func stringToIP(nn string) (string, error) { temp, err := strconv.ParseUint(nn, 10, 32) if err != nil { - log.Println("Error Parsing Int From Host String: ", err) + return "", ErrInvalidIP } intIP := uint32(temp) ip := make(net.IP, 4) binary.BigEndian.PutUint32(ip, intIP) - return ip.String() + return ip.String(), nil } diff --git a/dcc/dcc_test.go b/dcc/dcc_test.go index 556ff96..519190c 100644 --- a/dcc/dcc_test.go +++ b/dcc/dcc_test.go @@ -1,44 +1,75 @@ package dcc import ( - "fmt" + "bytes" + "reflect" "testing" + + "github.com/evan-buss/openbooks/mock" ) -// TestSearchParse makes sure that data is properly extracted from the DCC -// response string -func TestSearchParse(t *testing.T) { +// TestStringParsing makes sure that data is properly extracted from the DCC +// response string. (filename, IP conversion, port, and size) +func TestStringParsing(t *testing.T) { tables := []struct { search string - filename string - ip string - port string - size int + download *Download }{ { ":SearchOok!ook@only.ook PRIVMSG evan_28 :DCC SEND SearchOok_results_for__hp_lovecraft.txt.zip 1543751478 2043 784", - "SearchOok_results_for__hp_lovecraft.txt.zip", "92.3.199.54", "2043", 784}, + &Download{Filename: "SearchOok_results_for__hp_lovecraft.txt.zip", IP: "92.3.199.54", Port: "2043", Size: 784}, + }, + { + ":Search!Search@ihw-4q5hcb.dyn.suddenlink.net PRIVMSG evan_bot :DCC SEND SearchBot_results_for__stephen_king_the_stand.txt.zip 2907707975 4342 1116", + &Download{Filename: "SearchBot_results_for__stephen_king_the_stand.txt.zip", IP: "173.80.26.71", Port: "4342", Size: 1116}, + }, { - ":Search!Search@ihw-4q5hcb.dyn.suddenlink.net PRIVMSG evan_bot :DCC SEND SearchBot_results_for__stephen_king_the_stand.txt.zip 2907707975 4342 1116", "SearchBot_results_for__stephen_king_the_stand.txt.zip", "173.80.26.71", "4342", 1116}, + `:DV8!HandyAndy@ihw-39fkft.ip-164-132-173.eu PRIVMSG negative-bishop-1 :DCC SEND "Douglas Adams - [HITCHHIKER'S GUIDE TO THE GALAXY & THE 01] - Hitchhiker's Guide to the Galaxy & The (v5.0) (EPUB).rar" 2760158537 2050 2321788`, + &Download{Filename: "Douglas Adams - [HITCHHIKER'S GUIDE TO THE GALAXY & THE 01] - Hitchhiker's Guide to the Galaxy & The (v5.0) (EPUB).rar", IP: "164.132.173.73", Port: "2050", Size: 2321788}, + }, } - dcc := new(Data) for _, table := range tables { - - dcc.ParseDCC(table.search) - fmt.Printf(dcc.filename) - - if dcc.filename != table.filename { - t.Errorf("Search parser filename incorrect, got: %s, want: %s.", dcc.filename, table.filename) - } - if dcc.ip != table.ip { - t.Errorf("Search parser ip incorrect, got: %s, want: %s.", dcc.ip, table.ip) + download, err := ParseString(table.search) + if err != nil { + t.Error(err) } - if dcc.port != table.port { - t.Errorf("Search parser port incorrect, got: %s, want: %s.", dcc.port, table.port) - } - if dcc.size != table.size { - t.Errorf("Search parser size incorrect, got: %d, want: %d.", dcc.size, table.size) + + if !reflect.DeepEqual(download, table.download) { + t.Errorf("Got %#v want %#v\n", download, table.download) } } } + +func TestDownload(t *testing.T) { + text := "Test dcc download content." + + textDownload := Download{ + Filename: "test.txt", + IP: "localhost", + Port: "6969", + Size: int64(len(text)), + } + + reader := bytes.NewReader([]byte(text)) + server := mock.DccServer{ + Port: ":" + textDownload.Port, + Reader: reader, + } + + ready := make(chan struct{}, 1) + go server.Start(ready) + <-ready + + t.Log("After server start") + + received := new(mock.WriteCloser) + err := textDownload.Download(received) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(text, string(received.Data)) { + t.Errorf("data does not match. got: '%s' want: '%s'\n", received.Data, text) + } +} diff --git a/go.mod b/go.mod index 89acecc..a94a841 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,30 @@ go 1.17 require ( github.com/brianvoe/gofakeit/v5 v5.11.2 github.com/dsnet/compress v0.0.1 // indirect - github.com/frankban/quicktest v1.11.3 // indirect + github.com/go-chi/chi/v5 v5.0.3 github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 - github.com/mholt/archiver v3.1.1+incompatible + github.com/mholt/archiver/v3 v3.5.0 github.com/nwaples/rardecode v1.1.2 // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/rs/cors v1.8.0 + github.com/schollz/progressbar/v3 v3.8.3 github.com/spf13/cobra v1.2.1 github.com/ulikunitz/xz v0.5.10 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect ) require ( + github.com/andybalholm/brotli v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/klauspost/compress v1.10.10 // indirect + github.com/klauspost/pgzip v1.2.4 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/pierrec/lz4/v4 v4.0.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect + golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect + golang.org/x/term v0.0.0-20210916214954-140adaaadfaf // indirect ) diff --git a/go.sum b/go.sum index 5b59c34..6d21cd3 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -59,6 +61,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= @@ -71,13 +74,17 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4= +github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -109,6 +116,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -123,7 +131,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -174,28 +181,38 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= +github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A= +github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= -github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE= +github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -206,21 +223,29 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.0.3 h1:vNQKSVZNYUEAvRY9FaUXAF1XPbSOHJtDTiP41kzDz2E= +github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= +github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8= +github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -239,9 +264,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= @@ -271,6 +300,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -376,6 +408,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -406,7 +439,15 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI= +golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -471,7 +512,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -578,12 +618,15 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/irc/irc.go b/irc/irc.go index 8cd0f44..23b6f01 100644 --- a/irc/irc.go +++ b/irc/irc.go @@ -9,18 +9,19 @@ import ( type Conn struct { net.Conn channel string - username string + Username string realname string } // New creates a new IRC connection to the server using the supplied username and realname func New(username, realname string) *Conn { - irc := Conn{} - irc.channel = "" - irc.username = username - irc.realname = realname + irc := &Conn{ + channel: "", + Username: username, + realname: realname, + } - return &irc + return irc } // Connect connects to the given server at port 6667 @@ -33,8 +34,8 @@ func (i *Conn) Connect(address string) { i.Conn = conn - user := "USER " + i.username + " " + i.username + " " + i.username + " :" + i.realname + "\r\n" - nick := "NICK " + i.username + "\r\n" + user := "USER " + i.Username + " " + i.Username + " " + i.Username + " :" + i.realname + "\r\n" + nick := "NICK " + i.Username + "\r\n" i.Write([]byte(user)) i.Write([]byte(nick)) @@ -65,8 +66,8 @@ func (i *Conn) GetUsers(channel string) { i.Write([]byte("NAMES #" + channel + "\r\n")) } -// PONG sends a PONG message to the server, often used after a PING request -func (i *Conn) PONG(server string) { +// Pong sends a Pong message to the server, often used after a PING request +func (i *Conn) Pong(server string) { i.Write([]byte("PONG " + server + "\r\n")) } diff --git a/main.go b/main.go deleted file mode 100644 index ff0b4b8..0000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/evan-buss/openbooks/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/mock/dcc_server.go b/mock/dcc_server.go new file mode 100644 index 0000000..50fbd6e --- /dev/null +++ b/mock/dcc_server.go @@ -0,0 +1,61 @@ +package mock + +import ( + "io" + "log" + "net" + "os" +) + +type DccServer struct { + Port string + Reader io.ReadSeeker +} + +func (dcc *DccServer) Start(ready chan<- struct{}) { + logger := log.New(os.Stdout, "MOCK DCC: ", 0) + + server, err := net.Listen("tcp", dcc.Port) + if err != nil { + panic(err) + } + logger.Println("Listening on " + dcc.Port) + ready <- struct{}{} + + for { + conn, err := server.Accept() + if err != nil { + panic(err) + } + go dcc.handler(conn) + } +} + +func (dcc *DccServer) handler(conn net.Conn) { + defer func() { + dcc.Reader.Seek(0, io.SeekStart) + logger.Println("closing connection") + conn.Close() + }() + + logger.Println("Received a connection...") + n, err := io.Copy(conn, dcc.Reader) + if err != nil { + logger.Println(err) + } else { + logger.Printf("Done copying %d bytes\n", n) + } +} + +type WriteCloser struct { + Data []byte +} + +func (m *WriteCloser) Write(p []byte) (n int, err error) { + m.Data = append(m.Data, p...) + return len(p), nil +} + +func (m WriteCloser) Close() error { + return nil +} diff --git a/mock/irc_server.go b/mock/irc_server.go new file mode 100644 index 0000000..8a91752 --- /dev/null +++ b/mock/irc_server.go @@ -0,0 +1,77 @@ +package mock + +import ( + "bufio" + "fmt" + "log" + "net" + "os" + "strings" +) + +var logger *log.Logger + +type IrcServer struct { + Port string +} + +func (irc *IrcServer) Start(ready chan<- struct{}) { + logger = log.New(os.Stdout, "MOCK SERVER: ", 0) + + server, err := net.Listen("tcp", irc.Port) + if err != nil { + panic(err) + } + logger.Println("Listening on " + irc.Port) + ready <- struct{}{} + + for { + conn, err := server.Accept() + if err != nil { + panic(err) + } + go handler(conn) + } +} + +func handler(conn net.Conn) { + logger.Printf("Connection received from %s", conn.RemoteAddr().String()) + scanner := bufio.NewScanner(conn) + + serverHandler(conn) + + for scanner.Scan() { + request := scanner.Text() + if err := scanner.Err(); err != nil { + logger.Println(err) + } + + logger.Printf("Request Received: %s\n", request) + + if strings.Contains(request, "@search") { + go searchHandler(request, conn) + } + + if strings.Contains(request, "!") { + go downloadHandler(request, conn) + } + } + + logger.Println("Connection closed.") +} + +func serverHandler(conn net.Conn) { + fmt.Fprintf(conn, "353 +server1 ~server2 ~evan_irc\r\n") + fmt.Fprintf(conn, "end_list 366\r\n") +} + +func searchHandler(request string, conn net.Conn) { + logger.Printf("Sending search results.") + fmt.Fprint(conn, "NOTICE: Search returned 27 matches\r\n") + fmt.Fprint(conn, ":SearchOok!ook@only.ook PRIVMSG evan_28 :DCC SEND SearchOok_results_for__the_great_gatsby.txt.zip 2130706433 6668 1184\r\n") +} + +func downloadHandler(request string, conn net.Conn) { + logger.Println("Sending book file.") + fmt.Fprint(conn, ":SearchOok!ook@only.ook PRIVMSG evan_28 :DCC SEND great-gatsby.epub 2130706433 6669 358887\r\n") +} diff --git a/server/app/.prettierrc b/server/app/.prettierrc new file mode 100644 index 0000000..72c0ae1 --- /dev/null +++ b/server/app/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "quoteProps": "consistent" +} \ No newline at end of file diff --git a/server/app/package-lock.json b/server/app/package-lock.json index 59da75e..686b723 100644 --- a/server/app/package-lock.json +++ b/server/app/package-lock.json @@ -9,30 +9,36 @@ "version": "0.0.0", "dependencies": { "@reduxjs/toolkit": "^1.6.1", + "clsx": "^1.1.1", "evergreen-ui": "^6.4.0", "lodash": "^4.17.21", + "phosphor-react": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", - "react-redux": "^7.2.4", - "styled-components": "^5.3.0", - "use-local-storage-state": "^10.0.0" + "react-redux": "^7.2.5", + "react-router-dom": "^5.3.0", + "use-local-storage-state": "^11.0.0" }, "devDependencies": { - "@types/lodash": "^4.14.172", - "@types/node": "^16.7.1", - "@types/react": "^17.0.19", + "@types/lodash": "^4.14.173", + "@types/node": "^16.9.2", + "@types/react": "^17.0.21", "@types/react-dom": "^17.0.9", - "@types/styled-components": "^5.1.12", + "@types/react-router-dom": "^5.1.9", "@vitejs/plugin-react-refresh": "^1.3.6", - "typescript": "^4.3.5", - "vite": "^2.5.0" + "autoprefixer": "^10.3.4", + "postcss": "^8.3.6", + "tailwindcss": "^2.2.15", + "typescript": "^4.4.3", + "vite": "^2.5.10" } }, "node_modules/@babel/code-frame": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, "dependencies": { "@babel/highlight": "^7.14.5" }, @@ -83,6 +89,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, "dependencies": { "@babel/types": "^7.15.0", "jsesc": "^2.5.1", @@ -92,17 +99,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", - "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", @@ -125,6 +121,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.14.5", "@babel/template": "^7.14.5", @@ -138,6 +135,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, "dependencies": { "@babel/types": "^7.14.5" }, @@ -149,6 +147,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, "dependencies": { "@babel/types": "^7.14.5" }, @@ -172,6 +171,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, "dependencies": { "@babel/types": "^7.14.5" }, @@ -250,6 +250,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, "dependencies": { "@babel/types": "^7.14.5" }, @@ -261,6 +262,7 @@ "version": "7.14.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -292,6 +294,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", @@ -305,6 +308,7 @@ "version": "7.15.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -357,6 +361,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/parser": "^7.14.5", @@ -370,6 +375,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -389,6 +395,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" @@ -402,28 +409,40 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.4.tgz", "integrity": "sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A==" }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { - "@emotion/memoize": "0.7.4" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } }, "node_modules/@reduxjs/toolkit": { "version": "1.6.1", @@ -472,6 +491,12 @@ "react": ">=16.3" } }, + "node_modules/@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", + "dev": true + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -482,15 +507,21 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.172", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", - "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", + "version": "4.14.173", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", + "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==", "dev": true }, "node_modules/@types/node": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", - "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", + "version": "16.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", + "integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, "node_modules/@types/prop-types": { @@ -499,9 +530,9 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "node_modules/@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.21.tgz", + "integrity": "sha512-GzzXCpOthOjXvrAUFQwU/svyxu658cwu00Q9ugujS4qc1zXgLFaO0kS2SLOaMWLt2Jik781yuHCWB7UcYdGAeQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -528,6 +559,27 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.9.tgz", + "integrity": "sha512-Go0vxZSigXTyXx8xPkGiBrrc3YbBs82KE14WENMLS6TSUKcRFSmYVbL19zFOnNFqJhqrPqEs2h5eUpJhSRrwZw==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -541,17 +593,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "node_modules/@types/styled-components": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.12.tgz", - "integrity": "sha512-sTjc0+gMl08JvOHchQKgEGbbiSexSvWg5khUNSH4kosb7Tl4782AtfWMkAhQmeXMg2vIn6PthGVHFW+U/Dpihg==", - "dev": true, - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, "node_modules/@vitejs/plugin-react-refresh": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz", @@ -568,10 +609,43 @@ "node": ">=12.0.0" } }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -579,6 +653,25 @@ "node": ">=4" } }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true + }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -592,30 +685,75 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "node_modules/babel-plugin-styled-components": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz", - "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==", + "node_modules/autoprefixer": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.4.tgz", + "integrity": "sha512-EKjKDXOq7ug+jagLzmnoTRpTT0q1KVzEJqrJd0hCBa7FiG0WbFOBCcJCy2QkW1OckpO3qgttA1aWjVbeIPAecw==", + "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-module-imports": "^7.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11" + "browserslist": "^4.16.8", + "caniuse-lite": "^1.0.30001252", + "colorette": "^1.3.0", + "fraction.js": "^4.1.1", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, "peerDependencies": { - "styled-components": ">= 2" + "postcss": "^8.1.0" } }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } }, "node_modules/bowser": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==" }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.16.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", @@ -639,15 +777,37 @@ "url": "https://opencollective.com/browserslist" } }, - "node_modules/camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } }, "node_modules/caniuse-lite": { - "version": "1.0.30001251", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz", - "integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==", + "version": "1.0.30001255", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz", + "integrity": "sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==", "dev": true, "funding": { "type": "opencollective", @@ -658,6 +818,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -667,15 +828,67 @@ "node": ">=4" } }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/classnames": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", + "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -683,7 +896,36 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/color-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/colorette": { "version": "1.3.0", @@ -691,11 +933,26 @@ "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", "dev": true }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/compute-scroll-into-view": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -711,12 +968,29 @@ "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js." }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=", + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, "engines": { - "node": ">=4" + "node": ">=10" + } + }, + "node_modules/css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true, + "engines": { + "node": "*" } }, "node_modules/css-in-js-utils": { @@ -728,14 +1002,22 @@ "isobject": "^3.0.1" } }, - "node_modules/css-to-react-native": { + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true + }, + "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" } }, "node_modules/csstype": { @@ -747,6 +1029,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -759,6 +1042,41 @@ } } }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "dependencies": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -801,6 +1119,15 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.12.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.20.tgz", @@ -824,6 +1151,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -875,6 +1203,43 @@ "csstype": "^3.0.2" } }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", + "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fbjs": { "version": "0.8.17", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", @@ -889,6 +1254,51 @@ "ua-parser-js": "^0.7.18" } }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -935,14 +1345,53 @@ "through": "^2.3.8" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.1.tgz", + "integrity": "sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { "node": ">=4" } }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -959,10 +1408,30 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, "engines": { "node": ">=4" } }, + "node_modules/hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -976,6 +1445,27 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "node_modules/hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hyphenate-style-name": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", @@ -1001,6 +1491,71 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/inline-style-prefixer": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz", @@ -1010,6 +1565,38 @@ "css-in-js-utils": "^2.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "dependencies": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, "node_modules/is-core-module": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", @@ -1022,6 +1609,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -1030,6 +1647,11 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -1056,6 +1678,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -1063,6 +1686,12 @@ "node": ">=4" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -1078,6 +1707,33 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1093,6 +1749,12 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", + "dev": true + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1104,16 +1766,76 @@ "loose-envify": "cli.js" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "node_modules/modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/nanoid": { "version": "3.1.25", @@ -1127,6 +1849,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", @@ -1142,6 +1873,24 @@ "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", "dev": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1150,12 +1899,97 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/phosphor-react": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/phosphor-react/-/phosphor-react-1.3.1.tgz", + "integrity": "sha512-N4dk4Lrl8Pa2V9cImw/6zP8x9oPFOSh6ixtSvB73zmcpKrX6Sb+lPlu6Y222VOwZx19mfo01bP+OeAbXHw95Jg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -1186,10 +2020,95 @@ "url": "https://opencollective.com/postcss/" } }, + "node_modules/postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "dependencies": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true, + "engines": { + "node": ">= 0.8" + } }, "node_modules/promise": { "version": "7.3.1", @@ -1214,6 +2133,53 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/purgecss": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", + "dev": true, + "dependencies": { + "commander": "^6.0.0", + "glob": "^7.0.0", + "postcss": "^8.2.1", + "postcss-selector-parser": "^6.0.2" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -1250,9 +2216,9 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-redux": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", - "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz", + "integrity": "sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/react-redux": "^7.1.16", @@ -1287,6 +2253,48 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", + "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.1", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -1302,6 +2310,34 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, + "dependencies": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, "node_modules/redux": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", @@ -1338,6 +2374,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "node_modules/rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "2.56.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz", @@ -1353,6 +2440,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1387,15 +2497,26 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1409,44 +2530,137 @@ "node": ">=0.10.0" } }, - "node_modules/styled-components": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz", - "integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==", + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.15.tgz", + "integrity": "sha512-WgV41xTMbnSoTNMNnJvShQZ+8GmY86DmXTrCgnsveNZJdlybfwCItV8kAqjYmU49YiFr+ofzmT1JlAKajBZboQ==", + "dev": true, + "dependencies": { + "arg": "^5.0.1", + "bytes": "^3.0.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", + "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss-js": "^3.0.3", + "postcss-load-config": "^3.1.0", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.6", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "autoprefixer": "^10.0.2", + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/tailwindcss/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/tailwindcss/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/tailwindcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/tailwindcss/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/through": { @@ -1454,6 +2668,16 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "node_modules/tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tinycolor2": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", @@ -1462,18 +2686,43 @@ "node": "*" } }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, "engines": { "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1525,10 +2774,19 @@ "css-in-js-utils": "^2.0.0" } }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/use-local-storage-state": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-10.0.0.tgz", - "integrity": "sha512-NCab0oYOMZA8oT9y4OE7tMT6JS21SiyPsTjZdapnyvHe7bVFlIMSp6LaiuHBdS1OvduuLtG+pX/duFIBkd0PCA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-11.0.0.tgz", + "integrity": "sha512-laYWubKi/OX0WMiyZIfjBBQ4YA+lXH8bXuHq+nR5rtrlSNipJOsbCjFz18GYphf5EYSr+8b7Mnx6v6mLdfUlUg==", "engines": { "node": ">=10" }, @@ -1536,10 +2794,21 @@ "react": ">=16.8.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "node_modules/vite": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.0.tgz", - "integrity": "sha512-Dn4B+g54PJsMG5WCc4QeFy1ygMXRdTtFrUPegqfk4+vzVQcbF/DqqmI/1bxezArzbujBJg/67QeT5wz8edfJVQ==", + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", "dev": true, "dependencies": { "esbuild": "^0.12.17", @@ -1561,6 +2830,30 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } } }, "dependencies": { @@ -1568,6 +2861,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, "requires": { "@babel/highlight": "^7.14.5" } @@ -1605,20 +2899,13 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, "requires": { "@babel/types": "^7.15.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, - "@babel/helper-annotate-as-pure": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", - "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", - "requires": { - "@babel/types": "^7.14.5" - } - }, "@babel/helper-compilation-targets": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", @@ -1635,6 +2922,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.14.5", "@babel/template": "^7.14.5", @@ -1645,6 +2933,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, "requires": { "@babel/types": "^7.14.5" } @@ -1653,6 +2942,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, "requires": { "@babel/types": "^7.14.5" } @@ -1670,6 +2960,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, "requires": { "@babel/types": "^7.14.5" } @@ -1730,6 +3021,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, "requires": { "@babel/types": "^7.14.5" } @@ -1737,7 +3029,8 @@ "@babel/helper-validator-identifier": { "version": "7.14.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==" + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.14.5", @@ -1760,6 +3053,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", @@ -1769,7 +3063,8 @@ "@babel/parser": { "version": "7.15.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", - "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==" + "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", + "dev": true }, "@babel/plugin-transform-react-jsx-self": { "version": "7.14.9", @@ -1801,6 +3096,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, "requires": { "@babel/code-frame": "^7.14.5", "@babel/parser": "^7.14.5", @@ -1811,6 +3107,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, "requires": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -1827,6 +3124,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" @@ -1837,28 +3135,31 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.4.tgz", "integrity": "sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A==" }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "requires": { - "@emotion/memoize": "0.7.4" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } }, "@reduxjs/toolkit": { "version": "1.6.1", @@ -1889,6 +3190,12 @@ "prop-types": "^15.5.7" } }, + "@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", + "dev": true + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -1899,15 +3206,21 @@ } }, "@types/lodash": { - "version": "4.14.172", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", - "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", + "version": "4.14.173", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", + "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==", "dev": true }, "@types/node": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", - "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", + "version": "16.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", + "integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, "@types/prop-types": { @@ -1916,9 +3229,9 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.21.tgz", + "integrity": "sha512-GzzXCpOthOjXvrAUFQwU/svyxu658cwu00Q9ugujS4qc1zXgLFaO0kS2SLOaMWLt2Jik781yuHCWB7UcYdGAeQ==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1945,6 +3258,27 @@ "redux": "^4.0.0" } }, + "@types/react-router": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.9.tgz", + "integrity": "sha512-Go0vxZSigXTyXx8xPkGiBrrc3YbBs82KE14WENMLS6TSUKcRFSmYVbL19zFOnNFqJhqrPqEs2h5eUpJhSRrwZw==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -1958,17 +3292,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "@types/styled-components": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.12.tgz", - "integrity": "sha512-sTjc0+gMl08JvOHchQKgEGbbiSexSvWg5khUNSH4kosb7Tl4782AtfWMkAhQmeXMg2vIn6PthGVHFW+U/Dpihg==", - "dev": true, - "requires": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, "@vitejs/plugin-react-refresh": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz", @@ -1982,14 +3305,54 @@ "react-refresh": "^0.10.0" } }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2000,27 +3363,56 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "babel-plugin-styled-components": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz", - "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==", + "autoprefixer": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.4.tgz", + "integrity": "sha512-EKjKDXOq7ug+jagLzmnoTRpTT0q1KVzEJqrJd0hCBa7FiG0WbFOBCcJCy2QkW1OckpO3qgttA1aWjVbeIPAecw==", + "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-module-imports": "^7.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11" + "browserslist": "^4.16.8", + "caniuse-lite": "^1.0.30001252", + "colorette": "^1.3.0", + "fraction.js": "^4.1.1", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" } }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true }, "bowser": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==" }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browserslist": { "version": "4.16.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", @@ -2034,36 +3426,110 @@ "node-releases": "^1.1.75" } }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true }, "caniuse-lite": { - "version": "1.0.30001251", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz", - "integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==", + "version": "1.0.30001255", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz", + "integrity": "sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==", "dev": true }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "classnames": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, + "color": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", + "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", + "dev": true, + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -2071,7 +3537,18 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "colorette": { "version": "1.3.0", @@ -2079,11 +3556,23 @@ "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", "dev": true }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + }, "compute-scroll-into-view": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -2098,10 +3587,24 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" }, - "css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true }, "css-in-js-utils": { "version": "2.0.1", @@ -2112,15 +3615,17 @@ "isobject": "^3.0.1" } }, - "css-to-react-native": { + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true + }, + "cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "requires": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true }, "csstype": { "version": "3.0.8", @@ -2131,10 +3636,40 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { "ms": "2.1.2" } }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2176,6 +3711,15 @@ "iconv-lite": "^0.6.2" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "esbuild": { "version": "0.12.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.20.tgz", @@ -2191,7 +3735,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "estree-walker": { "version": "2.0.2", @@ -2234,6 +3779,39 @@ } } }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", + "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fbjs": { "version": "0.8.17", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", @@ -2248,6 +3826,38 @@ "ua-parser-js": "^0.7.18" } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "dev": true + }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2284,10 +3894,40 @@ "through": "^2.3.8" } }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.1.tgz", + "integrity": "sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true }, "has": { "version": "1.0.3", @@ -2301,40 +3941,130 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "requires": { - "react-is": "^16.7.0" + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true + }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "immer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==" + }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "requires": { + "import-from": "^3.0.0" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" }, "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true } } }, - "hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "once": "^1.3.0", + "wrappy": "1" } }, - "immer": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", - "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==" + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "inline-style-prefixer": { "version": "3.0.8", @@ -2345,6 +4075,35 @@ "css-in-js-utils": "^2.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, "is-core-module": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", @@ -2354,11 +4113,37 @@ "has": "^1.0.3" } }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -2381,7 +4166,14 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "json5": { "version": "2.2.0", @@ -2392,6 +4184,28 @@ "minimist": "^1.2.5" } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2407,6 +4221,12 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2415,16 +4235,57 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "nanoid": { "version": "3.1.25", @@ -2432,6 +4293,15 @@ "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", "dev": true }, + "node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", @@ -2447,17 +4317,91 @@ "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "phosphor-react": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/phosphor-react/-/phosphor-react-1.3.1.tgz", + "integrity": "sha512-N4dk4Lrl8Pa2V9cImw/6zP8x9oPFOSh6ixtSvB73zmcpKrX6Sb+lPlu6Y222VOwZx19mfo01bP+OeAbXHw95Jg==", + "requires": {} + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -2475,10 +4419,57 @@ "source-map-js": "^0.6.2" } }, + "postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + } + }, + "postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "requires": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, "postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true }, "promise": { "version": "7.3.1", @@ -2505,6 +4496,30 @@ } } }, + "purgecss": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", + "dev": true, + "requires": { + "commander": "^6.0.0", + "glob": "^7.0.0", + "postcss": "^8.2.1", + "postcss-selector-parser": "^6.0.2" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, "react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -2535,9 +4550,9 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "react-redux": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", - "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz", + "integrity": "sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==", "requires": { "@babel/runtime": "^7.12.1", "@types/react-redux": "^7.1.16", @@ -2560,6 +4575,44 @@ "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", "dev": true }, + "react-router": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", + "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "react-router-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.1", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -2571,6 +4624,33 @@ "prop-types": "^15.6.2" } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, "redux": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", @@ -2604,6 +4684,44 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "rollup": { "version": "2.56.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz", @@ -2613,6 +4731,15 @@ "fsevents": "~2.3.2" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2644,15 +4771,28 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-js": { "version": "0.6.2", @@ -2660,50 +4800,154 @@ "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", "dev": true }, - "styled-components": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz", - "integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } }, + "tailwindcss": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.15.tgz", + "integrity": "sha512-WgV41xTMbnSoTNMNnJvShQZ+8GmY86DmXTrCgnsveNZJdlybfwCItV8kAqjYmU49YiFr+ofzmT1JlAKajBZboQ==", + "dev": true, + "requires": { + "arg": "^5.0.1", + "bytes": "^3.0.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", + "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss-js": "^3.0.3", + "postcss-load-config": "^3.1.0", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.6", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tinycolor2": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, "ua-parser-js": { @@ -2731,16 +4975,33 @@ } } }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, "use-local-storage-state": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-10.0.0.tgz", - "integrity": "sha512-NCab0oYOMZA8oT9y4OE7tMT6JS21SiyPsTjZdapnyvHe7bVFlIMSp6LaiuHBdS1OvduuLtG+pX/duFIBkd0PCA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-11.0.0.tgz", + "integrity": "sha512-laYWubKi/OX0WMiyZIfjBBQ4YA+lXH8bXuHq+nR5rtrlSNipJOsbCjFz18GYphf5EYSr+8b7Mnx6v6mLdfUlUg==", "requires": {} }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vite": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.0.tgz", - "integrity": "sha512-Dn4B+g54PJsMG5WCc4QeFy1ygMXRdTtFrUPegqfk4+vzVQcbF/DqqmI/1bxezArzbujBJg/67QeT5wz8edfJVQ==", + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", "dev": true, "requires": { "esbuild": "^0.12.17", @@ -2754,6 +5015,24 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } } diff --git a/server/app/package.json b/server/app/package.json index 09f07d4..890adc7 100644 --- a/server/app/package.json +++ b/server/app/package.json @@ -8,23 +8,28 @@ }, "dependencies": { "@reduxjs/toolkit": "^1.6.1", + "clsx": "^1.1.1", "evergreen-ui": "^6.4.0", "lodash": "^4.17.21", + "phosphor-react": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", - "react-redux": "^7.2.4", - "styled-components": "^5.3.0", - "use-local-storage-state": "^10.0.0" + "react-redux": "^7.2.5", + "react-router-dom": "^5.3.0", + "use-local-storage-state": "^11.0.0" }, "devDependencies": { - "@types/lodash": "^4.14.172", - "@types/node": "^16.7.1", - "@types/react": "^17.0.19", + "@types/lodash": "^4.14.173", + "@types/node": "^16.9.2", + "@types/react": "^17.0.21", "@types/react-dom": "^17.0.9", - "@types/styled-components": "^5.1.12", + "@types/react-router-dom": "^5.1.9", "@vitejs/plugin-react-refresh": "^1.3.6", - "typescript": "^4.3.5", - "vite": "^2.5.0" + "autoprefixer": "^10.3.4", + "postcss": "^8.3.6", + "tailwindcss": "^2.2.15", + "typescript": "^4.4.3", + "vite": "^2.5.10" } } diff --git a/server/app/postcss.config.js b/server/app/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/server/app/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/server/app/src/App.tsx b/server/app/src/App.tsx index 2d1fe12..c3437cb 100644 --- a/server/app/src/App.tsx +++ b/server/app/src/App.tsx @@ -1,14 +1,20 @@ -import { Pane } from 'evergreen-ui'; -import React from 'react'; -import Sidebar from './components/Sidebar'; -import SearchPage from './pages/SearchPage'; +import React from "react"; +import Sidebar from "./components/SideBar/Sidebar"; +import SearchPage from "./pages/SearchPage"; +import { BrowserRouter, Switch, Route } from "react-router-dom"; function App() { return ( - - - - + +
+ + + + + + +
+
); } diff --git a/server/app/src/components/BooksGrid.tsx b/server/app/src/components/BooksGrid.tsx deleted file mode 100644 index 8a4bb8c..0000000 --- a/server/app/src/components/BooksGrid.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Button, Pane, Spinner, Table, Text } from 'evergreen-ui'; -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { BookDetail, MessageType } from '../models/messages'; -import { sendMessage } from '../state/stateSlice'; -import { RootState } from '../state/store'; -import image from '../assets/reading.svg' - - -export const BooksGrid: React.FC = () => { - const activeItem = useSelector((store: RootState) => store.state.activeItem); - const servers = useSelector((store: RootState) => new Set(store.state.serverFilters)); - const dispatch = useDispatch(); - - const download = (book: BookDetail) => { - dispatch(sendMessage({ type: MessageType.DOWNLOAD, payload: { book: book.full } })); - } - - const filtered = (books: BookDetail[], servers: Set) => { - if (servers.size === 0) return books; - return books.filter(x => servers.has(x.server)); - } - const filteredRows = filtered(activeItem?.results ?? [], servers); - - if (activeItem === null) { - return (<> - Search a book to get started. - person reading - ); - } - - // Active item selected, but the results are null - // The item's results haven't been loadeded. Show - // loading indicator - if (activeItem !== null && !activeItem.results) { - return ( - - - - ); - } - - // Results length is zero. No results for query - if (activeItem.results?.length === 0) { - return (No results.); - } - - // FilteredRows length is zero. Filter not valid. - if (filteredRows.length === 0) { - return (No books matching filter.); - } - - return ( - - - Server - Author - Title - Format - Size - Download? - - - { - filteredRows.map((book, i) => ( - - {book.server} - {book.author} - {book.title} - {book.format} - {book.size} - - - - - )) - } - -
- ) -} diff --git a/server/app/src/components/BooksGrid/BooksGrid.tsx b/server/app/src/components/BooksGrid/BooksGrid.tsx new file mode 100644 index 0000000..42439ec --- /dev/null +++ b/server/app/src/components/BooksGrid/BooksGrid.tsx @@ -0,0 +1,182 @@ +import { Button, majorScale, Pane, Spinner, Table, Text } from "evergreen-ui"; +import React, { useEffect, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { MessageType } from "../../models/messages"; +import { sendMessage } from "../../state/stateSlice"; +import { RootState } from "../../state/store"; +import image from "../../assets/reading.svg"; +import SelectMenuHeader, { makeStatusMenuItem } from "./SelectMenuHeader"; +import { useGetServersQuery } from "../../state/api"; +import { HistoryItem } from "../../state/historySlice"; + +const stringContains = (first: string, second: string): boolean => { + return first.toLowerCase().includes(second.toLowerCase()); +}; + +export const BooksGrid: React.FC = () => { + const dispatch = useDispatch(); + const activeItem: HistoryItem | null = useSelector( + (store: RootState) => store.state.activeItem + ); + const { data: onlineServers, refetch } = useGetServersQuery(null); + const [titleFilter, setTitleFilter] = useState(""); + const [authorFilter, setAuthorFilter] = useState(""); + const [formatFilter, setFormatFilter] = useState([]); + const [serverFilter, setServerFilter] = useState([]); + + useEffect(() => { + refetch(); + }, [activeItem]); + + const filteredBooks = useMemo(() => { + const books = activeItem?.results ?? []; + return books + .filter( + (x) => + serverFilter.length === 0 || + serverFilter.findIndex((server) => server === x.server) !== -1 + ) + .filter((x) => stringContains(x.author, authorFilter)) + .filter((x) => stringContains(x.title, titleFilter)) + .filter( + (x) => + formatFilter.length === 0 || + formatFilter.findIndex((format) => format === x.format) !== -1 + ); + }, [activeItem, authorFilter, titleFilter, formatFilter, serverFilter]); + + const availableExtensions = useMemo(() => { + const extensionTypes = activeItem?.results?.map((x) => x.format) ?? []; + return extensionTypes.filter((x, i, arr) => arr.indexOf(x) === i); + }, [activeItem]); + + const availableServers = useMemo(() => { + const servers = + onlineServers?.concat(activeItem?.results?.map((x) => x.server) ?? []) ?? + []; + return servers.filter((x, i, arr) => arr.indexOf(x) === i); + }, [onlineServers, activeItem]); + + if (activeItem === null) { + return ( + <> + + Search a book to get started. + + person reading + + ); + } + + if (activeItem !== null && !activeItem.results) { + return ( + + + + ); + } + + const renderBody = () => { + // Results length is zero. No results for query + if (activeItem.results?.length === 0) { + return ( + + No results for search. + + ); + } + + // FilteredRows length is zero. Filter not valid. + if (filteredBooks.length === 0) { + return ( + + No books matching filter. + + ); + } + + return filteredBooks.map((book, i) => ( + + + {book.server} + + + {book.author} + + {book.title} + + {book.format} + + + {book.size} + + + + + + )); + }; + + return ( + + + + setAuthorFilter(e)} + placeholder="AUTHOR" + flexBasis={250} + flexGrow={0} + flexShrink={0}> + setTitleFilter(e)} + placeholder="TITLE"> + + + Size + + + Download? + + + {renderBody()} +
+ ); +}; diff --git a/server/app/src/components/BooksGrid/SelectMenuHeader.tsx b/server/app/src/components/BooksGrid/SelectMenuHeader.tsx new file mode 100644 index 0000000..7ce020c --- /dev/null +++ b/server/app/src/components/BooksGrid/SelectMenuHeader.tsx @@ -0,0 +1,78 @@ +import { + Option, + SelectMenu, + SelectMenuItem, + Table, + Image, + TextDropdownButton, + StatusIndicator +} from "evergreen-ui"; +import React from "react"; + +interface Props { + columnTitle: string; + menuTitle: string; + options: string[]; + selected: string[]; + setSelected: React.Dispatch>; + itemRenderer?: any; +} + +const SelectMenuHeader = (props: Props) => { + const menuOptions: SelectMenuItem[] = props.options.map((opt) => ({ + label: opt, + value: opt + })); + + return ( + + + props.setSelected((curr) => [...curr, item.value.toString()]) + } + onDeselect={(item) => + props.setSelected((curr) => + curr.filter((filter) => filter !== item.value) + ) + }> + + {props.columnTitle.toUpperCase()} + + + + ); +}; + +// Closure over ServerMenuItem so we can pass in a list of online servers +// in order to set the status indicator when the server is online +export const makeStatusMenuItem = (onlineServers: string[]) => { + return function (props: any) { + return ( + + ); + }; +}; + +// Use the default Option menu item but augment it with a status indicator. +// Use "any" because evergreen doesn't export a type for the the + ); +}; + +export default SelectMenuHeader; diff --git a/server/app/src/components/Pulse.css b/server/app/src/components/Pulse.css deleted file mode 100644 index 707d8ec..0000000 --- a/server/app/src/components/Pulse.css +++ /dev/null @@ -1,31 +0,0 @@ -.center { - display: flex; - justify-content: space-evenly; - align-items: center; - } - - .circle { - background:#1070CA; - width: 10px; - height: 10px; - border-radius: 50%; - box-shadow: 0px 0px 1px 1px #0000001a; - } - - .pulse { - animation: pulse-animation 2s infinite; - } - - .disabled { - background: gray; - animation: none; - } - - @keyframes pulse-animation { - 0% { - box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2); - } - 100% { - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); - } - } diff --git a/server/app/src/components/Pulse.tsx b/server/app/src/components/Pulse.tsx deleted file mode 100644 index 8193a46..0000000 --- a/server/app/src/components/Pulse.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Position, Tooltip } from 'evergreen-ui'; -import React from 'react'; -import "./Pulse.css"; - -type Props = { - disabled?: boolean; -} - -const Pulse: React.FC = ({ disabled = false }) => { - return ( - -
-
-
-
- ); -} - -export default Pulse; diff --git a/server/app/src/components/SearchHistory.tsx b/server/app/src/components/SearchHistory.tsx deleted file mode 100644 index 8511293..0000000 --- a/server/app/src/components/SearchHistory.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Dispatch } from "@reduxjs/toolkit"; -import { Badge, EyeOffIcon, EyeOpenIcon, Menu, Pane, Popover, Position, SearchIcon, Spinner, Text, TrashIcon } from 'evergreen-ui'; -import React from 'react'; -import { useDispatch, useSelector } from "react-redux"; -import { deleteHistoryItem, HistoryItem, selectHistory } from "../state/historySlice"; -import { setActiveItem } from "../state/stateSlice"; -import { RootState } from "../state/store"; -import TogglePane from "./TogglePane"; - - -const SearchHistory: React.FC = () => { - const history = useSelector(selectHistory); - const activeTS = useSelector((store: RootState) => store.state.activeItem?.timestamp) ?? -1; - const dispatch = useDispatch(); - - return ( - <> - { - history.length > 0 ? history.map((item: HistoryItem) => - ) - : - History is a mystery. - - } - - ); -} - -type Props = { - activeTS: number; - item: HistoryItem; - dispatch: Dispatch; -} - -const HistoryCard: React.FC = ({ activeTS, item, dispatch }: Props) => { - const isActive = activeTS === item.timestamp; - - return ( - - {!isActive && - dispatch(setActiveItem(item))}> - Show - - } - {isActive && - dispatch(setActiveItem(null))}> - Hide - - } - dispatch(deleteHistoryItem(item.timestamp))} - intent="danger"> - Delete - - - }> - - - - {item.query} - - {!item.results?.length ? - : - - {`${item.results?.length} RESULTS`} - - } - - - ) -} - - - -export default SearchHistory; diff --git a/server/app/src/components/ServerList.tsx b/server/app/src/components/ServerList.tsx deleted file mode 100644 index 10d52ff..0000000 --- a/server/app/src/components/ServerList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Badge, DatabaseIcon, Pane, Text } from 'evergreen-ui'; -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { selectServers } from "../state/serverSlice"; -import { toggleServerFilter } from '../state/stateSlice'; -import { RootState } from '../state/store'; -import TogglePane from './TogglePane'; - -const ServerList: React.FC = () => { - const dispatch = useDispatch(); - const servers = useSelector(selectServers); - const filters = useSelector((store: RootState) => new Set(store.state.serverFilters)); - - return ( - <> - - - {servers.length > 0 ? 'Click to filter.' : 'Servers not found.'} - - - {servers.map(name => - ( - dispatch(toggleServerFilter(name))} - key={name} - active={filters.has(name) ? 1 : 0} - border - display="flex" - alignItems="center" - padding={6} elevation={1} margin={6}> - - - {name} - {filters.has(name) && - - Active - - } - - - ) - )} - - ); -}; - -export default ServerList; diff --git a/server/app/src/components/SideBar/BookList.tsx b/server/app/src/components/SideBar/BookList.tsx new file mode 100644 index 0000000..985b36a --- /dev/null +++ b/server/app/src/components/SideBar/BookList.tsx @@ -0,0 +1,86 @@ +import { + Text, + Badge, + Spinner, + Popover, + Position, + Menu, + Pane, + Tooltip +} from "evergreen-ui"; +import { Book, Download, Trash } from "phosphor-react"; +import React from "react"; +import { useDeleteBookMutation, useGetBooksQuery } from "../../state/api"; +import { downloadFile } from "../../state/util"; + +export default function BookList() { + const { data, isLoading, isSuccess } = useGetBooksQuery(null); + const [deleteBook] = useDeleteBookMutation(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isSuccess && data?.length === 0) { + return ( + + + No previous downloads. + + + ); + } + + return ( + <> + {data?.map((book) => ( + + } + onClick={() => downloadFile(book.downloadLink)}> + Download + + } + intent="danger" + onClick={() => deleteBook(book.name)}> + Delete + + + }> +
+ + + + {book.name} + + + + {new Date(book.time).toLocaleDateString("en-US")} + +
+
+ ))} + + ); +} diff --git a/server/app/src/components/SideBar/NotificationDrawer.tsx b/server/app/src/components/SideBar/NotificationDrawer.tsx new file mode 100644 index 0000000..7be00f1 --- /dev/null +++ b/server/app/src/components/SideBar/NotificationDrawer.tsx @@ -0,0 +1,77 @@ +import { SideSheet, Alert, IntentTypes, IconButton, Text } from "evergreen-ui"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + clearNotifications, + dismissNotification, + toggleDrawer +} from "../../state/notificationSlice"; +import { RootState } from "../../state/store"; +import { NotificationType } from "../../state/models"; +import { BellSimpleSlash } from "phosphor-react"; + +const NotificationDrawer = () => { + const { isOpen, notifications } = useSelector( + (store: RootState) => store.notifications + ); + const dispatch = useDispatch(); + + const getIntent = (type: NotificationType): IntentTypes => { + switch (type) { + case NotificationType.NOTIFY: + return "none"; + case NotificationType.DANGER: + return "danger"; + case NotificationType.SUCCESS: + return "success"; + case NotificationType.WARNING: + return "warning"; + } + }; + + return ( + dispatch(toggleDrawer())}> +
+

Notifications

+ + } + onClick={() => dispatch(clearNotifications())}> +
+
+ {notifications.length === 0 ? ( + + No notifications. + + ) : ( + notifications.map((notif) => ( +
+

+ {new Date(notif.timestamp).toLocaleTimeString("en-US", { + timeStyle: "short" + })} +

+ dispatch(dismissNotification(notif))} + isRemoveable> + {notif.detail} + +
+ )) + )} +
+
+ ); +}; + +export default NotificationDrawer; diff --git a/server/app/src/components/SideBar/Pulse.tsx b/server/app/src/components/SideBar/Pulse.tsx new file mode 100644 index 0000000..5e789e3 --- /dev/null +++ b/server/app/src/components/SideBar/Pulse.tsx @@ -0,0 +1,32 @@ +import clsx from "clsx"; +import { Tooltip, Position } from "evergreen-ui"; +import { BellSimple } from "phosphor-react"; +import React from "react"; + +interface PulseProps { + enabled: boolean; + onClick: React.MouseEventHandler; +} + +const Pulse = ({ enabled, onClick }: PulseProps) => { + return ( + +
+ +
+
+ ); +}; + +export default Pulse; diff --git a/server/app/src/components/SideBar/SearchHistory.tsx b/server/app/src/components/SideBar/SearchHistory.tsx new file mode 100644 index 0000000..f97a004 --- /dev/null +++ b/server/app/src/components/SideBar/SearchHistory.tsx @@ -0,0 +1,114 @@ +import { Dispatch } from "@reduxjs/toolkit"; +import clsx from "clsx"; +import { + Badge, + Menu, + Pane, + Popover, + Position, + Spinner, + Text +} from "evergreen-ui"; +import { Eye, EyeSlash, MagnifyingGlass, Trash } from "phosphor-react"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + deleteHistoryItem, + HistoryItem, + selectHistory +} from "../../state/historySlice"; +import { setActiveItem } from "../../state/stateSlice"; +import { RootState } from "../../state/store"; + +const SearchHistory: React.FC = () => { + const history = useSelector(selectHistory); + const activeTS = + useSelector((store: RootState) => store.state.activeItem?.timestamp) ?? -1; + const dispatch = useDispatch(); + + return ( + <> + {history.length > 0 ? ( + history.map((item: HistoryItem) => ( + + )) + ) : ( + + + History is a mystery. + + + )} + + ); +}; + +type Props = { + activeTS: number; + item: HistoryItem; + dispatch: Dispatch; +}; + +const HistoryCard: React.FC = ({ activeTS, item, dispatch }: Props) => { + const isActive = activeTS === item.timestamp; + + return ( + + {!isActive && ( + } + onClick={() => dispatch(setActiveItem(item))}> + Show + + )} + {isActive && ( + } + onClick={() => dispatch(setActiveItem(null))}> + Hide + + )} + } + onClick={() => dispatch(deleteHistoryItem(item.timestamp))} + intent="danger"> + Delete + + + }> +
+ + + {item.query} + + {!item.results?.length ? ( + + ) : ( + {`${item.results?.length} RESULTS`} + )} +
+
+ ); +}; + +export default SearchHistory; diff --git a/server/app/src/components/SideBar/Sidebar.tsx b/server/app/src/components/SideBar/Sidebar.tsx new file mode 100644 index 0000000..a43fab4 --- /dev/null +++ b/server/app/src/components/SideBar/Sidebar.tsx @@ -0,0 +1,69 @@ +import { Heading, Pane, Paragraph, Tab, Tablist } from "evergreen-ui"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import useLocalStorageState from "use-local-storage-state"; +import { RootState } from "../../state/store"; +import SearchHistory from "./SearchHistory"; +import Pulse from "./Pulse"; +import NotificationDrawer from "./NotificationDrawer"; +import BookList from "./BookList"; +import { toggleDrawer } from "../../state/notificationSlice"; +import { IdentificationBadge } from "phosphor-react"; + +const Sidebar: React.FC = () => { + const [selectedIndex, setIndex] = useLocalStorageState("index", 0); + const connected = useSelector((store: RootState) => store.state.isConnected); + const username = useSelector((store: RootState) => store.state.username); + const dispatch = useDispatch(); + + return ( + <> + + + + + OpenBooks + dispatch(toggleDrawer())} + /> + + + Download eBooks from IRC Highway + + + + + {["Search History", "Previous Downloads"].map((tab, index) => ( + setIndex(index)}> + {tab} + + ))} + + + + + {selectedIndex === 0 ? : } + + {username && ( +
+ + {username} +
+ )} +
+ + + + ); +}; + +export default Sidebar; diff --git a/server/app/src/components/Sidebar.tsx b/server/app/src/components/Sidebar.tsx deleted file mode 100644 index 65f3aea..0000000 --- a/server/app/src/components/Sidebar.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Heading, Pane, Paragraph, Tab, Tablist } from 'evergreen-ui'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import styled from "styled-components"; -import useLocalStorageState from "use-local-storage-state"; -import { RootState } from '../state/store'; -import Pulse from './Pulse'; -import SearchHistory from './SearchHistory'; -import ServerList from './ServerList'; - -const SidebarContent = styled(Pane)` - max-height: calc(100vh - 78px - 44px); -`; - -const Sidebar: React.FC = () => { - const [selectedIndex, setIndex] = useLocalStorageState("index", 0); - const connected = useSelector((store: RootState) => store.state.isConnected); - - return ( - <> - - - - - OpenBooks - - - - Download eBooks from IRC Highway - - - - - {['Search History', 'Online Servers'].map( - (tab, index) => ( - setIndex(index)}> - {tab} - )) - } - - - - - {selectedIndex === 0 ? : } - - - - ) -} - -export default Sidebar; diff --git a/server/app/src/components/TogglePane.tsx b/server/app/src/components/TogglePane.tsx deleted file mode 100644 index fdba9f4..0000000 --- a/server/app/src/components/TogglePane.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Pane } from "evergreen-ui"; -import styled, { css } from "styled-components"; - -const TogglePane = styled(Pane)` - ${props => props.active - ? css`border-left: 3px solid #1070CA;` - : css`border-left: 1px solid #E4E7EB;` - } -` - -export default TogglePane; diff --git a/server/app/src/index.css b/server/app/src/index.css index ec2585e..1896cf6 100644 --- a/server/app/src/index.css +++ b/server/app/src/index.css @@ -11,3 +11,7 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/server/app/src/models/messages.ts b/server/app/src/models/messages.ts index 5fb1cdd..81280a0 100644 --- a/server/app/src/models/messages.ts +++ b/server/app/src/models/messages.ts @@ -3,9 +3,9 @@ export enum MessageType { CONNECT = 2, SEARCH = 3, DOWNLOAD = 4, - SERVERS = 5, - WAIT = 6, - IRCERROR = 7 + // SERVERS = 5, + WAIT = 5, + IRCERROR = 6 } export interface BookResponse { diff --git a/server/app/src/pages/SearchPage.tsx b/server/app/src/pages/SearchPage.tsx index 9a6dfec..9b60377 100644 --- a/server/app/src/pages/SearchPage.tsx +++ b/server/app/src/pages/SearchPage.tsx @@ -1,51 +1,61 @@ -import { Button, Pane, SearchInput } from 'evergreen-ui'; -import React, { FormEvent, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import styled from 'styled-components'; -import { BooksGrid } from '../components/BooksGrid'; -import { sendSearch } from '../state/stateSlice'; -import { RootState } from '../state/store'; +import { Button, majorScale, Pane, SearchInput } from "evergreen-ui"; +import React, { FormEvent, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { BooksGrid } from "../components/BooksGrid/BooksGrid"; +import { sendSearch } from "../state/stateSlice"; +import { RootState } from "../state/store"; -const SearchPage: React.FC = () => { - const dispatch = useDispatch(); - const [searchQuery, setSearchQuery] = useState(""); - const activeItem = useSelector((store: RootState) => store.state.activeItem); +const SearchPage = () => { + const dispatch = useDispatch(); + const [searchQuery, setSearchQuery] = useState(""); + const activeItem = useSelector((store: RootState) => store.state.activeItem); - const searchHandler = (event: FormEvent) => { - event.preventDefault(); - if (searchQuery === "") return; - dispatch(sendSearch(searchQuery)); - setSearchQuery(""); - } + const searchHandler = (event: FormEvent) => { + event.preventDefault(); + if (searchQuery === "") return; + dispatch(sendSearch(searchQuery)); + setSearchQuery(""); + }; - return ( - - -
searchHandler(e)}> - setSearchQuery(e.target.value)} - placeholder="Search for a book." - height={40} - marginRight={24} - width="100%"> - - -
- -
-
- ) -} - -const Form = styled.form` - width: 100%; - display: flex; - flex-flow: row nowrap; - justify-content: center; - align-items: center; - margin: 24px; -` + return ( + +
searchHandler(e)}> + setSearchQuery(e.target.value)} + placeholder="Search for a book." + height={majorScale(5)} + className="rounded-md" + marginRight={majorScale(4)} + width="100%"> + +
+ +
+ ); +}; export default SearchPage; diff --git a/server/app/src/state/api.ts b/server/app/src/state/api.ts new file mode 100644 index 0000000..c63da97 --- /dev/null +++ b/server/app/src/state/api.ts @@ -0,0 +1,45 @@ +import { ResultDescription } from "@reduxjs/toolkit/dist/query/endpointDefinitions"; +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { getApiURL } from "./util"; + +export interface IrcServer { + elevatedUsers?: string[]; + regularUsers?: string[]; +} + +export interface Book { + name: string; + downloadLink: string; + time: string; +} + +export const openbooksApi = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: getApiURL().href, + credentials: "include", + mode: "cors" + }), + tagTypes: ["books", "servers"], + endpoints: (builder) => ({ + getServers: builder.query({ + query: () => `servers`, + transformResponse: (ircServers: IrcServer) => { + return ircServers.elevatedUsers ?? []; + } + }), + getBooks: builder.query({ + query: () => `library`, + providesTags: ["books"] + }), + deleteBook: builder.mutation({ + query: (book) => ({ + url: `library/${book}`, + method: "DELETE" + }), + invalidatesTags: ["books"] + }) + }) +}); + +export const { useGetServersQuery, useGetBooksQuery, useDeleteBookMutation } = + openbooksApi; diff --git a/server/app/src/state/models.ts b/server/app/src/state/models.ts new file mode 100644 index 0000000..72587b1 --- /dev/null +++ b/server/app/src/state/models.ts @@ -0,0 +1,13 @@ +export enum NotificationType { + NOTIFY, + SUCCESS, + WARNING, + DANGER +} + +export interface Notification { + type: NotificationType; + title: string; + detail?: string; + timestamp: number; +} diff --git a/server/app/src/state/notificationSlice.ts b/server/app/src/state/notificationSlice.ts new file mode 100644 index 0000000..3f92655 --- /dev/null +++ b/server/app/src/state/notificationSlice.ts @@ -0,0 +1,41 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { Notification } from "./models"; + +interface NotificationState { + isOpen: boolean; + notifications: Notification[]; +} + +const initialState: NotificationState = { + isOpen: false, + notifications: [] +}; + +const notificationSlice = createSlice({ + name: "notifications", + initialState, + reducers: { + addNotification(state, action: PayloadAction) { + state.notifications = [action.payload, ...state.notifications]; + }, + dismissNotification(state, action: PayloadAction) { + state.notifications = state.notifications.filter( + (x) => x.timestamp !== action.payload.timestamp + ); + }, + toggleDrawer(state) { + state.isOpen = !state.isOpen; + }, + clearNotifications(state) { + state.notifications = []; + } + } +}); + +export const { + addNotification, + dismissNotification, + toggleDrawer, + clearNotifications +} = notificationSlice.actions; +export default notificationSlice.reducer; diff --git a/server/app/src/state/notifications.ts b/server/app/src/state/notifications.ts deleted file mode 100644 index 7cf506e..0000000 --- a/server/app/src/state/notifications.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { toaster } from "evergreen-ui"; - -export enum NotificationType { - NOTIFY, - SUCCESS, - WARNING, - DANGER -} - -export const displayNotification = (type: NotificationType = NotificationType.NOTIFY, message: string, subText?: string) => { - switch (type) { - case NotificationType.NOTIFY: - toaster.notify(message, { - description: subText - }); - break; - case NotificationType.SUCCESS: - toaster.success(message, { - description: subText - }); - break; - case NotificationType.WARNING: - toaster.warning(message, { - description: subText - }); - break; - case NotificationType.DANGER: - toaster.danger(message, { - description: subText - }); - break; - } -} diff --git a/server/app/src/state/serverSlice.ts b/server/app/src/state/serverSlice.ts deleted file mode 100644 index ee12c2e..0000000 --- a/server/app/src/state/serverSlice.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { RootState } from "./store"; - -interface ServerState { - servers: string[]; -} - -const initialState: ServerState = { - servers: [] -} - -export const serverSlice = createSlice({ - name: "servers", - initialState, - reducers: { - setServers(state, action: PayloadAction) { - state.servers = action.payload; - } - } -}); - -export const { setServers } = serverSlice.actions; - -export const selectServers = (state: RootState) => state.servers.servers; - -export default serverSlice.reducer; diff --git a/server/app/src/state/socketMiddleware.ts b/server/app/src/state/socketMiddleware.ts index 25e64cb..012dd07 100644 --- a/server/app/src/state/socketMiddleware.ts +++ b/server/app/src/state/socketMiddleware.ts @@ -1,89 +1,120 @@ import { AnyAction, PayloadAction, Store } from "@reduxjs/toolkit"; import { BookDetail, BookResponse, MessageType } from "../models/messages"; -import { displayNotification, NotificationType } from "./notifications"; -import { setServers } from "./serverSlice"; -import { sendMessage, setConnectionState, setSearchResults } from "./stateSlice"; +import { displayNotification, downloadFile } from "./util"; +import { + sendMessage, + setConnectionState, + setSearchResults, + setUsername +} from "./stateSlice"; +import { addNotification } from "./notificationSlice"; +import { Notification, NotificationType } from "./models"; +import { openbooksApi } from "./api"; -// Web socket redux middleware. -// Listens to socket and dispatches handlers. +// Web socket redux middleware. +// Listens to socket and dispatches handlers. // Handles send_message actions by sending to socket. export const websocketConn = (wsUrl: string): any => { - return (store: Store) => { - console.log(store); - const socket = new WebSocket(wsUrl); + return (store: Store) => { + const socket = new WebSocket(wsUrl); - socket.onopen = () => onOpen(store); - socket.onclose = () => onClose(store); - socket.onmessage = (message) => route(store, message); - socket.onerror = (event) => console.error(event); + socket.onopen = () => onOpen(store); + socket.onclose = () => onClose(store); + socket.onmessage = (message) => route(store, message); + socket.onerror = (event) => console.error(event); - return (next: any) => (action: PayloadAction) => { - // Send Message action? Send data to the socket. - if (sendMessage.match(action)) { - if (socket.readyState === socket.OPEN) { - socket.send(action.payload.message) - } else { - displayNotification(NotificationType.WARNING, "Server connection closed. Reload page."); - } - } - - return next(action); + return (next: any) => (action: PayloadAction) => { + // Send Message action? Send data to the socket. + if (sendMessage.match(action)) { + if (socket.readyState === socket.OPEN) { + socket.send(action.payload.message); + } else { + displayNotification({ + type: NotificationType.WARNING, + title: "Server connection closed. Reload page.", + timestamp: new Date().getTime() + }); } - } -} + } -const route = (store: Store, msg: MessageEvent): void => { - console.log(msg); + return next(action); + }; + }; +}; +const onOpen = (store: Store): void => { + console.log("WebSocket connected."); + store.dispatch(setConnectionState(true)); + store.dispatch(sendMessage({ type: MessageType.CONNECT, payload: {} })); +}; + +const onClose = (store: Store): void => { + console.log("WebSocket closed."); + store.dispatch(setConnectionState(false)); +}; + +const route = (store: Store, msg: MessageEvent): void => { + const getNotif = (): Notification => { let response = JSON.parse(msg.data) as BookResponse; + const timestamp = new Date().getTime(); switch (response.type) { - // TODO: How to get the message type with typed properties - case MessageType.ERROR: - displayNotification(NotificationType.DANGER, response.details) - break; - case MessageType.CONNECT: - displayNotification(NotificationType.SUCCESS, "Welcome, connection established."); - store.dispatch(sendMessage({ type: MessageType.SERVERS, payload: {} })); - break; - case MessageType.SEARCH: - console.log("search results") - store.dispatch((setSearchResults(response.books as BookDetail[]) as unknown) as AnyAction); - displayNotification(NotificationType.SUCCESS, "Search results received.") - break; - case MessageType.DOWNLOAD: - displayNotification(NotificationType.SUCCESS, "Book file received.", response.name); - saveByteArray(response.name, response.file); - break; - case MessageType.SERVERS: - store.dispatch(setServers(response.servers)) - break; - case MessageType.WAIT: - displayNotification(NotificationType.SUCCESS, response.status) - break; - case MessageType.IRCERROR: - displayNotification(NotificationType.DANGER, "IRC Error. Try again.") - break; - default: - console.error(response); - displayNotification(NotificationType.DANGER, "Unknown error type. See console.") + case MessageType.ERROR: + return { + type: NotificationType.DANGER, + title: response.details, + timestamp + }; + case MessageType.CONNECT: + store.dispatch(setUsername(response.name)); + return { + type: NotificationType.SUCCESS, + title: "Welcome, connection established.", + detail: `IRC username ${response.name}`, + timestamp + }; + case MessageType.SEARCH: + store.dispatch( + setSearchResults( + response.books as BookDetail[] + ) as unknown as AnyAction + ); + return { + type: NotificationType.SUCCESS, + title: "Search results received.", + timestamp + }; + case MessageType.DOWNLOAD: + downloadFile(response.downloadLink); + store.dispatch(openbooksApi.util.invalidateTags(["books"])); + return { + type: NotificationType.SUCCESS, + title: "Book file received.", + detail: response.name, + timestamp + }; + case MessageType.WAIT: + return { + type: NotificationType.NOTIFY, + title: response.status, + timestamp + }; + case MessageType.IRCERROR: + return { + type: NotificationType.DANGER, + title: "IRC Error. Try again.", + timestamp + }; + default: + console.error(response); + return { + type: NotificationType.DANGER, + title: "Unknown message type. See console.", + timestamp + }; } -} + }; -const onOpen = (store: Store): void => { - console.log("WebSocket connected."); - store.dispatch(setConnectionState(true)); - store.dispatch(sendMessage({ type: MessageType.CONNECT, payload: {} })); -} - -// saveByteArray creates a link and download popup for the returned file -function saveByteArray(fileName: string, byte: Blob) { - let link = document.createElement('a'); - link.href = `data:application/octet-stream;base64,${byte}`; - link.download = fileName; - link.click(); + const notif = getNotif(); + store.dispatch(addNotification(notif)); + displayNotification(notif); }; - -const onClose = (store: Store): void => { - console.log("WebSocket closed."); - store.dispatch(setConnectionState(false)); -} diff --git a/server/app/src/state/stateSlice.ts b/server/app/src/state/stateSlice.ts index 6ea68e9..64e7733 100644 --- a/server/app/src/state/stateSlice.ts +++ b/server/app/src/state/stateSlice.ts @@ -4,89 +4,93 @@ import { addHistoryItem, HistoryItem, updateHistoryItem } from "./historySlice"; import { AppThunk } from "./store"; interface AppState { - isConnected: boolean; - serverFilters: string[]; - activeItem: HistoryItem | null; + isConnected: boolean; + activeItem: HistoryItem | null; + username?: string; } +const loadActive = (): HistoryItem | null => { + try { + return JSON.parse(localStorage.getItem("active")!) ?? null; + } catch (err) { + return null; + } +}; + const initialState: AppState = { - isConnected: false, - serverFilters: [], - activeItem: null -} + isConnected: false, + activeItem: loadActive(), + username: undefined +}; const stateSlice = createSlice({ - name: "state", - initialState, - reducers: { - toggleServerFilter(state, action: PayloadAction) { - const serverName = action.payload; - const filterSet = new Set(state.serverFilters); - if (filterSet.has(serverName)) { - filterSet.delete(serverName); - } else { - filterSet.add(serverName); - } - state.serverFilters = [...filterSet]; - }, - setActiveItem(state, action: PayloadAction) { - state.activeItem = action.payload; - }, - setConnectionState(state, action: PayloadAction) { - state.isConnected = action.payload; - } + name: "state", + initialState, + reducers: { + setActiveItem(state, action: PayloadAction) { + state.activeItem = action.payload; }, + setConnectionState(state, action: PayloadAction) { + state.isConnected = action.payload; + }, + setUsername(state, action: PayloadAction) { + state.username = action.payload; + } + } }); // Action that sends a websocket message to the server -const sendMessage = createAction( - "socket/send_message", - (message: any) => ({ - payload: { message: JSON.stringify(message) } - }) -); +const sendMessage = createAction("socket/send_message", (message: any) => ({ + payload: { message: JSON.stringify(message) } +})); // Send a search to the server. Add to query history and set loading. -const sendSearch = (queryString: string): AppThunk => dispatch => { +const sendSearch = + (queryString: string): AppThunk => + (dispatch) => { // Send the books search query to the server - dispatch(sendMessage({ + dispatch( + sendMessage({ type: MessageType.SEARCH, payload: { - query: queryString + query: queryString } - })); + }) + ); const timestamp = new Date().getTime(); // Add query to item history. dispatch(addHistoryItem({ query: queryString, timestamp })); dispatch(setActiveItem({ query: queryString, timestamp: timestamp })); -} + }; -const setSearchResults = (results: BookDetail[]): AppThunk => (dispatch, getStore) => { +const setSearchResults = + (results: BookDetail[]): AppThunk => + (dispatch, getStore) => { const activeItem = getStore().state.activeItem; if (activeItem === null) { - return; + return; } const updatedItem: HistoryItem = { - query: activeItem.query, - timestamp: activeItem.timestamp, - results: results - } + query: activeItem.query, + timestamp: activeItem.timestamp, + results: results + }; dispatch(setActiveItem(updatedItem)); - dispatch(updateHistoryItem(updatedItem)) -} -const { toggleServerFilter, setActiveItem, setConnectionState } = stateSlice.actions; + dispatch(updateHistoryItem(updatedItem)); + }; +const { setActiveItem, setConnectionState, setUsername } = stateSlice.actions; export { - stateSlice, - sendMessage, - toggleServerFilter, - setActiveItem, - setConnectionState, - sendSearch, - setSearchResults, + stateSlice, + sendMessage, + setActiveItem, + setConnectionState, + sendSearch, + setSearchResults, + setUsername }; export default stateSlice.reducer; diff --git a/server/app/src/state/store.ts b/server/app/src/state/store.ts index bf39c6a..b105a01 100644 --- a/server/app/src/state/store.ts +++ b/server/app/src/state/store.ts @@ -1,44 +1,50 @@ import { Action, configureStore, ThunkAction } from "@reduxjs/toolkit"; import throttle from "lodash/throttle"; import historyReducer from "./historySlice"; +import notificationReducer from "./notificationSlice"; import { websocketConn } from "./socketMiddleware"; -import serverReducer from "./serverSlice"; import stateReducer from "./stateSlice"; import { enableMapSet } from "immer"; - -const websocketURL = new URL(window.location.href + "ws") -if (websocketURL.protocol.startsWith("https")) { - websocketURL.protocol = websocketURL.protocol.replace("https", "wss"); -} else { - websocketURL.protocol = websocketURL.protocol.replace("http", "ws"); -} +import { getWebsocketURL } from "./util"; +import { openbooksApi } from "./api"; +import { setupListeners } from "@reduxjs/toolkit/dist/query"; enableMapSet(); export const store = configureStore({ - reducer: { - state: stateReducer, - history: historyReducer, - servers: serverReducer - }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(websocketConn(websocketURL.href)) + reducer: { + state: stateReducer, + history: historyReducer, + notifications: notificationReducer, + [openbooksApi.reducerPath]: openbooksApi.reducer + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat( + websocketConn(getWebsocketURL().href), + openbooksApi.middleware + ) }); +setupListeners(store.dispatch); + const saveState = (key: string, state: any): void => { - try { - const serialized = JSON.stringify(state); - localStorage.setItem(key, serialized); - } catch (err) { } -} + try { + const serialized = JSON.stringify(state); + localStorage.setItem(key, serialized); + } catch (err) {} +}; -store.subscribe(throttle(() => { +store.subscribe( + throttle(() => { saveState("history", store.getState().history.items); -}, 1000)); + saveState("active", store.getState().state.activeItem); + }, 1000) +); export type RootState = ReturnType; export type AppThunk = ThunkAction< - ReturnType, - RootState, - unknown, - Action + ReturnType, + RootState, + unknown, + Action >; diff --git a/server/app/src/state/util.ts b/server/app/src/state/util.ts new file mode 100644 index 0000000..4d1a09c --- /dev/null +++ b/server/app/src/state/util.ts @@ -0,0 +1,67 @@ +import { toaster } from "evergreen-ui"; +import { Notification, NotificationType } from "./models"; + +export const getWebsocketURL = (): URL => { + const websocketURL = new URL(window.location.href + "ws"); + if (websocketURL.protocol.startsWith("https")) { + websocketURL.protocol = websocketURL.protocol.replace("https", "wss"); + } else { + websocketURL.protocol = websocketURL.protocol.replace("http", "ws"); + } + + if (import.meta.env.DEV) { + websocketURL.port = "5228"; + } + + return websocketURL; +}; + +export const getApiURL = (): URL => { + const apiURL = new URL(window.location.href); + if (import.meta.env.DEV) { + apiURL.port = "5228"; + } + + return apiURL; +}; + +export const displayNotification = ({ + type = NotificationType.NOTIFY, + title, + detail +}: Notification) => { + switch (type) { + case NotificationType.NOTIFY: + toaster.notify(title, { + description: detail + }); + break; + case NotificationType.SUCCESS: + toaster.success(title, { + description: detail + }); + break; + case NotificationType.WARNING: + toaster.warning(title, { + description: detail + }); + break; + case NotificationType.DANGER: + toaster.danger(title, { + description: detail + }); + break; + } +}; + +export const downloadFile = (relativeURL: string) => { + let url = getApiURL(); + url.pathname = relativeURL; + + let link = document.createElement("a"); + link.download = ""; + link.target = "_blank"; + link.href = url.href; + link.click(); + link.remove(); +}; diff --git a/server/app/tailwind.config.js b/server/app/tailwind.config.js new file mode 100644 index 0000000..37ec93d --- /dev/null +++ b/server/app/tailwind.config.js @@ -0,0 +1,20 @@ +module.exports = { + mode: "jit", + purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], + darkMode: false, // or 'media' or 'class' + theme: { + extend: { + colors: { + "custom-blue": "#3366ff", + "tint1": "#FAFBFF", + "active-blue": "#ebf0ff", + "active-text-blue": "#3366FF", + "hover-blue": "#f4f5f9" + } + } + }, + variants: { + extend: {} + }, + plugins: [] +}; diff --git a/server/client.go b/server/client.go index 40b4a2c..7a20def 100644 --- a/server/client.go +++ b/server/client.go @@ -1,9 +1,8 @@ package server import ( - "fmt" + "context" "log" - "net/http" "time" "github.com/evan-buss/openbooks/irc" @@ -36,59 +35,22 @@ type Client struct { // Unique ID for the client uuid uuid.UUID - // Global hub that manages client registrations - hub *Hub - // The websocket connection. conn *websocket.Conn // Signal to indicate the connection should be terminated. - disconnect chan struct{} + // disconnect chan struct{} // Message to send to the client ws connection send chan interface{} // Individual IRC connection per connected client. irc *irc.Conn -} - -func closeClient(c *Client) { - c.irc.Disconnect() - c.conn.Close() - c.hub.unregister <- c -} -// serveWs handles websocket requests from the peer. -func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { - upgrader.CheckOrigin = func(req *http.Request) bool { - return true - } - - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - return - } + log *log.Logger - // Each client gets its own connection, so use different usernames by appending count - name := fmt.Sprintf("%s-%d", config.UserName, *numConnections+1) - client := &Client{ - hub: hub, - conn: conn, - send: make(chan interface{}, 128), - disconnect: make(chan struct{}), - uuid: uuid.New(), - irc: irc.New(name, "OpenBooks - Search and download eBooks"), - } - - client.hub.register <- client - - // Allow collection of memory referenced by the caller by doing all work in - // new goroutines. - go client.writePump() - go client.readPump() - - log.Printf("%s: Client connected from %s\n", client.uuid, conn.RemoteAddr().String()) + // Context is used to signal when this client should close. + ctx context.Context } // readPump pumps messages from the websocket connection to the hub. @@ -96,33 +58,31 @@ func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { // The application runs readPump in a per-connection goroutine. The application // ensures that there is at most one reader on a connection by executing all // reads from this goroutine. -func (c *Client) readPump() { +func (s *server) readPump(c *Client) { defer func() { - closeClient(c) + c.irc.Disconnect() + c.conn.Close() + s.unregister <- c }() c.conn.SetReadLimit(maxMessageSize) c.conn.SetReadDeadline(time.Now().Add(pongWait)) c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { select { - case <-c.disconnect: + case <-c.ctx.Done(): return default: var request Request err := c.conn.ReadJSON(&request) - log.Printf("%s: %s Message Received\n", c.uuid.String(), messageToString(request.RequestType)) - if err != nil { - if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("%s: Close error: %v", c.uuid.String(), err) - } else { - log.Printf("%s: Client Disconnected.\n", c.uuid.String()) - } + c.log.Printf("Connection Closed: %v", err) return } - c.routeMessage(request) + c.log.Printf("%s Message Received\n", messageToString(request.RequestType)) + + s.routeMessage(request, c) } } } @@ -132,11 +92,10 @@ func (c *Client) readPump() { // A goroutine running writePump is started for each connection. The // application ensures that there is at most one writer to a connection by // executing all writes from this goroutine. -func (c *Client) writePump() { +func (s *server) writePump(c *Client) { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() - closeClient(c) }() for { @@ -151,10 +110,10 @@ func (c *Client) writePump() { err := c.conn.WriteJSON(message) if err != nil { - log.Printf("%s: Error writing JSON to websocket: %s", c.uuid.String(), err.Error()) + c.log.Printf("Error writing JSON to websocket: %s\n", err) return } - case <-c.disconnect: + case <-c.ctx.Done(): return case <-ticker.C: c.conn.SetWriteDeadline(time.Now().Add(writeWait)) diff --git a/server/hub.go b/server/hub.go deleted file mode 100644 index 6eead1c..0000000 --- a/server/hub.go +++ /dev/null @@ -1,54 +0,0 @@ -package server - -import ( - "sync/atomic" -) - -// Hub maintains the set of active clients and broadcasts messages to the -// clients. -type Hub struct { - // Registered clients. - clients map[*Client]bool - - // Inbound messages from the clients. - shutdown chan struct{} - - // Register requests from the clients. - register chan *Client - - // Unregister requests from clients. - unregister chan *Client -} - -func newHub() *Hub { - return &Hub{ - shutdown: make(chan struct{}), - register: make(chan *Client), - unregister: make(chan *Client), - clients: make(map[*Client]bool), - } -} - -func (h *Hub) run() { - for { - select { - case client := <-h.register: - atomic.AddInt32(numConnections, 1) - h.clients[client] = true - case client := <-h.unregister: - if _, ok := h.clients[client]; ok { - atomic.AddInt32(numConnections, -1) - close(client.send) - close(client.disconnect) - delete(h.clients, client) - } - case <-h.shutdown: - for client := range h.clients { - close(client.send) - close(client.disconnect) - delete(h.clients, client) - } - return - } - } -} diff --git a/server/irc_events.go b/server/irc_events.go new file mode 100644 index 0000000..d742f2f --- /dev/null +++ b/server/irc_events.go @@ -0,0 +1,122 @@ +package server + +import ( + "os" + "path" + + "github.com/evan-buss/openbooks/core" +) + +func (server *server) NewIrcEventHandler(client *Client) core.EventHandler { + handler := core.EventHandler{} + handler[core.SearchResult] = client.searchResultHandler(server.config.DownloadDir) + handler[core.BookResult] = client.bookResultHandler(server.config.DownloadDir) + handler[core.NoResults] = client.noResultsHandler + handler[core.BadServer] = client.badServerHandler + handler[core.SearchAccepted] = client.searchAcceptedHandler + handler[core.MatchesFound] = client.matchesFoundHandler + handler[core.Ping] = client.pingHandler + handler[core.ServerList] = client.userListHandler(server.repository) + return handler +} + +// searchResultHandler downloads from DCC server, parses data, and sends data to client +func (c *Client) searchResultHandler(downloadDir string) core.HandlerFunc { + return func(text string) { + extractedPath, err := core.DownloadExtractDCCString(path.Join(downloadDir, "books"), text, nil) + if err != nil { + c.log.Println(err) + } + + results, errors := core.ParseSearchFile(extractedPath) + // Output all errors so parser can be improved over time + if len(errors) > 0 { + c.log.Printf("%d Search Result Parsing Errors\n", len(errors)) + for _, err := range errors { + c.log.Println(err) + } + } + + if len(results) == 0 { + c.noResultsHandler(text) + return + } + + c.log.Printf("Sending %d search results.\n", len(results)) + c.send <- SearchResponse{ + MessageType: SEARCH, + Books: results, + } + + err = os.Remove(extractedPath) + if err != nil { + c.log.Printf("Error deleting search results file: %v", err) + } + } +} + +// bookResultHandler downloads the book file and sends it over the websocket +func (c *Client) bookResultHandler(downloadDir string) core.HandlerFunc { + return func(text string) { + extractedPath, err := core.DownloadExtractDCCString(path.Join(downloadDir, "books"), text, nil) + if err != nil { + c.log.Println(err) + c.send <- ErrorResponse{ + Error: ERROR, + Details: err.Error(), + } + return + } + + fileName := path.Base(extractedPath) + + c.log.Printf("Sending book entitled '%s'.\n", fileName) + c.send <- DownloadResponse{ + MessageType: DOWNLOAD, + Name: fileName, + DownloadLink: path.Join("/static/library", fileName), + } + } +} + +// NoResults is called when the server returns that nothing was found for the query +func (c *Client) noResultsHandler(_ string) { + c.send <- IrcErrorResponse{ + MessageType: IRCERROR, + Status: "No results found for the query.", + } +} + +// BadServer is called when the requested download fails because the server is not available +func (c *Client) badServerHandler(_ string) { + c.send <- IrcErrorResponse{ + MessageType: IRCERROR, + Status: "Server is not available. Try another one.", + } +} + +// SearchAccepted is called when the user's query is accepted into the search queue +func (c *Client) searchAcceptedHandler(_ string) { + c.send <- WaitResponse{ + MessageType: WAIT, + Status: "Search accepted into the queue.", + } +} + +// MatchesFound is called when the server finds matches for the user's query +func (c *Client) matchesFoundHandler(num string) { + c.send <- WaitResponse{ + MessageType: WAIT, + Status: "Found " + num + " results for your query.", + } +} + +func (c *Client) pingHandler(serverUrl string) { + c.irc.Pong(serverUrl) +} + +func (c *Client) userListHandler(repo *Repository) core.HandlerFunc { + return func(text string) { + repo.servers = core.ParseServers(text) + } +} diff --git a/server/irc_handler.go b/server/irc_handler.go deleted file mode 100644 index a23704e..0000000 --- a/server/irc_handler.go +++ /dev/null @@ -1,95 +0,0 @@ -package server - -import ( - "io/ioutil" - "log" - "os" - "path/filepath" - - "github.com/evan-buss/openbooks/core" - "github.com/evan-buss/openbooks/dcc" -) - -// Handler is the server implementation of the ReaderHandler interface. -type Handler struct { - *Client -} - -// DownloadSearchResults downloads from DCC server, parses data, and sends data to client -func (h Handler) DownloadSearchResults(text string) { - searchDownloaded := make(chan string) - // Download the file and wait until it is completed - go dcc.NewDownload(text, config.DownloadDir, searchDownloaded) - // Retrieve the file's location - fileLocation := <-searchDownloaded - - log.Printf("%s: Sending search results.\n", h.uuid.String()) - h.send <- SearchResponse{ - MessageType: SEARCH, - Books: core.ParseSearchFile(fileLocation), - } - - err := os.Remove(fileLocation) - if err != nil { - log.Printf("%s: Error deleting search results file: %v.\n", h.uuid, err) - } -} - -// DownloadBookFile downloads the book file and sends it over the websocket -func (h Handler) DownloadBookFile(text string) { - bookDownloaded := make(chan string) - go dcc.NewDownload(text, config.DownloadDir, bookDownloaded) - fileLocation := <-bookDownloaded - fileName := filepath.Base(fileLocation) - - data, err := ioutil.ReadFile(fileLocation) - if err != nil { - log.Printf("%s: Error reading book file: %v.\n", h.uuid, err) - } - - log.Printf("%s: Sending book entitled %s.\n", h.uuid.String(), fileName) - h.send <- DownloadResponse{ - MessageType: DOWNLOAD, - Name: fileName, - File: data, - } - - if !config.Persist { - err = os.Remove(fileLocation) - if err != nil { - log.Printf("%s: Error deleting search results file %v.\n", h.uuid, err) - } - } -} - -// NoResults is called when the server returns that nothing was found for the query -func (h Handler) NoResults() { - h.send <- IrcErrorResponse{ - MessageType: IRCERROR, - Status: "No results found for the query.", - } -} - -// BadServer is called when the requested download fails because the server is not available -func (h Handler) BadServer() { - h.send <- IrcErrorResponse{ - MessageType: IRCERROR, - Status: "Server is not available. Try another one.", - } -} - -// SearchAccepted is called when the user's query is accepted into the search queue -func (h Handler) SearchAccepted() { - h.send <- WaitResponse{ - MessageType: WAIT, - Status: "Search accepted into the queue.", - } -} - -// MatchesFound is called when the server finds matches for the user's query -func (h Handler) MatchesFound(num string) { - h.send <- WaitResponse{ - MessageType: WAIT, - Status: "Found " + num + " results for your query.", - } -} diff --git a/server/messages.go b/server/messages.go index f3992ea..279fb90 100644 --- a/server/messages.go +++ b/server/messages.go @@ -13,13 +13,12 @@ const ( CONNECT SEARCH DOWNLOAD - SERVERS WAIT IRCERROR ) func messageToString(s int) string { - name := []string{"INVALID", "ERROR", "CONNECT", "SEARCH", "DOWNLOAD", "SERVERS", "WAIT", "IRCERROR"} + name := []string{"INVALID", "ERROR", "CONNECT", "SEARCH", "DOWNLOAD", "WAIT", "IRCERROR"} i := uint8(s) switch { case i <= uint8(IRCERROR): @@ -48,6 +47,7 @@ type ConnectionRequest struct{} type ConnectionResponse struct { MessageType int `json:"type"` Status string `json:"status"` + Name string `json:"name"` Wait int `json:"wait"` } @@ -62,15 +62,6 @@ type SearchResponse struct { Books []core.BookDetail `json:"books"` } -// ServersRequest is a request that lists available IRC servers -type ServersRequest struct{} - -// ServersResponse is a response that lists the IRC servers that are online and available -type ServersResponse struct { - MessageType int `json:"type"` - Servers []string `json:"servers"` -} - // DownloadRequest is a request to download a specific book from the IRC server type DownloadRequest struct { Book string `json:"book"` @@ -78,9 +69,9 @@ type DownloadRequest struct { // DownloadResponse is a response that sends the requested book to the client type DownloadResponse struct { - MessageType int `json:"type"` - Name string `json:"name"` - File []byte `json:"file"` + MessageType int `json:"type"` + Name string `json:"name"` + DownloadLink string `json:"downloadLink"` } // WaitResponse is a response that reports status updates to the client. IRC is asynchronous diff --git a/server/middlewares.go b/server/middlewares.go new file mode 100644 index 0000000..89db080 --- /dev/null +++ b/server/middlewares.go @@ -0,0 +1,60 @@ +package server + +import ( + "context" + "log" + "net/http" + + "github.com/google/uuid" +) + +type userCtxKeyType string +type uuidCtxKeyType string + +const ( + userCtxKey userCtxKeyType = "user-client" + uuidCtxKey uuidCtxKeyType = "user-uuid" +) + +func (server *server) requireUser(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("OpenBooks") + if err != nil { + server.log.Println(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + userUUID, err := uuid.Parse(cookie.Value) + if err != nil { + server.log.Println(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), uuidCtxKey, userUUID))) + }) +} + +// getClient should only be called when requireUser is in the middleware chain. +func (server *server) getClient(ctx context.Context) *Client { + + user := getUUID(ctx) + if user == uuid.Nil { + return nil + } + + if client, ok := server.clients[user]; ok { + return client + } + + return nil +} + +func getUUID(ctx context.Context) uuid.UUID { + uid, ok := ctx.Value(uuidCtxKey).(uuid.UUID) + if !ok { + log.Println("Unable to find user.") + } + return uid +} diff --git a/server/repository.go b/server/repository.go new file mode 100644 index 0000000..a2a03d6 --- /dev/null +++ b/server/repository.go @@ -0,0 +1,11 @@ +package server + +import "github.com/evan-buss/openbooks/core" + +type Repository struct { + servers core.IrcServers +} + +func NewRepository() *Repository { + return &Repository{servers: core.IrcServers{}} +} diff --git a/server/routes.go b/server/routes.go index 6b2b71e..bdb7b50 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,47 +1,221 @@ package server import ( + "context" "embed" + "encoding/json" + "errors" "fmt" "io/fs" "log" "net/http" + "net/url" + "os" "path" + "time" + + "github.com/evan-buss/openbooks/irc" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" ) //go:embed app/dist var reactClient embed.FS -func registerRoutes(hub *Hub) { - basePath := getBaseRoute() - fmt.Printf("Base Path: %s\n", basePath) - - http.Handle(basePath, serveStaticFiles(basePath, "app/dist")) +func (server *server) registerRoutes() *chi.Mux { + router := chi.NewRouter() + router.Handle("/*", server.staticFilesHandler("app/dist")) + router.Get("/ws", server.serveWs()) + router.Get("/stats", server.statsHandler()) + router.Get("/servers", server.serverListHandler()) - http.HandleFunc(path.Join(basePath, "ws"), func(w http.ResponseWriter, r *http.Request) { - serveWs(hub, w, r) + router.Group(func(r chi.Router) { + r.Use(server.requireUser) + r.Get("/library", server.libraryHandler()) + r.Delete("/library/{fileName}", server.deleteLibraryFileHandler()) + r.Get("/static/library/*", server.libraryFileHandler("/static/library").ServeHTTP) + r.Get("/test", server.testHandler()) }) - http.HandleFunc(path.Join(basePath, "connections"), func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "There are currently %d active connections.", *numConnections) - }) + return router } -func serveStaticFiles(basePath, assetPath string) http.Handler { +// serveWs handles websocket requests from the peer. +func (server *server) serveWs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("OpenBooks") + if errors.Is(err, http.ErrNoCookie) { + cookie = &http.Cookie{ + Name: "OpenBooks", + Value: uuid.New().String(), + Secure: true, + HttpOnly: true, + Expires: time.Now().Add(time.Hour * 24 * 7), + SameSite: http.SameSiteStrictMode, + } + w.Header().Add("Set-Cookie", cookie.String()) + } + + userId := uuid.MustParse(cookie.Value) + + upgrader.CheckOrigin = func(req *http.Request) bool { + return true + } + + conn, err := upgrader.Upgrade(w, r, w.Header()) + if err != nil { + server.log.Println(err) + return + } + + // Each client gets its own connection, so use different usernames by appending count + name := fmt.Sprintf("%s-%d", server.config.UserName, len(server.clients)+1) + client := &Client{ + conn: conn, + send: make(chan interface{}, 128), + // disconnect: make(chan struct{}), + uuid: userId, + irc: irc.New(name, "OpenBooks - Search and download eBooks"), + log: log.New(os.Stdout, fmt.Sprintf("CLIENT (%s): ", name), log.LstdFlags|log.Lmsgprefix), + ctx: context.Background(), + } + + server.log.Printf("Client connected from %s\n", conn.RemoteAddr().String()) + client.log.Println("New client created.") + + server.register <- client + + // Allow collection of memory referenced by the caller by doing all work in + // new goroutines. + go server.writePump(client) + go server.readPump(client) + } +} + +func (server *server) staticFilesHandler(assetPath string) http.Handler { // update the embedded file system's tree so that index.html is at the root app, err := fs.Sub(reactClient, assetPath) if err != nil { - log.Fatal(err) + server.log.Fatal(err) } // strip the predefined base path and serve the static file - return http.StripPrefix(basePath, http.FileServer(http.FS(app))) + return http.StripPrefix(server.config.Basepath, http.FileServer(http.FS(app))) +} + +func (server *server) statsHandler() http.HandlerFunc { + type statsReponse struct { + UUID string `json:"uuid"` + IP string `json:"ip"` + Name string `json:"name"` + } + + return func(w http.ResponseWriter, r *http.Request) { + result := make([]statsReponse, 0, len(server.clients)) + + for _, client := range server.clients { + details := statsReponse{ + UUID: client.uuid.String(), + Name: client.irc.Username, + IP: client.conn.RemoteAddr().String(), + } + + result = append(result, details) + } + + json.NewEncoder(w).Encode(result) + } +} + +func (server *server) serverListHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(server.repository.servers) + } +} + +func (server *server) libraryHandler() http.HandlerFunc { + type download struct { + Name string `json:"name"` + DownloadLink string `json:"downloadLink"` + Time time.Time `json:"time"` + } + + return func(w http.ResponseWriter, r *http.Request) { + libraryDir := path.Join(server.config.DownloadDir, "books") + books, err := os.ReadDir(libraryDir) + if err != nil { + server.log.Printf("Unable to list books. %s\n", err) + } + + output := make([]download, 0) + for _, book := range books { + if !book.IsDir() { + info, err := book.Info() + if err != nil { + server.log.Println(err) + } + + dl := download{ + Name: book.Name(), + DownloadLink: path.Join("/static/library", book.Name()), + Time: info.ModTime(), + } + + output = append(output, dl) + } + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(output) + } +} + +func (server *server) libraryFileHandler(route string) http.Handler { + libraryPath := path.Join(server.config.DownloadDir, "books") + fs := http.FileServer(http.Dir(libraryPath)) + + // Inline middleware that deletes files after sending to user if "Persist" + // config option is disabled. + fileDeleter := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + + if !server.config.Persist { + _, fileName := path.Split(r.URL.Path) + bookPath := path.Join(server.config.DownloadDir, "books", fileName) + err := os.Remove(bookPath) + if err != nil { + server.log.Printf("Error when deleting book file. %s", err) + } + } + }) + } + return fileDeleter(http.StripPrefix(route, fs)) +} + +func (server *server) deleteLibraryFileHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + fileName, err := url.PathUnescape(chi.URLParam(r, "fileName")) + if err != nil { + server.log.Printf("Error unescaping path: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + } + + err = os.Remove(path.Join(server.config.DownloadDir, "books", fileName)) + if err != nil { + server.log.Printf("Error deleting book file: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + } + } } -func getBaseRoute() string { - cleaned := path.Clean(config.Basepath) - if cleaned == "/" { - return cleaned +func (server *server) testHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + client := server.getClient(r.Context()) + if client == nil { + w.Write([]byte("Client not found.")) + return + } + fmt.Fprintf(w, "Your user id is %s", client.irc.Username) } - return cleaned + "/" } diff --git a/server/server.go b/server/server.go index 39fef70..8065a73 100644 --- a/server/server.go +++ b/server/server.go @@ -1,7 +1,7 @@ package server import ( - "fmt" + "context" "log" "net/http" "os" @@ -9,8 +9,33 @@ import ( "path" "syscall" "time" + + "github.com/evan-buss/openbooks/util" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/google/uuid" + "github.com/rs/cors" ) +type server struct { + // Shared app configuration + config *Config + + // Shared data + repository *Repository + + // Registered clients. + clients map[uuid.UUID]*Client + + // Register requests from the clients. + register chan *Client + + // Unregister requests from clients. + unregister chan *Client + + log *log.Logger +} + // Config contains settings for server type Config struct { Log bool @@ -20,36 +45,96 @@ type Config struct { Persist bool DownloadDir string Basepath string + Server string } -var config Config -var numConnections *int32 = new(int32) +func New(config Config) *server { + return &server{ + repository: NewRepository(), + config: &config, + register: make(chan *Client), + unregister: make(chan *Client), + clients: make(map[uuid.UUID]*Client), + log: log.New(os.Stdout, "SERVER: ", log.LstdFlags|log.Lmsgprefix), + } +} // Start instantiates the web server and opens the browser -func Start(conf Config) { - config = conf +func Start(config Config) { + createBooksDirectory(config) + router := chi.NewRouter() + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(middleware.Recoverer) - hub := newHub() - go hub.run() + corsConfig := cors.Options{ + AllowCredentials: true, + AllowedOrigins: []string{"http://localhost:3000"}, + AllowedHeaders: []string{"*"}, + AllowedMethods: []string{"GET", "DELETE"}, + } + router.Use(cors.New(corsConfig).Handler) + server := New(config) + routes := server.registerRoutes() + + ctx, cancel := context.WithCancel(context.Background()) + go server.startClientHub(ctx) + server.registerGracefulShutdown(cancel) + router.Mount(config.Basepath, routes) + + server.log.Printf("Base Path: %v\n", config.Basepath) + if config.OpenBrowser { + browserUrl := "http://127.0.0.1:" + path.Join(config.Port+config.Basepath) + util.OpenBrowser(browserUrl) + } + + server.log.Printf("OpenBooks is listening on port %v", config.Port) + server.log.Fatal(http.ListenAndServe(":"+config.Port, router)) +} + +// The client hub is to be run in a goroutine and handles management of +// websocket client registrations. +func (s *server) startClientHub(ctx context.Context) { + for { + select { + case client := <-s.register: + s.clients[client.uuid] = client + case client := <-s.unregister: + if _, ok := s.clients[client.uuid]; ok { + _, cancel := context.WithCancel(client.ctx) + close(client.send) + cancel() + delete(s.clients, client.uuid) + } + case <-ctx.Done(): + for _, client := range s.clients { + _, cancel := context.WithCancel(client.ctx) + close(client.send) + cancel() + delete(s.clients, client.uuid) + } + return + } + } +} + +func (server *server) registerGracefulShutdown(cancel context.CancelFunc) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c + server.log.Println("Graceful shutdown.") // Close the shutdown channel. Triggering all reader/writer WS handlers to close. - close(hub.shutdown) + cancel() time.Sleep(time.Second) - os.Exit(1) + os.Exit(0) }() +} - registerRoutes(hub) - - if config.OpenBrowser { - browserUrl := "http://127.0.0.1:" + path.Join(config.Port+config.Basepath) - fmt.Println(browserUrl) - openbrowser(browserUrl) +func createBooksDirectory(config Config) { + err := os.MkdirAll(path.Join(config.DownloadDir, "books"), os.FileMode(0755)) + if err != nil { + panic(err) } - - log.Printf("OpenBooks is listening on port %v", config.Port) - log.Fatal(http.ListenAndServe(":"+config.Port, nil)) } diff --git a/server/request_router.go b/server/websocket_requests.go similarity index 56% rename from server/request_router.go rename to server/websocket_requests.go index 845bffc..768dbd0 100644 --- a/server/request_router.go +++ b/server/websocket_requests.go @@ -2,9 +2,9 @@ package server import ( "encoding/json" - "log" "github.com/evan-buss/openbooks/core" + "github.com/evan-buss/openbooks/util" ) // RequestHandler defines a generic handle() method that is called when a specific request type is made @@ -13,48 +13,62 @@ type RequestHandler interface { } // messageRouter is used to parse the incoming request and respond appropriately -func (c *Client) routeMessage(message Request) { - var obj RequestHandler +func (s *server) routeMessage(message Request, c *Client) { + var obj interface{} switch message.RequestType { - case CONNECT: - obj = new(ConnectionRequest) case SEARCH: obj = new(SearchRequest) case DOWNLOAD: obj = new(DownloadRequest) - case SERVERS: - obj = new(ServersRequest) - default: - log.Printf("%s: Unknown request type received.\n", c.uuid.String()) } err := json.Unmarshal(message.Payload, &obj) if err != nil { - log.Printf("%s: Invalid request payload.\n", c.uuid.String()) + s.log.Println("Invalid request payload.") c.send <- ErrorResponse{ Error: message.RequestType, Details: err.Error(), } } - obj.handle(c) + + switch message.RequestType { + case CONNECT: + c.startIrcConnection(s) + case SEARCH: + c.sendSearchRequest(obj.(*SearchRequest)) + case DOWNLOAD: + c.sendDownloadRequest(obj.(*DownloadRequest)) + default: + s.log.Println("Unknown request type received.") + } } // handle ConnectionRequests and either connect to the server or do nothing -func (ConnectionRequest) handle(c *Client) { - core.Join(c.irc) - // Start the Read Daemon - go core.ReadDaemon(c.irc, config.Log, Handler{Client: c}, c.disconnect) +func (c *Client) startIrcConnection(server *server) { + core.Join(c.irc, server.config.Server) + handler := server.NewIrcEventHandler(c) + + if server.config.Log { + logger, _, err := util.CreateLogFile(c.irc.Username, server.config.DownloadDir) + if err != nil { + server.log.Println(err) + } + handler[core.Message] = func(text string) { logger.Println(text) } + } + + go core.StartReader(c.ctx, c.irc, handler) c.send <- ConnectionResponse{ MessageType: CONNECT, Status: "IRC Server Ready", + Name: c.irc.Username, Wait: 0, } } // handle SearchRequests and send the query to the book server -func (s SearchRequest) handle(c *Client) { +func (c *Client) sendSearchRequest(s *SearchRequest) { core.SearchBook(c.irc, s.Query) c.send <- WaitResponse{ @@ -64,7 +78,7 @@ func (s SearchRequest) handle(c *Client) { } // handle DownloadRequests by sending the request to the book server -func (d DownloadRequest) handle(c *Client) { +func (c *Client) sendDownloadRequest(d *DownloadRequest) { core.DownloadBook(c.irc, d.Book) c.send <- WaitResponse{ @@ -72,15 +86,3 @@ func (d DownloadRequest) handle(c *Client) { Status: "Download request received", } } - -// handle ServerRequests by sending the currently available book servers -func (s ServersRequest) handle(c *Client) { - servers := make(chan []string, 1) - go core.GetServers(servers) - results := <-servers - - c.send <- ServersResponse{ - MessageType: SERVERS, - Servers: results, - } -} diff --git a/todo b/todo index b3bd7f2..51345b7 100644 --- a/todo +++ b/todo @@ -1,12 +1,9 @@ -- Migrate as much communication from WebSocket to HTTP api as possible. - - We are overusing websocket communication and it just adds complexity - - Send files via HTTP instead of base64 encoding in the websocket JSON message -- Use evergreen virtual scrolling grid that fits page size -- Add filtering to grid - - See: https://evergreen-storybook.netlify.app/?path=/story/table--advanced-sortable-table - - Server / Title - Search Text Box - - Format - Find unique formats and have clickable options -- Use new Pulsar component instead of custom version -- Make the "connected" indicator actually work... -- Show IRC name in web interface -- Show raw IRC logs in web interface \ No newline at end of file +Future: + + - Allow reconnecting to same IRC connection if websocket disconnects. + - Currently we immediately terminate the IRC connection and remove the client + - Possible use a timer that keeps the Client around for a set period of time + after websocket disconnect. + - Convert grid to use React Grid instead of Evergreen's built in one. + - Things like styling are more difficult with Evergreen (ex. can't round the bottom corners) + - Send download progress updates to the browser like we show for the CLI \ No newline at end of file diff --git a/util/archiver.go b/util/archiver.go new file mode 100644 index 0000000..5822405 --- /dev/null +++ b/util/archiver.go @@ -0,0 +1,64 @@ +package util + +import ( + "errors" + "io" + "log" + "os" + "path/filepath" + + "github.com/mholt/archiver/v3" +) + +var ( + ErrNotFullyCopied = errors.New("didn't copy entire file from the archive") +) + +func ExtractArchive(archivePath string) (string, error) { + var newPath string + err := archiver.Walk(archivePath, func(f archiver.File) error { + newPath = filepath.Join(filepath.Dir(archivePath), f.Name()) + + out, err := os.Create(newPath) + if err != nil { + return err + } + + copied, err := io.Copy(out, f) + if err != nil { + return err + } + if copied != f.Size() { + return ErrNotFullyCopied + } + + err = out.Close() + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return "", err + } + + // If we extracted a file, send that file and remove the zip file + if newPath != "" { + err := os.Remove(archivePath) + if err != nil { + log.Println("remove error", err) + } + return newPath, nil + } else { + return archivePath, nil + } +} + +// IsArchive returns true if the file at the given path is an archive that can +// be extracted. Returns false otherwise. +func IsArchive(path string) bool { + _, err := archiver.ByExtension(path) + return err == nil +} diff --git a/server/util.go b/util/browser.go similarity index 89% rename from server/util.go rename to util/browser.go index 3fa9471..6f3ece8 100644 --- a/server/util.go +++ b/util/browser.go @@ -1,4 +1,4 @@ -package server +package util import ( "log" @@ -6,7 +6,7 @@ import ( "runtime" ) -func openbrowser(url string) { +func OpenBrowser(url string) { var err error switch runtime.GOOS { diff --git a/util/logger.go b/util/logger.go new file mode 100644 index 0000000..2ecd40d --- /dev/null +++ b/util/logger.go @@ -0,0 +1,28 @@ +package util + +import ( + "fmt" + "io" + "log" + "os" + "path" + "time" +) + +func CreateLogFile(username, dir string) (*log.Logger, io.Closer, error) { + date := time.Now().Format("2006-01-02--15-04-05") + fileName := fmt.Sprintf("%s--%s.log", username, date) + + err := os.MkdirAll(path.Join(dir, "logs"), os.FileMode(0755)) + if err != nil { + return nil, nil, err + } + + path := path.Join(dir, "logs", fileName) + logFile, err := os.Create(path) + if err != nil { + return nil, nil, err + } + + return log.New(logFile, "", 0), logFile, nil +}