├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build-and-push.yml │ └── nextjs-bundle-analysis.yml ├── .gitmodules ├── .local ├── dev-certificate.pfx ├── secrets │ ├── .gitignore │ └── README.md ├── traefik │ ├── certs │ │ └── .gitignore │ └── config.yaml └── volumes │ ├── .gitignore │ └── factorio │ └── README.md ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── backend ├── .config │ └── dotnet-tools.json ├── .dockerignore ├── .gitignore ├── Dockerfile ├── FactorioTech.sln ├── FactorioTech.sln.DotSettings ├── README.md ├── src │ ├── FactorioTech.Api │ │ ├── Controllers │ │ │ ├── AssetController.cs │ │ │ ├── BuildController.cs │ │ │ ├── PayloadController.cs │ │ │ ├── RpcController.cs │ │ │ └── UserController.cs │ │ ├── Extensions │ │ │ ├── Json │ │ │ │ ├── CustomJsonStringEnumConverter.cs │ │ │ │ ├── HashJsonConverter.cs │ │ │ │ ├── PolymorphicJsonConverter.cs │ │ │ │ └── VersionJsonConverter.cs │ │ │ ├── OAuthResponsesOperationFilter.cs │ │ │ └── ValidateTagAttribute.cs │ │ ├── FactorioTech.Api.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Services │ │ │ ├── CoverProvider.cs │ │ │ ├── DevDataSeeder.cs │ │ │ ├── LinkBuilder.cs │ │ │ ├── NullImageCache.cs │ │ │ ├── RenderingProvider.cs │ │ │ └── ViewModelMapper.cs │ │ ├── Startup.cs │ │ ├── ViewModels │ │ │ ├── BuildModel.cs │ │ │ ├── BuildsModel.cs │ │ │ ├── BuildsQueryParams.cs │ │ │ ├── PayloadModel.cs │ │ │ ├── Requests │ │ │ │ ├── CoverRequest.cs │ │ │ │ ├── CreateRequest.cs │ │ │ │ ├── EditBuildRequest.cs │ │ │ │ └── VersionRequest.cs │ │ │ ├── UserModel.cs │ │ │ ├── UsersModel.cs │ │ │ ├── VersionModel.cs │ │ │ ├── VersionsModel.cs │ │ │ └── ViewModelBase.cs │ │ └── appsettings.json │ ├── FactorioTech.Core │ │ ├── AppConfig.cs │ │ ├── BlocklistAttribute.cs │ │ ├── BuildInformation.cs │ │ ├── Data │ │ │ ├── AppDbContext.cs │ │ │ ├── AppDbContextFactory.cs │ │ │ ├── Migrations │ │ │ │ ├── 20220824154950_Init.Designer.cs │ │ │ │ ├── 20220824154950_Init.cs │ │ │ │ └── AppDbContextModelSnapshot.cs │ │ │ └── PersistedGrantIdentityContext.cs │ │ ├── Domain │ │ │ ├── Build.cs │ │ │ ├── BuildTags.cs │ │ │ ├── BuildVersion.cs │ │ │ ├── Favorite.cs │ │ │ ├── GameIcon.cs │ │ │ ├── Hash.cs │ │ │ ├── IconType.cs │ │ │ ├── ImageMeta.cs │ │ │ ├── Payload.cs │ │ │ ├── PayloadType.cs │ │ │ ├── Role.cs │ │ │ └── User.cs │ │ ├── FactorioApi.cs │ │ ├── FactorioTech.Core.csproj │ │ ├── IdentityExtensions.cs │ │ ├── PayloadCache.cs │ │ ├── Services │ │ │ ├── AssetService.cs │ │ │ ├── BlueprintConverter.cs │ │ │ ├── BuildService.cs │ │ │ ├── FbsrClient.cs │ │ │ ├── FollowerService.cs │ │ │ ├── ImageService.cs │ │ │ ├── SlugService.cs │ │ │ └── TempCoverHandle.cs │ │ ├── SnakeCaseNamingPolicy.cs │ │ ├── TelemetryInitializers.cs │ │ └── Utils.cs │ └── FactorioTech.Identity │ │ ├── Configuration │ │ ├── IdentityConfig.cs │ │ ├── OAuthClientConfig.cs │ │ └── OAuthProviderConfig.cs │ │ ├── DevDataSeeder.cs │ │ ├── Extensions │ │ ├── CustomProfileService.cs │ │ ├── CustomUserNamePolicy.cs │ │ └── SecurityHeadersAttribute.cs │ │ ├── FactorioTech.Identity.csproj │ │ ├── Pages │ │ ├── ConfirmEmail.cshtml │ │ ├── ConfirmEmail.cshtml.cs │ │ ├── ConfirmEmailChange.cshtml │ │ ├── ConfirmEmailChange.cshtml.cs │ │ ├── Errors │ │ │ ├── 403.cshtml │ │ │ ├── 403.cshtml.cs │ │ │ ├── 404.cshtml │ │ │ ├── 404.cshtml.cs │ │ │ ├── 500.cshtml │ │ │ └── 500.cshtml.cs │ │ ├── ExternalLogin.cshtml │ │ ├── ExternalLogin.cshtml.cs │ │ ├── Login.cshtml │ │ ├── Login.cshtml.cs │ │ ├── Logout.cshtml │ │ ├── Logout.cshtml.cs │ │ ├── Manage │ │ │ ├── Email.cshtml │ │ │ ├── Email.cshtml.cs │ │ │ ├── ExternalLogins.cshtml │ │ │ ├── ExternalLogins.cshtml.cs │ │ │ ├── Index.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ ├── ManageNavPages.cs │ │ │ ├── _Layout.cshtml │ │ │ └── _ManageNav.cshtml │ │ ├── Shared │ │ │ ├── _Layout.cshtml │ │ │ ├── _StatusMessage.cshtml │ │ │ └── _ValidationScripts.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ ├── appsettings.json │ │ └── wwwroot │ │ ├── custom.css │ │ └── favicon.ico ├── tags.json └── test │ └── FactorioTech.Tests │ ├── BlueprintConverterTests.cs │ ├── BuildsServiceTests.cs │ ├── FactorioTech.Tests.csproj │ └── Helpers │ ├── BuildBuilder.cs │ ├── PayloadBuilder.cs │ ├── TestData.cs │ ├── TestUtils.cs │ └── UserBuilder.cs ├── deploy ├── .gitignore ├── .helmignore ├── Chart.yaml ├── templates │ ├── _helpers.tpl │ ├── api │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── protected-pvc.yaml │ │ └── service.yaml │ ├── fbsr-wrapper │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ └── service.yaml │ ├── identity │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── protected-pvc.yaml │ │ ├── secret.yaml │ │ └── service.yaml │ ├── postgres │ │ ├── deployment.yaml │ │ ├── secret.yaml │ │ └── service.yaml │ └── web │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── secret.yaml │ │ └── service.yaml └── values.yaml ├── docker-compose.yaml ├── fbsr-wrapper ├── .dockerignore ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── build-local.sh ├── config.docker.json ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── tech │ │ │ └── factorio │ │ │ └── fbsrwrapper │ │ │ ├── BlueprintController.java │ │ │ └── FbsrWrapperApplication.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── tech │ └── factorio │ └── fbsrwrapper │ └── FbsrWrapperApplicationTests.java ├── frontend ├── .babelrc ├── .dockerignore ├── .env.local.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── Dockerfile ├── README.md ├── components │ ├── form │ │ ├── Checkbox │ │ │ ├── checkbox.component.tsx │ │ │ ├── checkbox.styles.tsx │ │ │ └── index.ts │ │ ├── ErrorMessage │ │ │ ├── error-message.component.tsx │ │ │ ├── error-message.styles.tsx │ │ │ └── index.ts │ │ ├── FormError │ │ │ ├── form-error.component.tsx │ │ │ ├── form-error.styles.tsx │ │ │ └── index.ts │ │ ├── FormikCheckbox │ │ │ ├── formik-checkbox.component.tsx │ │ │ └── index.ts │ │ ├── FormikInput │ │ │ ├── formik-input.component.tsx │ │ │ └── index.ts │ │ ├── FormikInputWrapper │ │ │ ├── formik-input-wrapper.component.tsx │ │ │ └── index.ts │ │ ├── FormikSelect │ │ │ ├── formik-select.component.tsx │ │ │ └── index.ts │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── input.component.tsx │ │ │ └── input.styles.tsx │ │ ├── InputGroup │ │ │ ├── index.ts │ │ │ ├── input-group.component.tsx │ │ │ └── input-group.styles.tsx │ │ ├── InputWrapper │ │ │ ├── index.ts │ │ │ ├── input-wrapper.component.tsx │ │ │ └── input-wrapper.styles.tsx │ │ ├── MarkdownEditor │ │ │ ├── index.ts │ │ │ ├── markdown-editor.component.tsx │ │ │ └── markdown-editor.styles.tsx │ │ └── Radio │ │ │ ├── index.ts │ │ │ ├── radio.component.tsx │ │ │ └── radio.styles.tsx │ ├── pages │ │ ├── BuildFormPage │ │ │ ├── build-form-page.component.tsx │ │ │ ├── build-form-page.d.tsx │ │ │ ├── build-form-page.helpers.ts │ │ │ ├── build-form-page.styles.tsx │ │ │ ├── index.ts │ │ │ ├── pager.component.tsx │ │ │ ├── step-1.component.tsx │ │ │ ├── step-2-cover.component.tsx │ │ │ ├── step-2-data.component.tsx │ │ │ ├── step-2.component.tsx │ │ │ ├── useCanSave.ts │ │ │ └── validation.ts │ │ ├── BuildListPage │ │ │ ├── build-list-page.component.tsx │ │ │ └── index.ts │ │ ├── BuildPage │ │ │ ├── build-page.component.tsx │ │ │ ├── build-page.styles.tsx │ │ │ ├── glow.component.tsx │ │ │ ├── index.ts │ │ │ ├── tabs.component.tsx │ │ │ ├── tabs │ │ │ │ ├── blueprint-json-tab.component.tsx │ │ │ │ ├── blueprint-string-tab.component.tsx │ │ │ │ ├── blueprints-tab.component.tsx │ │ │ │ ├── details-tab.component.tsx │ │ │ │ ├── image-mobile-tab.component.tsx │ │ │ │ ├── required-items-tab.component.tsx │ │ │ │ └── tab.component.tsx │ │ │ └── usePayload.ts │ │ └── UserBuildListPage │ │ │ ├── index.ts │ │ │ └── user-build-list-page.component.tsx │ └── ui │ │ ├── Avatar │ │ ├── avatar.component.tsx │ │ ├── avatar.styles.tsx │ │ └── index.ts │ │ ├── BlueprintItem │ │ ├── blueprint-item.component.tsx │ │ ├── blueprint-item.styles.tsx │ │ └── index.ts │ │ ├── BlueprintItemExplorer │ │ ├── blueprint-item-explorer.component.tsx │ │ ├── blueprint-item-explorer.provider.tsx │ │ └── index.ts │ │ ├── BlueprintRequiredItems │ │ ├── blueprint-required-items.component.tsx │ │ ├── blueprint-required-items.styles.tsx │ │ └── index.ts │ │ ├── BuildCard │ │ ├── build-card.component.tsx │ │ ├── build-card.styles.tsx │ │ └── index.ts │ │ ├── BuildCardList │ │ ├── build-card-list.component.tsx │ │ ├── build-card-list.styles.tsx │ │ └── index.ts │ │ ├── BuildHeader │ │ ├── build-header.component.tsx │ │ ├── build-header.styles.tsx │ │ └── index.ts │ │ ├── BuildIcon │ │ ├── build-icon.component.tsx │ │ ├── build-icon.styles.tsx │ │ └── index.ts │ │ ├── BuildImage │ │ ├── build-image.component.tsx │ │ ├── build-image.styles.tsx │ │ └── index.ts │ │ ├── BuildList │ │ ├── build-list.component.tsx │ │ ├── build-list.styles.tsx │ │ └── index.ts │ │ ├── BuildListLookupStats │ │ ├── build-list-lookup-stats.component.tsx │ │ ├── build-list-lookup-stats.styles.tsx │ │ └── index.ts │ │ ├── BuildListSort │ │ ├── build-list-sort.component.tsx │ │ ├── build-list-sort.styles.tsx │ │ └── index.ts │ │ ├── Button │ │ ├── button.component.tsx │ │ ├── button.styles.tsx │ │ └── index.ts │ │ ├── ButtonClipboard │ │ └── button-clipboard.component.tsx │ │ ├── Container │ │ ├── container.component.tsx │ │ ├── container.styles.tsx │ │ └── index.ts │ │ ├── Dropdown │ │ ├── dropdown.component.tsx │ │ ├── dropdown.styles.tsx │ │ └── index.ts │ │ ├── FavoriteButton │ │ ├── favorite-button.component.tsx │ │ ├── favorite-button.styles.tsx │ │ └── index.ts │ │ ├── Filter │ │ ├── filter.component.tsx │ │ └── index.ts │ │ ├── FilterList │ │ ├── filter-group.component.tsx │ │ ├── filter-list.component.tsx │ │ ├── filter-list.styles.tsx │ │ └── index.ts │ │ ├── Header │ │ ├── header.component.tsx │ │ ├── header.styles.tsx │ │ └── index.ts │ │ ├── ImageUpload │ │ ├── image-upload.component.tsx │ │ ├── image-upload.styles.tsx │ │ └── index.ts │ │ ├── ItemIcon │ │ ├── index.ts │ │ ├── item-icon.component.tsx │ │ └── item-icon.styles.tsx │ │ ├── Layout │ │ ├── index.ts │ │ └── layout.component.tsx │ │ ├── LayoutDefault │ │ ├── index.ts │ │ ├── layout-default.component.tsx │ │ └── layout-default.styles.tsx │ │ ├── LayoutSidebar │ │ ├── index.ts │ │ ├── layout-sidebar.component.tsx │ │ └── layout-sidebar.styles.tsx │ │ ├── Links │ │ ├── index.ts │ │ ├── links.component.tsx │ │ └── links.styles.tsx │ │ ├── RichText │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── rich-text.test.tsx.snap │ │ │ ├── rich-text.test.tsx │ │ │ └── useParseRichText.test.ts │ │ ├── index.ts │ │ ├── rich-text.component.tsx │ │ ├── rich-text.styles.tsx │ │ └── useParseRichText.hook.ts │ │ ├── Search │ │ ├── index.ts │ │ ├── search.component.tsx │ │ └── search.styles.tsx │ │ ├── Sidebar │ │ ├── index.ts │ │ ├── sidebar.component.tsx │ │ └── sidebar.styles.tsx │ │ ├── Spinner │ │ ├── index.ts │ │ ├── spinner.component.tsx │ │ └── spinner.styles.tsx │ │ ├── Stacker │ │ ├── index.ts │ │ ├── stacker.component.tsx │ │ └── stacker.styles.tsx │ │ ├── Subheader │ │ ├── index.ts │ │ ├── subheader.component.tsx │ │ └── subheader.styles.tsx │ │ ├── Tooltip │ │ ├── index.ts │ │ ├── tooltip.component.tsx │ │ └── tooltip.styles.tsx │ │ └── UserDropdown │ │ ├── index.ts │ │ ├── user-dropdown.component.tsx │ │ └── user-dropdown.styles.tsx ├── design │ ├── helpers │ │ └── typo.ts │ ├── stitches.config.ts │ ├── styles │ │ └── media.ts │ └── tokens │ │ ├── color.ts │ │ ├── layout.ts │ │ └── typo.ts ├── hooks │ ├── __tests__ │ │ └── useDistributeToColumn.test.tsx │ ├── useApi.ts │ ├── useDebouncedEffect.tsx │ ├── useDistributeToColumn.tsx │ └── useImage.ts ├── icons │ ├── burger.tsx │ ├── caret.tsx │ ├── copy.tsx │ ├── editor.tsx │ ├── github.tsx │ ├── lamp.tsx │ ├── line.tsx │ ├── logo.tsx │ ├── plus.tsx │ ├── raw.tsx │ ├── search-icon.tsx │ └── thumbs-up.tsx ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── [user] │ │ ├── [slug].tsx │ │ ├── [slug] │ │ │ └── edit.tsx │ │ └── builds.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── api │ │ └── auth │ │ │ └── [...auth0].ts │ ├── build │ │ └── create.tsx │ ├── index.tsx │ └── user │ │ ├── [id].tsx │ │ └── index.tsx ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── img │ │ ├── blueprint-book.png │ │ └── image-backdrop.jpg │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── redux │ ├── reducer.ts │ ├── reducers │ │ ├── auth.ts │ │ ├── filters.ts │ │ ├── layout.ts │ │ └── search.ts │ ├── selectors │ │ └── filters.ts │ └── store.ts ├── scripts │ └── bundle-size.js ├── tags.json ├── tsconfig.json ├── types │ ├── generated-api.ts │ ├── index.ts │ └── models.ts ├── utils │ ├── __tests__ │ │ ├── blueprint-heuristics.test.ts │ │ ├── blueprint.test.ts │ │ └── testdata │ │ │ └── blueprintMocks.ts │ ├── auth.ts │ ├── axios.ts │ ├── blueprint-heuristics.ts │ ├── blueprint.ts │ ├── build.ts │ ├── date.ts │ └── typescript.ts └── yarn.lock ├── infrastructure ├── .gitignore ├── .terraform.lock.hcl ├── main.tf ├── outputs.tf ├── traefik.values.yaml └── variables.tf └── scripts ├── generate-api-types.sh ├── generate-certificate.sh ├── kubectl-tunnel-postgres.sh ├── kubectl-tunnel-rabbitmq.sh └── kubectl-tunnel-traefik.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/*.*proj.user 3 | **/azds.yaml 4 | **/charts 5 | **/bin 6 | **/obj 7 | **/Dockerfile 8 | **/Dockerfile.develop 9 | **/docker-compose.yml 10 | **/docker-compose.*.yml 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/secrets.dev.yaml 14 | **/values.dev.yaml 15 | **/.toolstarget 16 | **/README.md 17 | **/node_modules 18 | **/appsettings*.json 19 | **.DotSettings* 20 | deploy/ 21 | infrastructure/ 22 | scripts/ 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 4 | # in Windows via a file share from Linux, the scripts will work. 5 | *.{cmd,[cC][mM][dD]} text eol=crlf 6 | *.{bat,[bB][aA][tT]} text eol=crlf 7 | 8 | # Force bash scripts to always use LF line endings so that if a repo is accessed 9 | # in Unix via a file share from Windows, the scripts will work. 10 | *.sh text eol=lf 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gitsubmodule" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | 12 | # infrastructure 13 | - package-ecosystem: "terraform" 14 | directory: "/infrastructure/" 15 | schedule: 16 | interval: "daily" 17 | 18 | # frontend 19 | - package-ecosystem: "docker" 20 | directory: "/frontend/" 21 | schedule: 22 | interval: "daily" 23 | # - package-ecosystem: "npm" 24 | # directory: "/frontend/" 25 | # schedule: 26 | # interval: "weekly" 27 | 28 | # backend 29 | - package-ecosystem: "docker" 30 | directory: "/backend/" 31 | schedule: 32 | interval: "daily" 33 | - package-ecosystem: "nuget" 34 | directory: "/backend/" 35 | schedule: 36 | interval: "daily" 37 | 38 | # fbsr-wrapper 39 | - package-ecosystem: "docker" 40 | directory: "/fbsr-wrapper/" 41 | schedule: 42 | interval: "daily" 43 | - package-ecosystem: "maven" 44 | directory: "/fbsr-wrapper/" 45 | schedule: 46 | interval: "weekly" 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fbsr-wrapper/deps/Factorio-FBSR"] 2 | path = fbsr-wrapper/deps/Factorio-FBSR 3 | url = git@github.com:demodude4u/Factorio-FBSR.git 4 | [submodule "fbsr-wrapper/deps/Java-Factorio-Data-Wrapper"] 5 | path = fbsr-wrapper/deps/Java-Factorio-Data-Wrapper 6 | url = git@github.com:demodude4u/Java-Factorio-Data-Wrapper.git 7 | [submodule "fbsr-wrapper/deps/Discord-Core-Bot-Apple"] 8 | path = fbsr-wrapper/deps/Discord-Core-Bot-Apple 9 | url = git@github.com:demodude4u/Discord-Core-Bot-Apple.git 10 | -------------------------------------------------------------------------------- /.local/dev-certificate.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorio-builds/factorio-builds-tech/5a221039f226a91037330dc0d9aca113386ab3d3/.local/dev-certificate.pfx -------------------------------------------------------------------------------- /.local/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !README.md 4 | -------------------------------------------------------------------------------- /.local/secrets/README.md: -------------------------------------------------------------------------------- 1 | # Secrets 2 | 3 | The secrets in this folder are loaded into the docker containers under `/mnt/secrets` when started with `docker-compose`. 4 | 5 | ## Available Secrets 6 | 7 | - `OAuthProviders__Discord__ClientId` 8 | - `OAuthProviders__Discord__ClientSecret` 9 | - `OAuthProviders__GitHub__ClientId` 10 | - `OAuthProviders__GitHub__ClientSecret` 11 | -------------------------------------------------------------------------------- /.local/traefik/certs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.local/traefik/config.yaml: -------------------------------------------------------------------------------- 1 | http: 2 | routers: 3 | traefik: 4 | rule: "Host(`traefik.local.factorio.tech`)" 5 | service: "api@internal" 6 | tls: 7 | domains: 8 | - main: "local.factorio.tech" 9 | sans: 10 | - "*.local.factorio.tech" 11 | 12 | tls: 13 | certificates: 14 | - certFile: "/etc/certs/local-cert.pem" 15 | keyFile: "/etc/certs/local-key.pem" 16 | -------------------------------------------------------------------------------- /.local/volumes/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !factorio/ 4 | !factorio/README.md 5 | -------------------------------------------------------------------------------- /.local/volumes/factorio/README.md: -------------------------------------------------------------------------------- 1 | # Factorio game data 2 | 3 | Factorio game data is expected to be present in this folder. You can download the game either using Steam or from the official website: https://www.factorio.com/download 4 | **Note**: To download the required files you must **own** the game and have and login on the website. The headless version does **not** work, as it doesn't include all necessary assets! 5 | 6 | After copying the assets, the structure in this directory should be such that this is a valid path: 7 | 8 | `.local/volumes/factorio/data/base/graphics/icons/beacon.png` 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "factorio", 4 | "formik", 5 | "mkcert", 6 | "nextjs", 7 | "openid", 8 | "postgres", 9 | "submodule", 10 | "submodules", 11 | "traefik", 12 | "untrusted" 13 | ] 14 | } -------------------------------------------------------------------------------- /backend/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "6.0.8", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | }, 11 | "swashbuckle.aspnetcore.cli": { 12 | "version": "6.4.0", 13 | "commands": [ 14 | "swagger" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/*.*proj.user 3 | **/azds.yaml 4 | **/charts 5 | **/bin 6 | **/obj 7 | **/Dockerfile 8 | **/Dockerfile.develop 9 | **/docker-compose.yml 10 | **/docker-compose.*.yml 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/secrets.dev.yaml 14 | **/values.dev.yaml 15 | **/.toolstarget 16 | **/README.md 17 | **/node_modules 18 | **/appsettings*.json 19 | **.DotSettings* 20 | !.config 21 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Extensions/Json/HashJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace FactorioTech.Api.Extensions.Json; 6 | 7 | public class HashJsonConverter : JsonConverter 8 | { 9 | public override Hash Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 10 | reader.TokenType == JsonTokenType.String && Hash.TryParse(reader.GetString(), out var hash) 11 | ? hash 12 | : throw new JsonException("The input is not a valid version"); 13 | 14 | public override void Write(Utf8JsonWriter writer, Hash value, JsonSerializerOptions options) => 15 | writer.WriteStringValue(value.ToString()); 16 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Extensions/Json/VersionJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace FactorioTech.Api.Extensions.Json; 5 | 6 | public class VersionJsonConverter : JsonConverter 7 | { 8 | public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 9 | reader.TokenType == JsonTokenType.String && Version.TryParse(reader.GetString(), out var version) 10 | ? version 11 | : throw new JsonException("The input is not a valid version"); 12 | 13 | public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) => 14 | writer.WriteStringValue(value.ToString(4)); 15 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Extensions/OAuthResponsesOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace FactorioTech.Api.Extensions; 6 | 7 | public class OAuthResponsesOperationFilter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 10 | { 11 | var authAttributes = context.MethodInfo?.DeclaringType?.GetCustomAttributes(true) 12 | .Union(context.MethodInfo.GetCustomAttributes(true)) 13 | .OfType(); 14 | 15 | if (authAttributes?.Any() == true) 16 | { 17 | operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); 18 | operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); 19 | 20 | operation.Security = new List 21 | { 22 | new() 23 | { 24 | [ 25 | new OpenApiSecurityScheme 26 | { 27 | Reference = new OpenApiReference 28 | { 29 | Type = ReferenceType.SecurityScheme, 30 | Id = SecuritySchemeType.OAuth2.ToString(), 31 | }, 32 | } 33 | ] = new[] { "openid profile" }, 34 | }, 35 | }; 36 | 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Extensions/ValidateTagAttribute.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FactorioTech.Api.Extensions; 5 | 6 | public class ValidateTagAttribute : ValidationAttribute 7 | { 8 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 9 | { 10 | if (value is IEnumerable tags) 11 | { 12 | var buildTags = validationContext.GetRequiredService(); 13 | var invalidTags = tags.Except(buildTags).ToArray(); 14 | if (invalidTags.Any()) 15 | { 16 | return new ValidationResult($"The following tags are invalid: {string.Join(',', invalidTags)}"); 17 | } 18 | } 19 | 20 | return ValidationResult.Success; 21 | } 22 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "FactorioTech.Api": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": "true", 7 | "launchBrowser": false, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:5101;http://localhost:5100", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Services/CoverProvider.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core; 2 | using FactorioTech.Core.Services; 3 | using SixLabors.ImageSharp.Web.Providers; 4 | using SixLabors.ImageSharp.Web.Resolvers; 5 | using SixLabors.ImageSharp.Web.Resolvers.Azure; 6 | 7 | namespace FactorioTech.Api.Services; 8 | 9 | public class CoverProvider : IImageProvider 10 | { 11 | public ProcessingBehavior ProcessingBehavior => ProcessingBehavior.All; 12 | 13 | public Func Match { get; set; } = context => 14 | context.Request.Path.StartsWithSegments("/images/covers"); 15 | 16 | public bool IsValidRequest(HttpContext context) => true; // todo - strongly type and check image id 17 | 18 | public async Task GetAsync(HttpContext context) 19 | { 20 | var imageService = context.RequestServices.GetRequiredService(); 21 | var imageId = context.Request.Path.Value!.Split('/').Last(); 22 | var blob = await imageService.GetCover(imageId); 23 | return blob != null ? new AzureBlobStorageImageResolver(blob) : null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Services/NullImageCache.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp.Web; 2 | using SixLabors.ImageSharp.Web.Caching; 3 | using SixLabors.ImageSharp.Web.Resolvers; 4 | 5 | namespace FactorioTech.Api.Services; 6 | 7 | public sealed class NullImageCache : IImageCache 8 | { 9 | public Task GetAsync(string key) => Task.FromResult(null); 10 | 11 | public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) => Task.CompletedTask; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/Services/RenderingProvider.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core; 2 | using FactorioTech.Core.Domain; 3 | using FactorioTech.Core.Services; 4 | using SixLabors.ImageSharp.Web.Providers; 5 | using SixLabors.ImageSharp.Web.Resolvers; 6 | using SixLabors.ImageSharp.Web.Resolvers.Azure; 7 | 8 | namespace FactorioTech.Api.Services; 9 | 10 | public class RenderingProvider : IImageProvider 11 | { 12 | public ProcessingBehavior ProcessingBehavior => ProcessingBehavior.All; 13 | 14 | public Func Match { get; set; } = context => 15 | context.Request.Path.StartsWithSegments("/images/renderings"); 16 | 17 | public bool IsValidRequest(HttpContext context) => 18 | Hash.TryParse(context.Request.Path.Value!.Split('/').Last(), out _); 19 | 20 | public async Task GetAsync(HttpContext context) 21 | { 22 | var imageService = context.RequestServices.GetRequiredService(); 23 | var hash = context.Request.Path.Value!.Split('/').Last().Let(Hash.Parse); 24 | var blob = await imageService.GetOrCreateRendering(hash); 25 | return blob != null ? new AzureBlobStorageImageResolver(blob) : null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/Requests/CoverRequest.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using FactorioTech.Core.Services; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace FactorioTech.Api.ViewModels.Requests; 6 | 7 | public class CoverRequest : IValidatableObject 8 | { 9 | /// 10 | /// The uploaded cover image. 11 | /// 12 | [DataType(DataType.Upload)] 13 | public IFormFile? File { get; set; } 14 | 15 | /// 16 | /// The hash of an existing blueprint rendering. 17 | /// 18 | public Hash? Hash { get; set; } 19 | 20 | /// 21 | /// An optional rectangle to specify how the image should be cropped before it is resized. 22 | /// If unspecified, the image will not be cropped and only resized to fit the cover limits. 23 | /// 24 | public ImageService.CropRectangle? Crop { get; set; } 25 | 26 | public IEnumerable Validate(ValidationContext validationContext) 27 | { 28 | if (File == null && Hash == null) 29 | { 30 | yield return new ValidationResult($"Either {nameof(File)} or {nameof(Hash)} must be set."); 31 | } 32 | else if (File != null && Hash != null) 33 | { 34 | yield return new ValidationResult($"Only one of either {nameof(File)} or {nameof(Hash)} must be set."); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/Requests/EditBuildRequest.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Api.Extensions; 2 | using FactorioTech.Core.Domain; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace FactorioTech.Api.ViewModels.Requests; 6 | 7 | public class EditBuildRequest 8 | { 9 | /// 10 | /// The title or display name of the build. 11 | /// If unset (null), the existing value will not be changed. 12 | /// 13 | /// My Awesome Build 14 | [StringLength(100, MinimumLength = 3)] 15 | [DataType(DataType.Text)] 16 | public string? Title { get; set; } 17 | 18 | /// 19 | /// The build description in Markdown. 20 | /// If unset (null), the existing value will not be changed. 21 | /// 22 | /// Hello **World**! 23 | [DataType(DataType.MultilineText)] 24 | public string? Description { get; set; } 25 | 26 | /// 27 | /// The build's tags. 28 | /// If unset (null), the existing value will not be changed. 29 | /// 30 | [ValidateTag] 31 | public IEnumerable? Tags { get; set; } 32 | 33 | /// 34 | /// The build's icons. 35 | /// If unset (null), the existing value will not be changed. 36 | /// 37 | public IEnumerable? Icons { get; set; } 38 | 39 | /// 40 | /// The build's cover image is either a file upload or an existing blueprint rendering, 41 | /// along with a crop rectangle. 42 | /// If unset (null), the existing value will not be changed. 43 | /// 44 | public CoverRequest? Cover { get; set; } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/Requests/VersionRequest.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | #pragma warning disable 8618 // Non-nullable property must contain a non-null value when exiting constructor. Consider declaring the property as nullable. 5 | 6 | namespace FactorioTech.Api.ViewModels.Requests; 7 | 8 | public class VersionRequest 9 | { 10 | /// 11 | /// The icons of the version to be created. 12 | /// 13 | [Required] 14 | public IEnumerable Icons { get; set; } 15 | 16 | /// 17 | /// An optional name for the version to be created. 18 | /// 19 | [StringLength(100, MinimumLength = 2)] 20 | [DataType(DataType.Text)] 21 | public string? Name { get; set; } 22 | 23 | /// 24 | /// An optional description for the version to be created. 25 | /// 26 | [DataType(DataType.MultilineText)] 27 | public string? Description { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/UserModel.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core; 2 | using NodaTime; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | #pragma warning disable 8618 // Non-nullable property must contain a non-null value when exiting constructor. Consider declaring the property as nullable. 6 | 7 | namespace FactorioTech.Api.ViewModels; 8 | 9 | public class ThinUserModel 10 | { 11 | /// 12 | /// The user's username, also known as slug. It can consist only of latin alphanumeric characters, 13 | /// underscores and hyphens. 14 | /// It is used in URLs like the user's profile or build pages. 15 | /// 16 | /// factorio_fritz 17 | [Required] 18 | [StringLength(AppConfig.Policies.Slug.MaximumLength, MinimumLength = AppConfig.Policies.Slug.MinimumLength)] 19 | [RegularExpression(AppConfig.Policies.Slug.AllowedCharactersRegex)] 20 | public string Username { get; set; } 21 | } 22 | 23 | public class FullUserModel : ThinUserModel 24 | { 25 | /// 26 | /// The user's display name can optionally be set by a user. It is meant to be displayed across the 27 | /// site in place of the username. 28 | /// If the value is unset (null), the username should be displayed instead. 29 | /// 30 | /// Factorio Fritz 31 | public string? DisplayName { get; set; } 32 | 33 | /// 34 | /// The user's registration timestamp in UTC. 35 | /// 36 | [Required] 37 | [DataType(DataType.DateTime)] 38 | public Instant RegisteredAt { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/UsersModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FactorioTech.Api.ViewModels; 4 | 5 | public class UsersModel 6 | { 7 | /// 8 | /// The number of results on the current page. 9 | /// 10 | [Required] 11 | public int Count { get; set; } 12 | 13 | /// 14 | /// The paged, filtered and ordered list of matching users. 15 | /// 16 | [Required] 17 | public IEnumerable Users { get; set; } = Enumerable.Empty(); 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/ViewModels/VersionsModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FactorioTech.Api.ViewModels; 4 | 5 | public class VersionsModel 6 | { 7 | /// 8 | /// The number of results on the current page. 9 | /// 10 | [Required] 11 | public int Count { get; set; } 12 | 13 | /// 14 | /// The paged, filtered and ordered list of matching versions. 15 | /// 16 | [Required] 17 | public IEnumerable Versions { get; set; } = Enumerable.Empty(); 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | // IMPORTANT! 3 | // The configuration in this file is ONLY respected when the application 4 | // is started in an IDE (e.g. Visual Studio) or via dotnet run 5 | // Use environment variables in docker-compose.yaml to configure the 6 | // application running in the Docker environment! 7 | 8 | "ConnectionStrings": { 9 | "Postgres": "Host=localhost;Database=postgres;Username=postgres;Password=postgres", 10 | "Storage": "UseDevelopmentStorage=true" 11 | }, 12 | "AppConfig": { 13 | //"WebUri": "https://local.factorio.tech", 14 | "WebUri": "http://localhost:3000", 15 | "ApiUri": "https://localhost:5101", 16 | //"IdentityUri": "https://identity.local.factorio.tech", 17 | "IdentityUri": "https://localhost:5001" 18 | }, 19 | "DetailedErrors": true 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/BlocklistAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FactorioTech.Core; 4 | 5 | public class BlocklistAttribute : ValidationAttribute 6 | { 7 | private readonly HashSet blocklist; 8 | 9 | public BlocklistAttribute(string blocklist) 10 | { 11 | this.blocklist = new HashSet(blocklist.Split(",")); 12 | } 13 | 14 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 15 | { 16 | var str = value?.ToString()?.ToLowerInvariant(); 17 | if (str != null && blocklist.Contains(str)) 18 | return new ValidationResult($"{validationContext.DisplayName} must not match any of the blocked terms."); 19 | 20 | return ValidationResult.Success; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/BuildInformation.cs: -------------------------------------------------------------------------------- 1 | namespace FactorioTech.Core; 2 | 3 | // note: this file will be written by docker build 4 | // when making changes here, remember to also change the dockerfile! 5 | public class BuildInformation 6 | { 7 | public const string Version = "0.0.0-local"; 8 | public const string Branch = ""; 9 | public const string Sha = ""; 10 | public const string Uri = ""; 11 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Data/AppDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.EntityFramework.Options; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace FactorioTech.Core.Data; 7 | 8 | public class AppDbContextFactory : IDesignTimeDbContextFactory 9 | { 10 | public AppDbContext CreateDbContext(string[] args) => 11 | CreateDbContext("Host=localhost;Database=postgres;Username=postgres;Password=postgres"); 12 | 13 | public static AppDbContext CreateDbContext(string connectionString) 14 | { 15 | var optionsBuilder = new DbContextOptionsBuilder(); 16 | optionsBuilder.UseNpgsql(connectionString, o => o.UseNodaTime()); 17 | return new AppDbContext(optionsBuilder.Options, Options.Create(new OperationalStoreOptions())); 18 | } 19 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/BuildVersion.cs: -------------------------------------------------------------------------------- 1 | using NodaTime; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FactorioTech.Core.Domain; 5 | 6 | public class BuildVersion 7 | { 8 | [Key] 9 | public Guid VersionId { get; init; } 10 | 11 | [Required] 12 | public Guid BuildId { get; init; } 13 | 14 | [Required] 15 | public Instant CreatedAt { get; init; } 16 | 17 | [Required] 18 | [MaxLength(32)] 19 | [MinLength(32)] 20 | public Hash Hash { get; init; } 21 | 22 | [Required] 23 | public PayloadType Type { get; init; } 24 | 25 | [Required] 26 | public Version GameVersion { get; init; } 27 | 28 | [MaxLength(100)] 29 | public string? Name { get; init; } 30 | 31 | public string? Description { get; init; } 32 | 33 | [Required] 34 | public IEnumerable Icons { get; init; } 35 | 36 | // navigation properties -> will be null if not included explicitly 37 | 38 | public Payload? Payload { get; init; } 39 | 40 | public Build? Build { get; init; } 41 | 42 | public BuildVersion( 43 | Guid versionId, 44 | Guid buildId, 45 | Instant createdAt, 46 | Hash hash, 47 | PayloadType type, 48 | Version gameVersion, 49 | string? name, 50 | string? description, 51 | IEnumerable icons) 52 | { 53 | VersionId = versionId; 54 | BuildId = buildId; 55 | CreatedAt = createdAt; 56 | GameVersion = gameVersion; 57 | Hash = hash; 58 | Type = type; 59 | Name = name; 60 | Description = description; 61 | Icons = icons; 62 | } 63 | 64 | #pragma warning disable 8618 // required for EF 65 | private BuildVersion() { } 66 | #pragma warning restore 8618 67 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/Favorite.cs: -------------------------------------------------------------------------------- 1 | using NodaTime; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FactorioTech.Core.Domain; 5 | 6 | public class Favorite 7 | { 8 | [Required] 9 | public Guid UserId { get; set; } 10 | 11 | [Required] 12 | public Guid BuildId { get; set; } 13 | 14 | [Required] 15 | public Instant CreatedAt { get; set; } 16 | 17 | // navigation properties -> will be null if not included explicitly 18 | 19 | public User? User { get; set; } 20 | public Build? Build { get; set; } 21 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/GameIcon.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FactorioTech.Core.Domain; 4 | 5 | public class GameIcon 6 | { 7 | [Required] 8 | public IconType Type { get; init; } 9 | 10 | [Required] 11 | public string Name { get; init; } 12 | 13 | public GameIcon(IconType type, string name) 14 | { 15 | Type = type; 16 | Name = name; 17 | } 18 | 19 | #pragma warning disable 8618 20 | [Obsolete("Do not use this constructor. It's required for model binding only.", true)] 21 | public GameIcon() { } 22 | #pragma warning restore 8618 23 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/IconType.cs: -------------------------------------------------------------------------------- 1 | namespace FactorioTech.Core.Domain; 2 | 3 | public enum IconType 4 | { 5 | Virtual, 6 | Item, 7 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/ImageMeta.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace FactorioTech.Core.Domain; 4 | 5 | [Owned] 6 | public class ImageMeta 7 | { 8 | public string ImageId { get; init; } 9 | public int Width { get; init; } 10 | public int Height { get; init; } 11 | public long Size { get; init; } 12 | 13 | public ImageMeta(string imageId, int width, int height, long size) 14 | { 15 | ImageId = imageId; 16 | Width = width; 17 | Height = height; 18 | Size = size; 19 | } 20 | 21 | #pragma warning disable 8618 // required for EF 22 | private ImageMeta() { } 23 | #pragma warning restore 8618 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/Payload.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FactorioTech.Core.Domain; 4 | 5 | public class Payload 6 | { 7 | [Key] 8 | [MaxLength(32)] 9 | [MinLength(32)] 10 | public Hash Hash { get; init; } 11 | 12 | [Required] 13 | public PayloadType Type { get; init; } 14 | 15 | [Required] 16 | public Version GameVersion { get; init; } 17 | 18 | [Required] 19 | public string Encoded { get; init; } 20 | 21 | public Payload(Hash hash, PayloadType type, Version gameVersion, string encoded) 22 | { 23 | Hash = hash; 24 | Type = type; 25 | GameVersion = gameVersion; 26 | Encoded = encoded; 27 | } 28 | 29 | #pragma warning disable 8618 // required for EF 30 | private Payload() { } 31 | #pragma warning restore 8618 32 | 33 | private sealed class HashEqualityComparer : IEqualityComparer 34 | { 35 | public bool Equals(Payload? x, Payload? y) 36 | { 37 | if (ReferenceEquals(x, y)) return true; 38 | if (ReferenceEquals(x, null)) return false; 39 | if (ReferenceEquals(y, null)) return false; 40 | if (x.GetType() != y.GetType()) return false; 41 | return x.Hash.Equals(y.Hash); 42 | } 43 | 44 | public int GetHashCode(Payload obj) => obj.Hash.GetHashCode(); 45 | } 46 | 47 | public static IEqualityComparer EqualityComparer => new HashEqualityComparer(); 48 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/PayloadType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace FactorioTech.Core.Domain; 4 | 5 | public enum PayloadType 6 | { 7 | [EnumMember(Value = "blueprint")] 8 | Blueprint, 9 | 10 | [EnumMember(Value = "blueprint-book")] 11 | Book, 12 | 13 | [EnumMember(Value = "deconstruction-planner")] 14 | DeconstructionPlanner, 15 | 16 | [EnumMember(Value = "upgrade-planner")] 17 | UpgradePlanner, 18 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/Role.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace FactorioTech.Core.Domain; 4 | 5 | public class Role : IdentityRole 6 | { 7 | public const string Administrator = "Administrator"; 8 | public const string Moderator = "Moderator"; 9 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Domain/User.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using NodaTime; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace FactorioTech.Core.Domain; 6 | 7 | public class User : IdentityUser 8 | { 9 | [Required] 10 | public Instant RegisteredAt { get; init; } 11 | 12 | public DateTimeZone? TimeZone { get; set; } 13 | 14 | [MinLength(3)] 15 | [MaxLength(256)] 16 | [ProtectedPersonalData] 17 | public string? DisplayName { get; set; } 18 | 19 | // navigation properties -> will be null if not included explicitly 20 | 21 | public IEnumerable? Builds { get; set; } 22 | 23 | public IEnumerable? Favorites { get; set; } 24 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/PayloadCache.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using FactorioTech.Core.Services; 3 | 4 | namespace FactorioTech.Core; 5 | 6 | public sealed class PayloadCache : Dictionary 7 | { 8 | public async Task EnsureInitializedGraph(FactorioApi.BlueprintEnvelope envelope) 9 | { 10 | await EnsureInitialized(envelope); 11 | 12 | if (envelope.BlueprintBook?.Blueprints != null) 13 | { 14 | foreach (var inner in envelope.BlueprintBook.Blueprints) 15 | { 16 | await EnsureInitializedGraph(inner); 17 | } 18 | } 19 | } 20 | 21 | public async Task EnsureInitialized(FactorioApi.BlueprintEnvelope envelope) 22 | { 23 | if (TryGetValue(envelope, out var payload)) 24 | { 25 | return payload; 26 | } 27 | 28 | var converter = new BlueprintConverter(); 29 | var encoded = await converter.Encode(envelope); 30 | 31 | payload = new Payload( 32 | Hash.Compute(encoded), 33 | converter.ParseType(envelope.Entity.Item), 34 | converter.DecodeGameVersion(envelope.Entity.Version), 35 | encoded); 36 | 37 | TryAdd(envelope, payload); 38 | return payload; 39 | } 40 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/TelemetryInitializers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Channel; 2 | using Microsoft.ApplicationInsights.Extensibility; 3 | using Microsoft.AspNetCore.Http; 4 | using System.Reflection; 5 | 6 | namespace FactorioTech.Core; 7 | 8 | public class CloudRoleInitializer : ITelemetryInitializer 9 | { 10 | private static readonly string AppName = Assembly.GetEntryAssembly()?.GetName().Name?.Split('.').Last() ?? "unknown"; 11 | 12 | public void Initialize(ITelemetry telemetry) 13 | { 14 | telemetry.Context.Cloud.RoleName = AppName; 15 | } 16 | } 17 | 18 | [AutoConstructor] 19 | public partial class UserInitializer : ITelemetryInitializer 20 | { 21 | private readonly IHttpContextAccessor httpContextAccessor; 22 | 23 | public void Initialize(ITelemetry telemetry) 24 | { 25 | var user = httpContextAccessor.HttpContext?.User; 26 | if (user?.Identity?.IsAuthenticated == true) 27 | { 28 | telemetry.Context.User.AccountId = user.GetUserId().ToString(); 29 | telemetry.Context.User.AuthenticatedUserId = user.GetUserName(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Core/Utils.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace FactorioTech.Core; 5 | 6 | public static class Utils 7 | { 8 | /// 9 | /// Shamelessly stolen from Kotlin 10 | /// 11 | /// inline fun T.let(block: (T) -> R): R 12 | /// 13 | /// see https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html 14 | /// 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static TR Let(this T v, Func f) => f(v); 17 | 18 | public static IEnumerable ToGameIcons(this IEnumerable? icons) => 19 | icons?.OrderBy(i => i.Index) 20 | .Select(i => new GameIcon(Enum.Parse(i.Signal.Type, true), i.Signal.Name)) 21 | ?? Enumerable.Empty(); 22 | 23 | public static IReadOnlyDictionary ToItemStats(this IEnumerable? items) => 24 | items?.GroupBy(e => e.Name) 25 | .OrderByDescending(g => g.Count()) 26 | .ToDictionary(g => g.Key.ToLowerInvariant(), g => g.Count()) 27 | ?? new Dictionary(0); 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Configuration/OAuthClientConfig.cs: -------------------------------------------------------------------------------- 1 | namespace FactorioTech.Identity.Configuration; 2 | 3 | public class OAuthClientConfig 4 | { 5 | public AvailableOAuthClients OAuthClients { get; init; } = new(); 6 | 7 | public record AvailableOAuthClients 8 | { 9 | public OAuthClientSettings Web { get; init; } = new() 10 | { 11 | ClientId = "frontend", 12 | ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", 13 | RedirectUri = "https://local.factorio.tech/api/auth/callback", 14 | PostLogoutRedirectUri = "https://local.factorio.tech", 15 | }; 16 | } 17 | 18 | public record OAuthClientSettings 19 | { 20 | public string ClientId { get; init; } = string.Empty; 21 | public string ClientSecret { get; init; } = string.Empty; 22 | public string RedirectUri { get; init; } = string.Empty; 23 | public string PostLogoutRedirectUri { get; init; } = string.Empty; 24 | } 25 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Configuration/OAuthProviderConfig.cs: -------------------------------------------------------------------------------- 1 | namespace FactorioTech.Identity.Configuration; 2 | 3 | public class OAuthProviderConfig 4 | { 5 | public IDictionary? OAuthProviders { get; init; } 6 | 7 | public record OAuthProviderCredentials 8 | { 9 | public string ClientId { get; init; } = string.Empty; 10 | public string ClientSecret { get; init; } = string.Empty; 11 | } 12 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/FactorioTech.Identity.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | latestMajor 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailModel 3 | @{ ViewData["Title"] = "Confirm email"; } 4 | 5 |
6 |

@ViewData["Title"]

7 | 8 |
9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/ConfirmEmail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using FactorioTech.Identity.Extensions; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.AspNetCore.WebUtilities; 8 | using System.Text; 9 | 10 | namespace FactorioTech.Identity.Pages; 11 | 12 | [AllowAnonymous] 13 | [SecurityHeaders] 14 | public class ConfirmEmailModel : PageModel 15 | { 16 | private readonly UserManager userManager; 17 | 18 | public ConfirmEmailModel(UserManager userManager) 19 | { 20 | this.userManager = userManager; 21 | } 22 | 23 | [TempData] 24 | public string? StatusMessage { get; set; } 25 | 26 | public async Task OnGetAsync(string? userId, string? code) 27 | { 28 | if (userId == null || code == null) 29 | return RedirectToPage("/Index"); 30 | 31 | var user = await userManager.FindByIdAsync(userId); 32 | if (user == null) 33 | return NotFound($"Unable to load user with ID '{userId}'."); 34 | 35 | var result = await userManager.ConfirmEmailAsync(user, 36 | Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))); 37 | 38 | StatusMessage = result.Succeeded 39 | ? "Thank you for confirming your email." 40 | : "Error confirming your email."; 41 | 42 | return RedirectToPage("./Manage/Email"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/ConfirmEmailChange.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailChangeModel 3 | @{ ViewData["Title"] = "Confirm email change"; } 4 | 5 |
6 |

@ViewData["Title"]

7 | 8 |
9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/403.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AccessDeniedModel 3 | @{ ViewData["Title"] = "Access denied"; } 4 | 5 |
6 |

@ViewData["Title"]

7 |

You do not have access to this resource.

8 |
9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/403.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace FactorioTech.Identity.Pages.Errors; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | [AllowAnonymous] 10 | public class AccessDeniedModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/404.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model NotFoundModel 3 | @{ ViewData["Title"] = "Page not found"; } 4 | 5 |
6 |

404 - @ViewData["Title"]

7 |

The page you're looking for does not exist :(

8 |
9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/404.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace FactorioTech.Identity.Pages.Errors; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | [AllowAnonymous] 10 | public class NotFoundModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/500.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ ViewData["Title"] = "Error"; } 4 | 5 |
6 |

@ViewData["Title"]

7 |

An error occurred while processing your request 😢

8 | 9 |

Request ID: @Model.RequestId

10 | 11 | @if (!string.IsNullOrWhiteSpace(Model.ErrorId)) 12 | { 13 |

Error ID: @Model.ErrorId

14 | } 15 | 16 |

17 | Please help us fix this error by creating a ticket in the GitHub issue tracker. 18 | Please quote the ID(s) above and describe what you did when the issue occurred. Thank you very much ❤️ 🚀 19 |

20 |
21 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Errors/500.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using System.Diagnostics; 5 | 6 | namespace FactorioTech.Identity.Pages.Errors; 7 | 8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 9 | [IgnoreAntiforgeryToken] 10 | [AllowAnonymous] 11 | public class ErrorModel : PageModel 12 | { 13 | public string RequestId { get; set; } = "unknown"; 14 | 15 | [FromQuery] 16 | public string? ErrorId { get; set; } 17 | 18 | public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 19 | } -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LogoutModel 3 | @{ ViewData["Title"] = "Log out"; } 4 | 5 |
6 |

@ViewData["Title"]

7 |

You have successfully logged out. Click here to return to the application.

8 |
9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Logout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using FactorioTech.Core; 3 | using FactorioTech.Core.Domain; 4 | using FactorioTech.Identity.Extensions; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace FactorioTech.Identity.Pages; 12 | 13 | [AllowAnonymous] 14 | [SecurityHeaders] 15 | public class LogoutModel : PageModel 16 | { 17 | private readonly SignInManager signInManager; 18 | private readonly ILogger logger; 19 | private readonly IIdentityServerInteractionService interaction; 20 | 21 | public Uri FrontendUri { get; } 22 | 23 | public LogoutModel( 24 | SignInManager signInManager, 25 | ILogger logger, 26 | IOptions appConfig, 27 | IIdentityServerInteractionService interaction) 28 | { 29 | this.logger = logger; 30 | this.interaction = interaction; 31 | this.signInManager = signInManager; 32 | FrontendUri = appConfig.Value.WebUri; 33 | } 34 | 35 | public async Task OnGetAsync(string? logoutId = null) 36 | { 37 | if (User.Identity?.IsAuthenticated == true) 38 | { 39 | await signInManager.SignOutAsync(); 40 | logger.LogInformation("User logged out"); 41 | 42 | var context = await interaction.GetLogoutContextAsync(logoutId); 43 | if (context.PostLogoutRedirectUri != null) 44 | { 45 | return Redirect(context.PostLogoutRedirectUri); 46 | } 47 | } 48 | 49 | return Page(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Manage/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Pages/Shared/_Layout.cshtml"; 3 | } 4 | 5 | 17 | 18 | @section Scripts { 19 | @await RenderSectionAsync("Scripts", required: false) 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Shared/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!string.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/Shared/_ValidationScripts.cshtml: -------------------------------------------------------------------------------- 1 | 5 | 9 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using FactorioTech.Core 3 | @using FactorioTech.Core.Data 4 | @using FactorioTech.Core.Domain 5 | @using FactorioTech.Identity 6 | @namespace FactorioTech.Identity.Pages 7 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 8 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "FactorioTech.Identity": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": "true", 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | // IMPORTANT! 3 | // The configuration in this file is ONLY respected when the application 4 | // is started in an IDE (e.g. Visual Studio) or via dotnet run 5 | // Use environment variables in docker-compose.yaml to configure the 6 | // application running in the Docker environment! 7 | 8 | "ConnectionStrings": { 9 | "Postgres": "Host=localhost;Database=postgres;Username=postgres;Password=postgres" 10 | }, 11 | "AppConfig": { 12 | "WebUri": "https://local.factorio.tech", 13 | //"ApiUri": "https://api.local.factorio.tech", 14 | "IdentityUri": "https://localhost:5001" 15 | }, 16 | "DetailedErrors": true 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/wwwroot/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: rgb(237, 239, 242); 3 | background: rgb(40, 42, 51); 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | } 7 | 8 | main { 9 | padding-top: 2rem; 10 | } 11 | 12 | hr { 13 | border-top: 1px solid rgb(79, 86, 110); 14 | } 15 | 16 | .navbar { 17 | height: 74px; 18 | background: rgb(17, 18, 22); 19 | } 20 | 21 | .navbar-brand svg { 22 | height: 40px; 23 | cursor: pointer; 24 | } 25 | 26 | .navbar-brand svg:hover { 27 | opacity: 0.8; 28 | } 29 | 30 | .table { 31 | color: rgb(237, 239, 242); 32 | } 33 | 34 | footer a { 35 | color: #ffffff; 36 | } 37 | 38 | footer a:hover { 39 | color: rgb(193, 244, 255); 40 | } 41 | 42 | .input-wrapper { 43 | padding: 5px 14px; 44 | background: rgb(26, 28, 35); 45 | border: 2px solid rgb(136, 141, 162); 46 | color: rgb(208, 211, 220); 47 | display: flex; 48 | -webkit-box-align: center; 49 | align-items: center; 50 | border-radius: 6px; 51 | } 52 | 53 | .input-control { 54 | font-size: 18px; 55 | font-weight: 400; 56 | font-family: "DM Sans", sans-serif; 57 | line-height: 1.8; 58 | border: 0px; 59 | background: transparent; 60 | color: rgb(208, 211, 220); 61 | flex: 1 0 auto; 62 | } 63 | 64 | .input-control:focus { 65 | outline: 0px; 66 | } 67 | 68 | .input-control[readonly] { 69 | cursor: not-allowed; 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/FactorioTech.Identity/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorio-builds/factorio-builds-tech/5a221039f226a91037330dc0d9aca113386ab3d3/backend/src/FactorioTech.Identity/wwwroot/favicon.ico -------------------------------------------------------------------------------- /backend/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "belt": [ 3 | "balancer", 4 | "prioritizer", 5 | "tap", 6 | "transport belt (yellow)", 7 | "fast transport belt (red)", 8 | "express transport belt (blue)" 9 | ], 10 | "state": ["early game", "mid game", "late game", "end game (megabase)"], 11 | "meta": [ 12 | "beaconized", 13 | "tileable", 14 | "compact", 15 | "marathon", 16 | "storage", 17 | "inputs are marked", 18 | "ups optimized" 19 | ], 20 | "power": ["nuclear", "kovarex enrichment", "solar", "steam", "accumulator"], 21 | "production": [ 22 | "oil processing", 23 | "coal liquification", 24 | "electronic circuit (green)", 25 | "advanced circuit (red)", 26 | "processing unit (blue)", 27 | "batteries", 28 | "rocket parts", 29 | "science", 30 | "research (labs)", 31 | "belts", 32 | "smelting", 33 | "mining", 34 | "uranium", 35 | "plastic", 36 | "modules", 37 | "mall (make everything)", 38 | "inserters", 39 | "guns and ammo", 40 | "robots", 41 | "other", 42 | "belt based", 43 | "logistic (bot) based" 44 | ], 45 | "train": [ 46 | "loading station", 47 | "unloading station", 48 | "pax", 49 | "junction", 50 | "roundabout", 51 | "crossing", 52 | "stacker", 53 | "track", 54 | "left-hand-drive", 55 | "right-hand-drive" 56 | ], 57 | "circuit": ["indicator", "counter"] 58 | } 59 | -------------------------------------------------------------------------------- /backend/test/FactorioTech.Tests/FactorioTech.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | latestMajor 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /backend/test/FactorioTech.Tests/Helpers/PayloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Data; 2 | using FactorioTech.Core.Domain; 3 | 4 | namespace FactorioTech.Tests.Helpers; 5 | 6 | public class PayloadBuilder 7 | { 8 | private string encoded = TestData.SimpleBlueprintEncoded; 9 | private Hash? hash; 10 | 11 | public PayloadBuilder WithEncoded(string encoded) 12 | { 13 | this.encoded = encoded; 14 | return this; 15 | } 16 | 17 | public PayloadBuilder WithRandomHash() 18 | { 19 | hash = Hash.Compute(Guid.NewGuid().ToString()); 20 | return this; 21 | } 22 | 23 | public async Task Save(AppDbContext dbContext, bool clearCache = true) 24 | { 25 | var payload = new Payload( 26 | hash ?? Hash.Compute(encoded), 27 | PayloadType.Book, 28 | Version.Parse("1.0.0.0"), 29 | encoded); 30 | 31 | dbContext.Add(payload); 32 | await dbContext.SaveChangesAsync(); 33 | 34 | if (clearCache) 35 | { 36 | dbContext.ClearCache(); 37 | } 38 | 39 | return payload; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/test/FactorioTech.Tests/Helpers/TestUtils.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Domain; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Security.Claims; 4 | 5 | namespace FactorioTech.Tests.Helpers; 6 | 7 | public static class TestUtils 8 | { 9 | public static Lazy Tags = new(BuildTags.Load); 10 | 11 | public static DbContext ClearCache(this DbContext db) 12 | { 13 | foreach (var entry in db.ChangeTracker.Entries()) 14 | { 15 | db.Entry(entry.Entity).State = EntityState.Detached; 16 | } 17 | 18 | return db; 19 | } 20 | 21 | public static ClaimsPrincipal ToClaimsPrincipal(this User user) => 22 | new(new ClaimsIdentity(new Claim[] 23 | { 24 | new(ClaimTypes.NameIdentifier, user.Id.ToString()), 25 | new("username", user.UserName), 26 | })); 27 | } -------------------------------------------------------------------------------- /backend/test/FactorioTech.Tests/Helpers/UserBuilder.cs: -------------------------------------------------------------------------------- 1 | using FactorioTech.Core.Data; 2 | using FactorioTech.Core.Domain; 3 | using NodaTime; 4 | 5 | namespace FactorioTech.Tests.Helpers; 6 | 7 | public class UserBuilder 8 | { 9 | private Guid userId; 10 | 11 | public async Task Save(AppDbContext dbContext, bool clearCache = true) 12 | { 13 | userId = Guid.NewGuid(); 14 | var user = new User 15 | { 16 | Id = userId, 17 | Email = $"test-{userId}@factorio.tech", 18 | NormalizedEmail = $"test-{userId}@factorio.tech".ToUpperInvariant(), 19 | UserName = $"test-{userId}", 20 | NormalizedUserName = $"test-{userId}".ToUpperInvariant(), 21 | DisplayName = $"Test: {userId}", 22 | RegisteredAt = SystemClock.Instance.GetCurrentInstant(), 23 | }; 24 | 25 | dbContext.Add(user); 26 | await dbContext.SaveChangesAsync(); 27 | 28 | if (clearCache) 29 | { 30 | dbContext.ClearCache(); 31 | } 32 | 33 | return user; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | *.secret.yaml 2 | -------------------------------------------------------------------------------- /deploy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: factorio-tech 3 | type: application 4 | 5 | # This is the chart version. This version number should be incremented each time you make changes 6 | # to the chart and its templates, including the app version. 7 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 8 | version: 1.0.0-ci 9 | 10 | # This is the version number of the application being deployed. This version number should be 11 | # incremented each time you make changes to the application. Versions are not expected to 12 | # follow Semantic Versioning. They should reflect the version the application is using. 13 | appVersion: ci 14 | -------------------------------------------------------------------------------- /deploy/templates/api/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-api 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: api 8 | data: 9 | AppConfig__DataDir: /mnt/data 10 | AppConfig__ProtectedDataDir: /mnt/protected 11 | AppConfig__FactorioDir: /mnt/factorio/Factorio_1.0.0 12 | AppConfig__FbsrWrapperUri: http://{{ include "factorio-tech.fullname" . }}-fbsr-wrapper 13 | AppConfig__WebUri: https://{{ .Values.hostNames.web }} 14 | AppConfig__ApiUri: https://{{ .Values.hostNames.api }} 15 | AppConfig__IdentityUri: https://{{ .Values.hostNames.identity }} 16 | -------------------------------------------------------------------------------- /deploy/templates/api/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRoute 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-api 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: api 8 | spec: 9 | entryPoints: 10 | - websecure 11 | routes: 12 | - match: Host(`{{ .Values.hostNames.api }}`) 13 | kind: Rule 14 | services: 15 | - name: {{ include "factorio-tech.fullname" . }}-api 16 | port: 80 17 | -------------------------------------------------------------------------------- /deploy/templates/api/protected-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-api-protected 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: api 8 | spec: 9 | accessModes: 10 | - ReadWriteMany 11 | storageClassName: azurefile 12 | resources: 13 | requests: 14 | storage: 1Gi 15 | -------------------------------------------------------------------------------- /deploy/templates/api/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-api 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: api 8 | spec: 9 | ports: 10 | - port: 80 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "factorio-tech.selectorLabels" . | nindent 4 }} 16 | app.kubernetes.io/component: api 17 | -------------------------------------------------------------------------------- /deploy/templates/fbsr-wrapper/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-fbsr-wrapper 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: fbsr-wrapper 8 | data: 9 | config.json: | 10 | { 11 | "factorio": "/mnt/factorio/Factorio_1.0.0" 12 | } 13 | -------------------------------------------------------------------------------- /deploy/templates/fbsr-wrapper/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-fbsr-wrapper 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: fbsr-wrapper 8 | spec: 9 | ports: 10 | - port: 80 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "factorio-tech.selectorLabels" . | nindent 4 }} 16 | app.kubernetes.io/component: fbsr-wrapper 17 | -------------------------------------------------------------------------------- /deploy/templates/identity/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-identity 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: identity 8 | data: 9 | AppConfig__ProtectedDataDir: /mnt/protected 10 | AppConfig__WebUri: https://{{ .Values.hostNames.web }} 11 | AppConfig__ApiUri: https://{{ .Values.hostNames.api }} 12 | AppConfig__IdentityUri: https://{{ .Values.hostNames.identity }} 13 | OAuthClients__Web__RedirectUri: https://{{ .Values.hostNames.web }}/api/auth/callback 14 | OAuthClients__Web__PostLogoutRedirectUri: https://{{ .Values.hostNames.web }} 15 | -------------------------------------------------------------------------------- /deploy/templates/identity/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRoute 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-identity 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: identity 8 | spec: 9 | entryPoints: 10 | - websecure 11 | routes: 12 | - match: Host(`{{ .Values.hostNames.identity }}`) 13 | kind: Rule 14 | services: 15 | - name: {{ include "factorio-tech.fullname" . }}-identity 16 | port: 80 17 | -------------------------------------------------------------------------------- /deploy/templates/identity/protected-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-identity-protected 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: identity 8 | spec: 9 | accessModes: 10 | - ReadWriteMany 11 | storageClassName: azurefile 12 | resources: 13 | requests: 14 | storage: 1Gi 15 | -------------------------------------------------------------------------------- /deploy/templates/identity/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-identity 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: identity 8 | type: Opaque 9 | data: 10 | OAuthClients__Web__ClientId: {{ .Values.web.clientId | b64enc }} 11 | OAuthClients__Web__ClientSecret: {{ .Values.web.clientSecret | b64enc }} 12 | {{- range .Values.identity.providers }} 13 | OAuthProviders__{{ .name }}__ClientId: {{ .clientId | b64enc }} 14 | OAuthProviders__{{ .name }}__ClientSecret: {{ .clientSecret | b64enc }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /deploy/templates/identity/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-identity 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: identity 8 | spec: 9 | ports: 10 | - port: 80 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "factorio-tech.selectorLabels" . | nindent 4 }} 16 | app.kubernetes.io/component: identity 17 | -------------------------------------------------------------------------------- /deploy/templates/postgres/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-postgres 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: postgres 8 | type: Opaque 9 | data: 10 | database: {{ .Values.postgres.database | b64enc }} 11 | username: {{ .Values.postgres.username | b64enc }} 12 | password: {{ .Values.postgres.password | b64enc }} 13 | connection_string: {{ printf "Host=factorio-tech-postgres;Database=%s;Username=%s;Password=%s" .Values.postgres.database .Values.postgres.username .Values.postgres.password | b64enc }} 14 | -------------------------------------------------------------------------------- /deploy/templates/postgres/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-postgres 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: postgres 8 | spec: 9 | ports: 10 | - port: 5432 11 | targetPort: postgres 12 | protocol: TCP 13 | name: postgres 14 | selector: 15 | {{- include "factorio-tech.selectorLabels" . | nindent 4 }} 16 | app.kubernetes.io/component: postgres 17 | -------------------------------------------------------------------------------- /deploy/templates/web/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRoute 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-web 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: web 8 | spec: 9 | entryPoints: 10 | - websecure 11 | routes: 12 | - match: Host(`{{ .Values.hostNames.web }}`) 13 | kind: Rule 14 | services: 15 | - name: {{ include "factorio-tech.fullname" . }}-web 16 | port: 80 17 | -------------------------------------------------------------------------------- /deploy/templates/web/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-web 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: web 8 | type: Opaque 9 | data: 10 | clientId: {{ .Values.web.clientId | b64enc }} 11 | clientSecret: {{ .Values.web.clientSecret | b64enc }} 12 | -------------------------------------------------------------------------------- /deploy/templates/web/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "factorio-tech.fullname" . }}-web 5 | labels: 6 | {{- include "factorio-tech.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: web 8 | spec: 9 | ports: 10 | - port: 80 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "factorio-tech.selectorLabels" . | nindent 4 }} 16 | app.kubernetes.io/component: web 17 | -------------------------------------------------------------------------------- /deploy/values.yaml: -------------------------------------------------------------------------------- 1 | imageNamespace: ghcr.io/factorio-builds/factorio-builds-tech 2 | 3 | hostNames: 4 | web: staging.factorio.tech 5 | api: api.factorio.tech 6 | identity: identity.factorio.tech 7 | cdn: static.factorio.tech 8 | 9 | replicas: 10 | web: 1 11 | api: 1 12 | identity: 1 13 | fbsrWrapper: 1 14 | 15 | postgres: 16 | image: postgres:13-alpine 17 | pullPolicy: IfNotPresent 18 | -------------------------------------------------------------------------------- /fbsr-wrapper/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/*.*proj.user 3 | **/azds.yaml 4 | **/charts 5 | **/bin 6 | **/obj 7 | **/Dockerfile 8 | **/Dockerfile.develop 9 | **/docker-compose.yml 10 | **/docker-compose.*.yml 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/secrets.dev.yaml 14 | **/values.dev.yaml 15 | **/.toolstarget 16 | **/README.md 17 | **/node_modules 18 | -------------------------------------------------------------------------------- /fbsr-wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Project stuff 35 | config.json 36 | src/main/resources/*.png 37 | -------------------------------------------------------------------------------- /fbsr-wrapper/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorio-builds/factorio-builds-tech/5a221039f226a91037330dc0d9aca113386ab3d3/fbsr-wrapper/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /fbsr-wrapper/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /fbsr-wrapper/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.1-jdk-11-slim as build 2 | WORKDIR /app 3 | 4 | COPY deps deps 5 | 6 | # patch call to setupWorkingDirectory because it doesn't work in Docker 7 | RUN sed -i '/setupWorkingDirectory();/d' \ 8 | deps/Java-Factorio-Data-Wrapper/FactorioDataWrapper/src/com/demod/factorio/FactorioData.java 9 | 10 | RUN mvn install --file deps/Discord-Core-Bot-Apple/DiscordCoreBotApple/pom.xml \ 11 | && mvn install --file deps/Java-Factorio-Data-Wrapper/FactorioDataWrapper/pom.xml \ 12 | && mvn install --file deps/Factorio-FBSR/FactorioBlueprintStringRenderer/pom.xml 13 | 14 | COPY src src 15 | COPY pom.xml . 16 | 17 | # add resources because they must be part of the main jar 18 | COPY deps/Factorio-FBSR/FactorioBlueprintStringRenderer/res/*.png src/main/resources/ 19 | 20 | RUN mvn package 21 | 22 | 23 | FROM openjdk:11-jdk-slim 24 | 25 | RUN apt-get update \ 26 | && apt-get install -y --no-install-recommends \ 27 | fontconfig \ 28 | && rm -rf /var/lib/apt/lists/* 29 | 30 | COPY --from=build /app/target/*.jar /app.jar 31 | 32 | VOLUME /mnt/config 33 | 34 | COPY config.docker.json /mnt/config/config.json 35 | RUN ln -s /mnt/config/config.json /config.json 36 | 37 | EXPOSE 8080 38 | ENTRYPOINT [ "java", "-classpath", "config.json", "-jar", "/app.jar" ] 39 | -------------------------------------------------------------------------------- /fbsr-wrapper/README.md: -------------------------------------------------------------------------------- 1 | # FBSR Wrapper 2 | 3 | This project is a [Spring Boot](https://spring.io/projects/spring-boot) wrapper around [Factorio-FBSR](https://github.com/demodude4u/Factorio-FBSR) that exposes a HTTP API for rendering blueprint images. 4 | 5 | ## Contributing 6 | 7 | Since FBSR components are not published to a package repository, they are included as git submodules in the [deps](deps) folder. Make sure that the submodule contents are fully available: 8 | 9 | ```bash 10 | git submodule update --init --recursive 11 | ``` 12 | 13 | You should then be able to build and package the project: 14 | 15 | ```bash 16 | ./build-local.sh 17 | ``` 18 | 19 | To allow running FBSR in Docker, a few patches/hacks have to be applied; see [build-local.sh](build-local.sh) and [Dockerfile](Dockerfile) for details. 20 | -------------------------------------------------------------------------------- /fbsr-wrapper/build-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | current_dir=$(pwd) 4 | 5 | ## patch stuff 6 | 7 | cp ${current_dir}/deps/Factorio-FBSR/FactorioBlueprintStringRenderer/res/*.png ${current_dir}/src/main/resources/ 8 | 9 | ## build dependencies 10 | 11 | cd ${current_dir}/deps/Discord-Core-Bot-Apple/DiscordCoreBotApple 12 | mvn install 13 | 14 | cd ${current_dir}/deps/Java-Factorio-Data-Wrapper/FactorioDataWrapper 15 | mvn install 16 | 17 | cd ${current_dir}/deps/Factorio-FBSR/FactorioBlueprintStringRenderer 18 | mvn install 19 | 20 | ## build package 21 | 22 | cd $current_dir 23 | mvn package 24 | -------------------------------------------------------------------------------- /fbsr-wrapper/config.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "factorio": "/mnt/factorio" 3 | } 4 | -------------------------------------------------------------------------------- /fbsr-wrapper/src/main/java/tech/factorio/fbsrwrapper/FbsrWrapperApplication.java: -------------------------------------------------------------------------------- 1 | package tech.factorio.fbsrwrapper; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.zalando.problem.ProblemModule; 9 | 10 | @SpringBootApplication 11 | public class FbsrWrapperApplication { 12 | 13 | @Configuration 14 | public static class JacksonConfiguration { 15 | 16 | @Bean 17 | public ObjectMapper objectMapper() { 18 | return new ObjectMapper() 19 | .registerModule(new ProblemModule().withStackTraces()); 20 | } 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(FbsrWrapperApplication.class, args); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /fbsr-wrapper/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.error.include-stacktrace = ALWAYS 2 | -------------------------------------------------------------------------------- /fbsr-wrapper/src/test/java/tech/factorio/fbsrwrapper/FbsrWrapperApplicationTests.java: -------------------------------------------------------------------------------- 1 | package tech.factorio.fbsrwrapper; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class FbsrWrapperApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | ["babel-plugin-transform-typescript-metadata"], 5 | ["@babel/plugin-transform-runtime"], 6 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/*.*proj.user 3 | **/azds.yaml 4 | **/charts 5 | **/bin 6 | **/obj 7 | **/Dockerfile 8 | **/Dockerfile.develop 9 | **/docker-compose.yml 10 | **/docker-compose.*.yml 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/secrets.dev.yaml 14 | **/values.dev.yaml 15 | **/.toolstarget 16 | **/README.md 17 | **/node_modules 18 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | # DEBUG_REDUX=true 2 | 3 | ## 4 | # URLS - OVERRIDE THESE IF YOU'RE NOT TARGETING THE DOCKER-COMPOSE STACK 5 | # 6 | # WEB_URL=http://localhost:3000 7 | # API_URL=https://localhost:5101 8 | # IDENTITY_URL=https://localhost:5001 9 | 10 | ### 11 | # DEVELOPMENT CERTIFICATES - YOU *MUST* CHOOSE ONE OF THE OPTIONS BELOW 12 | # 13 | # == Option 1: Trust mkcert == 14 | # 15 | # This only works if you used mkcert to generate certificates for the local 16 | # docker-compose stack. 17 | # Set this variable to "$(mkcert -CAROOT)/rootCA.pem" 18 | # Note that nextjs doesn't expand commands so you have to run the above in 19 | # your shell and copy/paste the output. 20 | # 21 | # NODE_EXTRA_CA_CERTS="xx" 22 | # 23 | # == Option 2: The Nuclear Option™ == 24 | # 25 | # Disable TLS validation altogether. Use this if you're not using mkcert or 26 | # option 1 doesn't work for some reason. 27 | # 28 | # NODE_TLS_REJECT_UNAUTHORIZED=0 29 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | out 4 | build -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | public/img/icons 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files 29 | .env 30 | .env.* 31 | !.env.local.example 32 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 16.17.0 2 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "jsxBracketSameLine": false, 6 | "printWidth": 80, 7 | "semi": false, 8 | "singleQuote": false, 9 | "tabWidth": 2, 10 | "trailingComma": "es5", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install --frozen-lockfile 7 | 8 | COPY . . 9 | 10 | ENV NODE_ENV=production 11 | ENV NEXT_TELEMETRY_DISABLED=1 12 | 13 | RUN yarn build 14 | 15 | 16 | FROM node:lts-alpine AS app 17 | 18 | ENV NODE_ENV=production 19 | ENV NEXT_TELEMETRY_DISABLED=1 20 | 21 | COPY --from=build /app/next.config.js . 22 | COPY --from=build /app/.next .next 23 | COPY --from=build /app/public public 24 | COPY --from=build /app/node_modules node_modules 25 | 26 | EXPOSE 3000 27 | ENTRYPOINT [ "node_modules/.bin/next", "start" ] 28 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Factorio Builds/Tech Frontend 2 | 3 | This project aims to be a website and tool to share and browse [blueprints](https://wiki.factorio.com/Blueprint) for the [Factorio](https://factorio.com/) game, with strong values in user experience to make it the least painful experience to search/filter, and create builds. 4 | 5 | ### Stack 6 | 7 | - React (https://reactjs.org/) 8 | - Nextjs (https://nextjs.org/) 9 | - TypeScript (https://www.typescriptlang.org/) 10 | 11 | ### Pre-requisites 12 | 13 | - Node (12.19.0)
14 | https://nodejs.org/download/release/v12.19.0/ or via `nvm`
15 | - Yarn
16 | https://classic.yarnpkg.com/lang/en/ 17 | 18 | ### Get started 19 | 20 | From the terminal, in the checked out directory: 21 | 22 | - Duplicate the env file
23 | `cp .env.example .env` 24 | - Fill `.env` file with your AWS and Discord credentials
25 | `AWS_S3_BUCKET=""`
26 | `AWS_ACCESS_KEY_ID=""`
27 | `AWS_SECRET_ACCESS_KEY=""`
28 | `DISCORD_CLIENT_ID=""`
29 | `DISCORD_CLIENT_SECRET=""` 30 | - Install dependencies
31 | `yarn` 32 | - Start Docker
33 | `docker-compose -f docker-compose.yml up` 34 | - Migrate/seed the database
35 | `yarn typeorm schema:sync`
36 | `yarn db:seed:run` 37 | - Start the dev server
38 | `yarn dev` 39 | 40 | Having problems with setting up the project? Open an issue! 41 | -------------------------------------------------------------------------------- /frontend/components/form/Checkbox/checkbox.component.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react" 2 | import { useCheckbox } from "@react-aria/checkbox" 3 | import { VisuallyHidden } from "@react-aria/visually-hidden" 4 | import { useToggleState } from "@react-stately/toggle" 5 | import cx from "classnames" 6 | import * as S from "./checkbox.styles" 7 | 8 | interface ICheckboxProps { 9 | id: string 10 | label: string 11 | prefix?: JSX.Element 12 | value: string 13 | checked: boolean 14 | onChange: (isSelected: boolean) => void 15 | inline?: boolean 16 | } 17 | 18 | const Checkbox: React.FC = (props) => { 19 | const state = useToggleState({ ...props, isSelected: props.checked }) 20 | const ref = useRef(null) 21 | const { inputProps } = useCheckbox(props, state, ref) 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {state.isSelected} 30 | 31 | {props.label && ( 32 | 33 | {props.prefix && {props.prefix}} 34 | {props.label} 35 | 36 | )} 37 | 38 | 39 | ) 40 | } 41 | 42 | Checkbox.defaultProps = { 43 | inline: false, 44 | } 45 | 46 | export default Checkbox 47 | -------------------------------------------------------------------------------- /frontend/components/form/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./checkbox.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/ErrorMessage/error-message.component.tsx: -------------------------------------------------------------------------------- 1 | import * as S from "./error-message.styles" 2 | 3 | interface ErrorMessageProps { 4 | children: React.ReactNode 5 | } 6 | 7 | const ErrorMessage: React.FC = (props) => { 8 | return {props.children} 9 | } 10 | 11 | export default ErrorMessage 12 | -------------------------------------------------------------------------------- /frontend/components/form/ErrorMessage/error-message.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const ErrorMessageWrapper = styled("div", getTypo(ETypo.FORM_INPUT), { 6 | display: "flex", 7 | alignItems: "center", 8 | color: "$danger", 9 | marginTop: "4px", 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/components/form/ErrorMessage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./error-message.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/FormError/form-error.component.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react" 2 | import { AxiosError } from "axios" 3 | import * as S from "./form-error.styles" 4 | 5 | interface IFormErrorProps { 6 | error: AxiosError 7 | message?: string 8 | } 9 | 10 | const FormError = (props: IFormErrorProps): JSX.Element => { 11 | const message = useMemo(() => { 12 | if (props.message) { 13 | return `Error: ${props.message}` 14 | } 15 | 16 | if (props.error.response?.status === 401) { 17 | return "Error: unauthentified, please log in." 18 | } 19 | 20 | return "Error: something unexpected happened." 21 | }, [props.error, props.message]) 22 | 23 | return {message} 24 | } 25 | 26 | export default FormError 27 | -------------------------------------------------------------------------------- /frontend/components/form/FormError/form-error.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const FormErrorWrapper = styled("div", { 4 | background: "linear-gradient(90deg, #6d2d28 0%, #333642 100%)", 5 | color: "$fadedBlue900", 6 | padding: "10px 14px", 7 | borderRadius: "5px", 8 | }) 9 | -------------------------------------------------------------------------------- /frontend/components/form/FormError/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./form-error.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/FormikCheckbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./formik-checkbox.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/FormikInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./formik-input.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/FormikInputWrapper/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./formik-input-wrapper.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/FormikSelect/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./formik-select.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./input.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/InputGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./input-group.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/InputGroup/input-group.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import Stacker from "../../ui/Stacker" 4 | import * as S from "./input-group.styles" 5 | 6 | interface IInputGroup { 7 | children?: React.ReactNode 8 | legend: string | JSX.Element 9 | error?: any 10 | } 11 | 12 | const InputGroup: React.FC = (props) => { 13 | const classNames = cx({ 14 | "is-error": props.error, 15 | }) 16 | 17 | return ( 18 | 19 | 20 | {props.legend} 21 | {props.children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default InputGroup 28 | -------------------------------------------------------------------------------- /frontend/components/form/InputGroup/input-group.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const StyledInputGroup = styled("div", { 6 | display: "flex", 7 | flexDirection: "column", 8 | }) 9 | 10 | export const Legend = styled("div", getTypo(ETypo.FORM_LABEL), { 11 | color: "$fadedBlue900", 12 | 13 | span: { 14 | fontWeight: 400, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/components/form/InputWrapper/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./input-wrapper.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/InputWrapper/input-wrapper.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import ThumbsUp from "../../../icons/thumbs-up" 4 | import ErrorMessage from "../ErrorMessage" 5 | import * as S from "./input-wrapper.styles" 6 | 7 | interface IInputWrapper extends React.ComponentPropsWithoutRef<"div"> { 8 | uid: string 9 | label?: string | React.ReactElement 10 | validFeedback?: string 11 | error?: any 12 | } 13 | 14 | const InputWrapper: React.FC = (props) => { 15 | const classNames = cx(props.className, { 16 | "is-error": props.error, 17 | "is-valid": props.validFeedback && !props.error, 18 | }) 19 | 20 | return ( 21 | 22 | {props.label && {props.label}} 23 | {props.children} 24 | {props.error && {props.error}} 25 | {props.validFeedback && ( 26 | 27 | 28 | {props.validFeedback} 29 | 30 | )} 31 | 32 | ) 33 | } 34 | 35 | export default InputWrapper 36 | -------------------------------------------------------------------------------- /frontend/components/form/InputWrapper/input-wrapper.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const StyledInputWrapper = styled("div", { 6 | display: "flex", 7 | flexDirection: "column", 8 | 9 | "&.size-small": { 10 | maxWidth: "250px", 11 | }, 12 | }) 13 | 14 | export const Label = styled("label", getTypo(ETypo.FORM_LABEL), { 15 | color: "$fadedBlue900", 16 | marginBottom: "6px", 17 | 18 | span: { 19 | fontWeight: 400, 20 | }, 21 | }) 22 | 23 | export const ValidMessage = styled("div", getTypo(ETypo.FORM_INPUT), { 24 | display: "flex", 25 | alignItems: "center", 26 | color: "#68c06b", 27 | marginTop: "8px", 28 | 29 | "& svg": { 30 | width: "16px", 31 | marginRight: "8px", 32 | }, 33 | 34 | "& svg path": { 35 | fill: "#68c06b", 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /frontend/components/form/MarkdownEditor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./markdown-editor.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/MarkdownEditor/markdown-editor.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import SimpleMDE from "react-simplemde-editor" 3 | import "easymde/dist/easymde.min.css" 4 | import * as S from "./markdown-editor.styles" 5 | 6 | interface IMarkdownEditorProps { 7 | id: string 8 | name?: string 9 | value: string 10 | onChange: (value: string) => void 11 | } 12 | 13 | const MarkdownEditor = ({ 14 | value, 15 | onChange, 16 | }: IMarkdownEditorProps): JSX.Element => { 17 | const options = React.useMemo(() => { 18 | return { 19 | spellChecker: false, 20 | toolbar: [ 21 | "bold", 22 | "italic", 23 | "heading", 24 | "|", 25 | "quote", 26 | "code", 27 | "unordered-list", 28 | "ordered-list", 29 | "|", 30 | "link", 31 | "image", 32 | "|", 33 | "preview", 34 | "|", 35 | "guide", 36 | ], 37 | } 38 | }, []) 39 | 40 | return ( 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | export default MarkdownEditor 48 | -------------------------------------------------------------------------------- /frontend/components/form/MarkdownEditor/markdown-editor.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const StyledMarkdownEditor = styled("div", { 4 | ".EasyMDEContainer:focus-within .CodeMirror, .EasyMDEContainer:focus-within .editor-toolbar": 5 | { 6 | boxShadow: "0 0 0 3px #aad1ff", 7 | outline: "none", 8 | }, 9 | 10 | ".CodeMirror": { 11 | background: "$input", 12 | color: "$fadedBlue700", 13 | border: "2px solid $fadedBlue500", 14 | borderBottomLeftRadius: "6px", 15 | borderBottomRightRadius: "6px", 16 | }, 17 | 18 | ".CodeMirror-cursor": { 19 | borderColor: "$fadedBlue700", 20 | }, 21 | 22 | ".CodeMirror-scroll": { 23 | marginRight: 0, 24 | }, 25 | 26 | ".editor-toolbar": { 27 | borderTop: "2px solid $fadedBlue500", 28 | borderLeft: "2px solid $fadedBlue500", 29 | borderRight: "2px solid $fadedBlue500", 30 | borderTopLeftRadius: "6px", 31 | borderTopRightRadius: "6px", 32 | }, 33 | 34 | ".editor-toolbar i.separator": { 35 | borderRight: "1px solid $fadedBlue300", 36 | borderLeft: "none", 37 | }, 38 | 39 | ".editor-toolbar button": { 40 | color: "$fadedBlue700", 41 | 42 | "&.active, &:hover": { 43 | background: "$input", 44 | border: "none", 45 | }, 46 | }, 47 | 48 | ".editor-toolbar.disabled-for-preview button:not(.no-disable)": { 49 | opacity: 0.4, 50 | }, 51 | 52 | ".editor-preview": { 53 | background: "$input", 54 | }, 55 | 56 | ".editor-statusbar": { 57 | color: "$fadedBlue700", 58 | }, 59 | }) 60 | -------------------------------------------------------------------------------- /frontend/components/form/Radio/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./radio.component" 2 | -------------------------------------------------------------------------------- /frontend/components/form/Radio/radio.component.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames" 2 | import * as S from "./radio.styles" 3 | 4 | interface IRadioProps { 5 | id: string 6 | label: string 7 | prefix?: JSX.Element 8 | value: React.ReactText 9 | checked: boolean 10 | onChange: (event: React.ChangeEvent) => void 11 | inline?: boolean 12 | } 13 | 14 | const Radio: React.FC = ({ 15 | id, 16 | label, 17 | prefix, 18 | value, 19 | checked, 20 | onChange, 21 | inline, 22 | }) => { 23 | return ( 24 | 25 | 32 | 33 | {checked} 34 | 35 | {label && ( 36 | 37 | {prefix && {prefix}} 38 | {label} 39 | 40 | )} 41 | 42 | 43 | ) 44 | } 45 | 46 | Radio.defaultProps = { 47 | inline: false, 48 | } 49 | 50 | export default Radio 51 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildFormPage/build-form-page.d.tsx: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios" 2 | import { ICreateBuildRequest } from "../../../types/models" 3 | 4 | export interface IFormValues { 5 | __operation: "CREATE" | "EDIT" 6 | isBook: boolean | undefined 7 | hash: string 8 | title: string 9 | slug: string 10 | description: string 11 | tags: string[] 12 | cover: { 13 | type: "file" | "hash" 14 | file: File | null 15 | url: string | null 16 | hash: string | null 17 | crop: { 18 | x: number 19 | y: number 20 | width: number 21 | height: number 22 | } | null 23 | } 24 | version?: ICreateBuildRequest["version"] 25 | } 26 | 27 | export interface IValidFormValues { 28 | isBook: boolean 29 | hash: string 30 | title: string 31 | slug: string 32 | description: string 33 | tags: string[] 34 | cover: { 35 | type: "file" | "hash" 36 | file: File | null 37 | url: string | null 38 | hash: string | null 39 | crop: { 40 | x: number 41 | y: number 42 | width: number 43 | height: number 44 | } | null 45 | } 46 | version?: ICreateBuildRequest["version"] 47 | } 48 | 49 | interface ISubmitStatusNeutral { 50 | loading: false 51 | error: false 52 | } 53 | 54 | interface ISubmitStatusLoading { 55 | loading: true 56 | error: false 57 | } 58 | 59 | interface ISubmitStatusError { 60 | loading: false 61 | error: AxiosError 62 | } 63 | 64 | export type ISubmitStatus = 65 | | ISubmitStatusNeutral 66 | | ISubmitStatusLoading 67 | | ISubmitStatusError 68 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildFormPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-form-page.component" 2 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildFormPage/useCanSave.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react" 2 | import getConfig from "next/config" 3 | import useImage from "../../../hooks/useImage" 4 | import { IStep2Props } from "./step-2.component" 5 | 6 | const { publicRuntimeConfig } = getConfig() 7 | 8 | function useCanSave( 9 | payloadData: IStep2Props["payloadData"], 10 | submitStatus: IStep2Props["submitStatus"], 11 | formikProps: IStep2Props["formikProps"] 12 | ): { 13 | canSave: boolean 14 | waitingForRender: boolean 15 | } { 16 | const selectedImageHref = useMemo(() => { 17 | if (payloadData.type === "blueprint") { 18 | return payloadData._links.rendering?.href || formikProps.values.cover.url 19 | } 20 | 21 | return formikProps.values.cover.hash 22 | ? `${publicRuntimeConfig.apiUrl}/images/renderings/${formikProps.values.cover.hash}` 23 | : null 24 | }, [ 25 | payloadData._links.rendering?.href, 26 | formikProps.values.cover.url, 27 | formikProps.values.cover.hash, 28 | ]) 29 | const { loaded: renderIsReady } = useImage(selectedImageHref || undefined) 30 | 31 | const { canSave, waitingForRender } = useMemo(() => { 32 | const waitingForRender = 33 | formikProps.values.cover.type === "hash" && !renderIsReady 34 | 35 | return { 36 | canSave: 37 | !waitingForRender && !submitStatus.loading && formikProps.isValid, 38 | waitingForRender, 39 | } 40 | }, [ 41 | renderIsReady, 42 | submitStatus.loading, 43 | formikProps.isValid, 44 | formikProps.values.cover.type, 45 | ]) 46 | 47 | return { canSave, waitingForRender } 48 | } 49 | 50 | export default useCanSave 51 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildListPage/build-list-page.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useAppSelector } from "../../../redux/store" 3 | import BuildCardList from "../../ui/BuildCardList" 4 | import FilterList from "../../ui/FilterList" 5 | import LayoutSidebar from "../../ui/LayoutSidebar" 6 | import Links from "../../ui/Links" 7 | import Search from "../../ui/Search" 8 | import Stacker from "../../ui/Stacker" 9 | 10 | function BuildListPage(): JSX.Element { 11 | const { search, sort } = useAppSelector((store) => ({ 12 | search: store.search, 13 | sort: store.filters.sort, 14 | })) 15 | 16 | return ( 17 | 20 | 21 | 22 | 23 | 24 | } 25 | > 26 | 32 | 33 | ) 34 | } 35 | 36 | export default BuildListPage 37 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildListPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-list-page.component" 2 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-page.component" 2 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/blueprint-json-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { decodeBlueprint } from "../../../../utils/blueprint" 3 | import * as S from "../build-page.styles" 4 | import { TTabComponent } from "../tabs.component" 5 | import Tab from "./tab.component" 6 | 7 | const BlueprintJsonTab: TTabComponent = (props) => { 8 | const encoded = props.build.latest_version.payload.encoded 9 | const parsed = React.useMemo(() => { 10 | const decoded = decodeBlueprint(props.build.latest_version.payload.encoded) 11 | return { 12 | json: decoded, 13 | stringified: JSON.stringify(decoded, null, 1), 14 | } 15 | }, [encoded]) 16 | 17 | return ( 18 | 19 | e.currentTarget.select()} 23 | /> 24 | 25 | ) 26 | } 27 | 28 | export default BlueprintJsonTab 29 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/blueprint-string-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as S from "../build-page.styles" 3 | import { TTabComponent } from "../tabs.component" 4 | import Tab from "./tab.component" 5 | 6 | const BlueprintStringTab: TTabComponent = (props) => { 7 | return ( 8 | 9 | e.currentTarget.select()} 13 | /> 14 | 15 | ) 16 | } 17 | 18 | export default BlueprintStringTab 19 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/blueprints-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { isBook } from "../../../../utils/build" 3 | import BlueprintItemExplorer from "../../../ui/BlueprintItemExplorer" 4 | import { TTabComponent } from "../tabs.component" 5 | import Tab from "./tab.component" 6 | 7 | const BlueprintsTab: TTabComponent = (props) => { 8 | return ( 9 | 10 | {props.payload.loading && "loading..."} 11 | {props.payload.error && "error?"} 12 | 13 | {props.payload.data && 14 | isBook(props.payload.data) && 15 | isBook(props.build.latest_version.payload) && ( 16 | <> 17 | 18 | {props.payload.data.children} 19 | 20 | 21 | )} 22 | 23 | ) 24 | } 25 | 26 | export default BlueprintsTab 27 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/details-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactMarkdown from "react-markdown" 3 | import * as S from "../build-page.styles" 4 | import { TTabComponent } from "../tabs.component" 5 | import Tab from "./tab.component" 6 | 7 | const DetailsTab: TTabComponent = (props) => { 8 | return ( 9 | 10 |

Description

11 | 12 | {props.build.description ? ( 13 | {props.build.description} 14 | ) : ( 15 | No description provided 16 | )} 17 | 18 |
19 | ) 20 | } 21 | 22 | export default DetailsTab 23 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/image-mobile-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import BuildImage from "../../../ui/BuildImage" 3 | import * as S from "../build-page.styles" 4 | import { TTabComponent } from "../tabs.component" 5 | import Tab from "./tab.component" 6 | 7 | const ImageMobileTab: TTabComponent = (props) => { 8 | return ( 9 | 10 | 11 | {props.build._links.cover ? ( 12 | 13 | ) : ( 14 | "No image" 15 | )} 16 | 17 | 18 | ) 19 | } 20 | 21 | export default ImageMobileTab 22 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/required-items-tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { isBook } from "../../../../utils/build" 3 | import BlueprintRequiredItems from "../../../ui/BlueprintRequiredItems" 4 | import { TTabComponent } from "../tabs.component" 5 | import Tab from "./tab.component" 6 | 7 | const RequiredItemsTab: TTabComponent = (props) => { 8 | return ( 9 | 10 | {!isBook(props.build.latest_version.payload) && ( 11 | 15 | )} 16 | 17 | ) 18 | } 19 | 20 | export default RequiredItemsTab 21 | -------------------------------------------------------------------------------- /frontend/components/pages/BuildPage/tabs/tab.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import isEqual from "lodash/isEqual" 4 | import * as S from "../build-page.styles" 5 | import { ITabComponentProps } from "../tabs.component" 6 | 7 | const Tab: React.FC = (props) => { 8 | return ( 9 | 10 | {props.children} 11 | 12 | ) 13 | } 14 | 15 | export default React.memo(Tab, isEqual) 16 | -------------------------------------------------------------------------------- /frontend/components/pages/UserBuildListPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./user-build-list-page.component" 2 | -------------------------------------------------------------------------------- /frontend/components/pages/UserBuildListPage/user-build-list-page.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { IThinBuild } from "../../../types/models" 3 | import BuildList from "../../ui/BuildList" 4 | import Container from "../../ui/Container" 5 | import LayoutDefault from "../../ui/LayoutDefault" 6 | 7 | export interface IUserBuildListPageProps { 8 | builds: IThinBuild[] 9 | } 10 | 11 | function UserBuildListPage(props: IUserBuildListPageProps): JSX.Element { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default UserBuildListPage 22 | -------------------------------------------------------------------------------- /frontend/components/ui/Avatar/avatar.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import * as S from "./avatar.styles" 4 | 5 | interface IAvatarProps { 6 | username: string 7 | size: "medium" | "large" 8 | } 9 | 10 | function Avatar({ username, size }: IAvatarProps): JSX.Element { 11 | return ( 12 | 18 | {username[0]} 19 | 20 | ) 21 | } 22 | 23 | export default Avatar 24 | -------------------------------------------------------------------------------- /frontend/components/ui/Avatar/avatar.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const AvatarWrapper = styled("div", getTypo(ETypo.BODY), { 6 | display: "flex", 7 | justifyContent: "center", 8 | alignItems: "center", 9 | width: "22px", 10 | height: "22px", 11 | background: "linear-gradient(153.43deg, #37d291 0%, #225594 106.06%)", 12 | 13 | "&.size-medium": { 14 | width: "22px", 15 | height: "22px", 16 | fontSize: "13px", 17 | borderRadius: "5px", 18 | }, 19 | 20 | "&.size-large": { 21 | width: "28px", 22 | height: "28px", 23 | fontSize: "15px", 24 | borderRadius: "5px", 25 | fontWeight: 700, 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /frontend/components/ui/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./avatar.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BlueprintItem/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./blueprint-item.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BlueprintItemExplorer/blueprint-item-explorer.provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react" 2 | import { IFullPayload } from "../../../types/models" 3 | 4 | interface IBlueprintItemExplorerContextSelectable { 5 | type: "selectable" 6 | onSelect: (e: React.MouseEvent, hash: IFullPayload["hash"]) => void 7 | selectedHash: IFullPayload["hash"] | null 8 | } 9 | 10 | interface IBlueprintItemExplorerContextReadonly { 11 | type: "readOnly" 12 | } 13 | 14 | export type IBlueprintItemExplorerContext = 15 | | IBlueprintItemExplorerContextSelectable 16 | | IBlueprintItemExplorerContextReadonly 17 | 18 | const BlueprintItemExplorerContext = 19 | React.createContext(null) 20 | 21 | export const useBlueprintItemExplorer = () => { 22 | const context = useContext(BlueprintItemExplorerContext) 23 | 24 | if (!context) { 25 | throw "Missing BlueprintItemExplorerContext" 26 | } 27 | 28 | return context 29 | } 30 | 31 | export default BlueprintItemExplorerContext 32 | -------------------------------------------------------------------------------- /frontend/components/ui/BlueprintItemExplorer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./blueprint-item-explorer.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BlueprintRequiredItems/blueprint-required-items.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import ItemIcon from "../ItemIcon" 5 | import Stacker from "../Stacker" 6 | 7 | export const WithRequiredItem = styled(Stacker, getTypo(ETypo.METADATA), { 8 | fontSize: "16px", 9 | alignItems: "center", 10 | }) 11 | 12 | export const IconImg = styled(ItemIcon, { 13 | width: "20px", 14 | marginRight: "4px", 15 | }) 16 | -------------------------------------------------------------------------------- /frontend/components/ui/BlueprintRequiredItems/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./blueprint-required-items.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-card.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildCardList/build-card-list.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const BuildCardListWrapper = styled("div") 4 | 5 | export const Header = styled("div", { 6 | background: "#333642", 7 | margin: "0 -20px 20px", 8 | padding: "20px", 9 | display: "flex", 10 | justifyContent: "space-between", 11 | alignItems: "center", 12 | }) 13 | 14 | export const Columns = styled("div", { 15 | display: "flex", 16 | flexWrap: "wrap", 17 | alignContent: "flex-start", 18 | gap: "var(--gutter)", 19 | }) 20 | 21 | export const Column = styled("div", { 22 | "--width": `calc(100% / var(--cols) - (var(--gutter) * (var(--cols) - 1) / var(--cols)))`, 23 | flex: "0 0 var(--width)", 24 | width: "var(--width)", 25 | }) 26 | 27 | export const Item = styled("div", { 28 | "& + &": { 29 | marginTop: "var(--gutter)", 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildCardList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-card-list.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-header.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildIcon/build-icon.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import { IIcon } from "../../../types/models" 4 | import ItemIcon from "../ItemIcon" 5 | import * as S from "./build-icon.styles" 6 | 7 | interface IBuildIconProps { 8 | icons: IIcon[] 9 | size?: "medium" | "large" 10 | } 11 | 12 | function BuildIcon({ icons, size = "medium" }: IBuildIconProps): JSX.Element { 13 | return ( 14 | 1, 18 | "size-medium": size === "medium", 19 | "size-large": size === "large", 20 | })} 21 | > 22 | {icons.map((icon, index) => ( 23 | 24 | ))} 25 | 26 | ) 27 | } 28 | 29 | export default BuildIcon 30 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildIcon/build-icon.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const BuildIconWrapper = styled("div", { 4 | boxSizing: "border-box", 5 | display: "flex", 6 | flexWrap: "wrap", 7 | justifyContent: "flex-start", 8 | alignItems: "flex-start", 9 | background: "linear-gradient(135deg, #235581, #133756)", 10 | border: "2px dashed #7fbace", 11 | 12 | img: { 13 | margin: "5% !important", 14 | }, 15 | 16 | "&.large-icons img": { 17 | width: "80%", 18 | height: "80%", 19 | }, 20 | 21 | "&.medium-icons img": { 22 | width: "40%", 23 | height: "40%", 24 | }, 25 | 26 | "&.large-icons": { 27 | justifyContent: "center", 28 | alignItems: "center", 29 | }, 30 | 31 | "&.size-medium": { 32 | width: "40px", 33 | height: "40px", 34 | flex: "0 0 40px", 35 | }, 36 | 37 | "&.size-large": { 38 | width: "60px", 39 | height: "60px", 40 | flex: "0 0 60px", 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-icon.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildImage/build-image.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const OuterWrapper = styled("div", { 4 | position: "relative", 5 | width: "100%", 6 | height: 0, 7 | paddingBottom: "calc((1 / var(--ratio)) * 100%)", 8 | }) 9 | 10 | export const InnerWrapper = styled("div", { 11 | position: "absolute", 12 | top: 0, 13 | right: 0, 14 | bottom: 0, 15 | left: 0, 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildImage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-image.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildList/build-list.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import Stacker from "../Stacker" 5 | 6 | export const BuildListWrapper = styled("div", { 7 | margin: "20px 0", 8 | width: "100%", 9 | }) 10 | 11 | export const Title = styled(Stacker, getTypo(ETypo.PAGE_HEADER), { 12 | alignItems: "center", 13 | fontWeight: 400, 14 | }) 15 | 16 | export const Subtitle = styled("div", getTypo(ETypo.PAGE_SUBTITLE), { 17 | fontWeight: 400, 18 | }) 19 | 20 | export const Sort = styled("button", { 21 | cursor: "pointer", 22 | background: "transparent", 23 | border: "none", 24 | color: "#fff", 25 | }) 26 | 27 | export const Table = styled("table", { 28 | width: "100%", 29 | textAlign: "left", 30 | borderSpacing: 0, 31 | 32 | "thead th": { 33 | paddingBottom: "10px !important", 34 | }, 35 | 36 | "thead th svg": { 37 | marginRight: "4px", 38 | }, 39 | 40 | "tr th, tr td": { 41 | borderBottom: "1px solid $fadedBlue300", 42 | }, 43 | 44 | "th, td": { 45 | padding: "8px", 46 | }, 47 | 48 | "tr:nth-child(odd) td": { 49 | background: "$sub", 50 | }, 51 | 52 | img: { 53 | display: "block", 54 | }, 55 | 56 | a: { 57 | color: "#fff", 58 | fontWeight: 700, 59 | textDecoration: "none", 60 | }, 61 | 62 | "a:hover": { 63 | borderBottom: "2px solid #fff", 64 | }, 65 | }) 66 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-list.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildListLookupStats/build-list-lookup-stats.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as S from "./build-list-lookup-stats.styles" 3 | 4 | interface IBuildListLookupStatProps { 5 | count: number 6 | totalCount: number 7 | } 8 | 9 | const BuildListLookupStats: React.FC = ({ 10 | count, 11 | totalCount, 12 | }) => { 13 | return ( 14 | 15 | 16 | Displaying {count} out of {totalCount} builds 17 | 18 | 19 | ) 20 | } 21 | 22 | export default BuildListLookupStats 23 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildListLookupStats/build-list-lookup-stats.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const BuildListLookupStatWrapper = styled("div", getTypo(ETypo.BODY), { 6 | fontSize: "16px", 7 | }) 8 | 9 | export const Count = styled("div", { 10 | color: "$fadedBlue700", 11 | }) 12 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildListLookupStats/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-list-lookup-stats.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildListSort/build-list-sort.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | 5 | export const BuildListSortWrapper = styled("div", getTypo(ETypo.BODY), { 6 | fontSize: "16px", 7 | display: "flex", 8 | color: "$fadedBlue700", 9 | textAlign: "right", 10 | }) 11 | 12 | export const SortDropdownWapper = styled("div", { 13 | position: "relative", 14 | marginLeft: "4px", 15 | }) 16 | 17 | export const DropdownTrigger = styled("div", { 18 | fontWeight: 700, 19 | cursor: "pointer", 20 | }) 21 | 22 | export const DropdownMenu = styled("div", { 23 | display: "flex", 24 | flexDirection: "column", 25 | overflow: "hidden", 26 | background: "#333642", 27 | boxShadow: "0 2px 5px rgba(0, 0, 0, 0.25)", 28 | position: "absolute", 29 | top: "100%", 30 | left: "-8px", 31 | right: "-8px", 32 | zIndex: 1, 33 | marginTop: "5px", 34 | borderRadius: "4px", 35 | minWidth: "fit-content", 36 | }) 37 | 38 | export const DropdownItem = styled("button", { 39 | background: "transparent", 40 | border: 0, 41 | color: "$fadedBlue900", 42 | whiteSpace: "nowrap", 43 | padding: "4px 10px", 44 | cursor: "pointer", 45 | textAlign: "left", 46 | minWidth: "fit-content", 47 | boxSizing: "border-box", 48 | 49 | "&:hover, &.is-selected": { 50 | background: "rgba(0, 0, 0, 0.2)", 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /frontend/components/ui/BuildListSort/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./build-list-sort.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Button/button.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import * as S from "./button.styles" 4 | 5 | export interface IButtonProps extends React.ComponentPropsWithoutRef<"button"> { 6 | as?: keyof JSX.IntrinsicElements 7 | variant?: "success" | "alt" | "cta" | "default" 8 | size?: "medium" | "small" 9 | counter?: React.ReactText 10 | } 11 | 12 | const Button: React.FC = ({ 13 | as = "button", 14 | children, 15 | variant, 16 | size, 17 | counter, 18 | ...restProps 19 | }) => { 20 | return ( 21 | 26 | {children} 27 | {counter !== undefined && {counter}} 28 | 29 | ) 30 | } 31 | 32 | Button.defaultProps = { 33 | variant: "default", 34 | size: "medium", 35 | } 36 | 37 | export default Button 38 | -------------------------------------------------------------------------------- /frontend/components/ui/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./button.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/ButtonClipboard/button-clipboard.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useCallback } from "react" 3 | import Copy from "../../../icons/copy" 4 | import Button from "../../ui/Button" 5 | import { IButtonProps } from "../Button/button.component" 6 | 7 | interface ICopyToClipboardProps extends IButtonProps { 8 | toCopy: string 9 | } 10 | 11 | const CopyToClipboard = ({ 12 | toCopy, 13 | children, 14 | ...restProps 15 | }: React.PropsWithChildren): JSX.Element => { 16 | const copyToClipboard = useCallback(() => { 17 | navigator.clipboard.writeText(toCopy) 18 | }, [toCopy]) 19 | 20 | return ( 21 | 24 | ) 25 | } 26 | 27 | const MemoizedCopyToClipboard = 28 | React.memo(CopyToClipboard) 29 | 30 | export const CopyStringToClipboard = ({ 31 | toCopy, 32 | ...restProps 33 | }: ICopyToClipboardProps): JSX.Element => { 34 | return ( 35 | 36 | Copy to clipboard 37 | 38 | ) 39 | } 40 | 41 | export const CopyJsonToClipboard = ({ 42 | toCopy, 43 | ...restProps 44 | }: ICopyToClipboardProps): JSX.Element => { 45 | return ( 46 | 47 | Copy JSON to clipboard 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /frontend/components/ui/Container/container.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cx from "classnames" 3 | import * as S from "./container.styles" 4 | 5 | interface IContainerProps { 6 | children: React.ReactNode 7 | direction?: "row" | "column" 8 | size?: "small" | "medium" | "large" 9 | } 10 | 11 | function Container({ 12 | children, 13 | direction = "row", 14 | size = "large", 15 | }: IContainerProps): JSX.Element { 16 | return ( 17 | 26 | {children} 27 | 28 | ) 29 | } 30 | 31 | export default Container 32 | -------------------------------------------------------------------------------- /frontend/components/ui/Container/container.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const ContainerWrapper = styled("div", { 4 | width: "100%", 5 | maxWidth: "calc(100% - 20px * 2)", 6 | margin: "0 auto", 7 | display: "flex", 8 | padding: "0 20px", 9 | flex: 1, 10 | 11 | "&.dir-row": { 12 | flexDirection: "row", 13 | }, 14 | 15 | "&.dir-column": { 16 | flexDirection: "column", 17 | }, 18 | 19 | "&.size-small": { 20 | width: "calc(768px - 20px * 2)", 21 | }, 22 | 23 | "&.size-medium": { 24 | width: "calc(1366px - 20px * 2)", 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /frontend/components/ui/Container/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./container.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Dropdown/dropdown.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import Stacker from "../Stacker" 5 | 6 | export const MenuTrigger = styled("button", getTypo(ETypo.BUTTON), { 7 | display: "flex", 8 | alignItems: "center", 9 | background: "linear-gradient(180deg, #333742, #272b33)", 10 | color: "$fadedBlue900", 11 | height: "38px", 12 | padding: "0 11px 0 9px", 13 | borderRadius: "5px", 14 | border: "none", 15 | cursor: "pointer", 16 | 17 | "&:hover": { 18 | background: "linear-gradient(180deg, #2f323a, #23262b)", 19 | }, 20 | 21 | "svg path": { 22 | fill: "$fadedBlue500", 23 | }, 24 | }) 25 | 26 | export const InnerMenuTrigger = styled(Stacker, { 27 | display: "flex", 28 | alignItems: "center", 29 | }) 30 | 31 | export const StyledMenuButton = styled("div", { 32 | position: "relative", 33 | display: "inline-block", 34 | }) 35 | 36 | export const StyledMenuPopup = styled("ul", { 37 | boxSizing: "border-box", 38 | listStyle: "none", 39 | minWidth: "100%", 40 | position: "absolute", 41 | right: 0, 42 | borderRadius: "5px", 43 | margin: "4px 0 0 0", 44 | border: "1px solid #454854", 45 | padding: "6px 0", 46 | background: "$header", 47 | overflow: "hidden", 48 | }) 49 | 50 | export const StyledMenuItem = styled("li", { 51 | color: "$fadedBlue900", 52 | padding: "6px 9px", 53 | outline: "none", 54 | cursor: "pointer", 55 | textAlign: "right", 56 | 57 | "&:hover, &.is-focused": { 58 | background: "linear-gradient(180deg, #333742, #272b33)", 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /frontend/components/ui/Dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./dropdown.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/FavoriteButton/favorite-button.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react" 2 | import cx from "classnames" 3 | import { useApi } from "../../../hooks/useApi" 4 | import { useAppSelector } from "../../../redux/store" 5 | import { IFullBuild } from "../../../types/models" 6 | import { IButtonProps } from "../Button/button.component" 7 | import * as S from "./favorite-button.styles" 8 | 9 | interface IFavoriteButtonProps extends IButtonProps { 10 | build: IFullBuild 11 | } 12 | 13 | const FavoriteButton: React.FC = ({ 14 | build, 15 | ...restProps 16 | }) => { 17 | const links = build._links 18 | 19 | const authUser = useAppSelector((state) => state.auth?.user) 20 | const [{ loading, error }, execute] = useApi( 21 | { url: links.followers.href }, 22 | { manual: true } 23 | ) 24 | 25 | const [count, setCount] = useState(build._links.followers.count) 26 | const [isFavorite, setIsFavorite] = useState(Boolean(links.remove_favorite)) 27 | 28 | const toggle = useCallback(() => { 29 | execute({ method: isFavorite ? "DELETE" : "PUT" }).then(() => { 30 | setIsFavorite((prevFavorite) => !prevFavorite) 31 | execute().then((res) => { 32 | setCount(res.data.count) 33 | }) 34 | }) 35 | }, [isFavorite]) 36 | 37 | return ( 38 | 47 | {isFavorite ? "Unfavorite" : "Favorite"} 48 | {loading && "..."} 49 | 50 | ) 51 | } 52 | 53 | export default FavoriteButton 54 | -------------------------------------------------------------------------------- /frontend/components/ui/FavoriteButton/favorite-button.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import Button from "../Button" 5 | 6 | export const FavoriteButtonWrapper = styled(Button, getTypo(ETypo.BUTTON), { 7 | pointerEvents: "none", 8 | 9 | "&.is-error": { 10 | border: "1px solid red", 11 | }, 12 | 13 | "&.is-clickable": { 14 | pointerEvents: "auto", 15 | cursor: "pointer", 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /frontend/components/ui/FavoriteButton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./favorite-button.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Filter/filter.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import upperFirst from "lodash/upperFirst" 3 | import { ITag } from "../../../redux/reducers/filters" 4 | import { searchBuildsAsync } from "../../../redux/reducers/search" 5 | import { useAppDispatch } from "../../../redux/store" 6 | import Checkbox from "../../form/Checkbox" 7 | 8 | interface IFilterProps { 9 | group: ITag["group"] 10 | icon?: JSX.Element 11 | text: string 12 | isSelected: boolean 13 | name: ITag["name"] 14 | } 15 | 16 | function Filter(props: IFilterProps): JSX.Element { 17 | const dispatch = useAppDispatch() 18 | 19 | function toggleChecked(): void { 20 | dispatch({ 21 | type: "TOGGLE_FILTER", 22 | payload: { 23 | group: props.group, 24 | name: props.name, 25 | }, 26 | }) 27 | dispatch(searchBuildsAsync()) 28 | } 29 | 30 | return ( 31 | 40 | ) 41 | } 42 | 43 | export default Filter 44 | -------------------------------------------------------------------------------- /frontend/components/ui/Filter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./filter.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/FilterList/filter-list.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import tags from "../../../tags.json" 3 | import Stacker from "../Stacker" 4 | import FilterGroup from "./filter-group.component" 5 | import * as S from "./filter-list.styles" 6 | 7 | // TODO: typesafe the tags JSON 8 | 9 | function FilterList(): JSX.Element { 10 | return ( 11 | 12 | Filter builds 13 | 14 | {Object.keys(tags).map((tagCategory) => { 15 | const tagGroup = tags[tagCategory as keyof typeof tags] 16 | 17 | return ( 18 | 26 | ) 27 | })} 28 | 29 | 30 | ) 31 | } 32 | 33 | export default FilterList 34 | -------------------------------------------------------------------------------- /frontend/components/ui/FilterList/filter-list.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import Caret from "../../../icons/caret" 5 | import Stacker from "../Stacker" 6 | 7 | export const FilterListWrapper = styled("div") 8 | 9 | export const Title = styled("div", getTypo(ETypo.FORM_LABEL), { 10 | marginBottom: "8px", 11 | }) 12 | 13 | export const FilterGroup = styled("div", { 14 | padding: "4px 20px 4px 0", 15 | marginRight: "-20px", 16 | 17 | "& + &": { 18 | borderTop: "1px solid $fadedBlue300", 19 | }, 20 | }) 21 | 22 | export const GroupName = styled("button", { 23 | display: "flex", 24 | alignItems: "center", 25 | background: "none", 26 | fontSize: "14px", 27 | border: "none", 28 | padding: "4px 0", 29 | height: "20px", 30 | color: "$fadedBlue900", 31 | fontWeight: 700, 32 | width: "100%", 33 | cursor: "pointer", 34 | 35 | "svg path": { 36 | fill: "#82d2a5", 37 | }, 38 | }) 39 | 40 | export const GroupCount = styled("div", { 41 | display: "flex", 42 | justifyContent: "center", 43 | alignItems: "center", 44 | background: "linear-gradient(90deg, #2b4564 0%, #333642 100%)", 45 | width: "20px", 46 | height: "20px", 47 | borderRadius: "6px", 48 | fontSize: "10px", 49 | fontWeight: 700, 50 | marginLeft: "8px", 51 | }) 52 | 53 | export const GroupFilters = styled(Stacker, { 54 | margin: "8px 0", 55 | }) 56 | 57 | export const StyledCaret = styled(Caret, { 58 | marginLeft: "auto", 59 | }) 60 | -------------------------------------------------------------------------------- /frontend/components/ui/FilterList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./filter-list.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./header.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/ImageUpload/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./image-upload.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/ItemIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./item-icon.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/ItemIcon/item-icon.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import getConfig from "next/config" 3 | import { IIcon } from "../../../types/models" 4 | import * as S from "./item-icon.styles" 5 | 6 | const { publicRuntimeConfig } = getConfig() 7 | 8 | interface IItemIconProps { 9 | name: IIcon["name"] 10 | type: IIcon["type"] 11 | } 12 | 13 | const ItemIcon: React.FC = (props) => { 14 | return ( 15 | 20 | ) 21 | } 22 | 23 | export default ItemIcon 24 | -------------------------------------------------------------------------------- /frontend/components/ui/ItemIcon/item-icon.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const ItemIconWrapper = styled("img") 4 | -------------------------------------------------------------------------------- /frontend/components/ui/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./layout.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Layout/layout.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect } from "react" 2 | import { useInView } from "react-intersection-observer" 3 | import "intersection-observer" 4 | import Head from "next/head" 5 | import { HEADER_HEIGHT } from "../../../design/tokens/layout" 6 | import { useAppDispatch } from "../../../redux/store" 7 | import Header from "../Header" 8 | 9 | interface ILayoutProps { 10 | title?: string 11 | children?: ReactNode 12 | } 13 | 14 | const Layout: React.FC = ({ children, title }) => { 15 | const dispatch = useAppDispatch() 16 | const { ref, entry } = useInView({ 17 | threshold: Array.from({ length: HEADER_HEIGHT }).map((_, i) => { 18 | return i / HEADER_HEIGHT 19 | }), 20 | }) 21 | 22 | useEffect(() => { 23 | if (entry) { 24 | dispatch({ 25 | type: "SET_HEADER", 26 | payload: entry.intersectionRect.height, 27 | }) 28 | } 29 | }, [entry]) 30 | 31 | return ( 32 | <> 33 | 34 | {["Factorio Builds", title].filter(Boolean).join(" | ")} 35 | 36 | 37 | 38 |
39 | {children} 40 | 41 | ) 42 | } 43 | 44 | export default Layout 45 | -------------------------------------------------------------------------------- /frontend/components/ui/LayoutDefault/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./layout-default.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/LayoutDefault/layout-default.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react" 2 | import Container from "../Container" 3 | import Layout from "../Layout" 4 | import Links from "../Links" 5 | import * as S from "./layout-default.styles" 6 | 7 | interface ILayoutProps { 8 | children?: ReactNode 9 | title?: string 10 | } 11 | 12 | const LayoutDefault: React.FC = ({ children, title }) => { 13 | return ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default LayoutDefault 31 | -------------------------------------------------------------------------------- /frontend/components/ui/LayoutDefault/layout-default.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | import Stacker from "../Stacker" 3 | 4 | export const ContentWrapper = styled("div", { 5 | display: "flex", 6 | flexDirection: "column", 7 | flex: "1 0 auto", 8 | }) 9 | 10 | export const BodyWrapper = styled(Stacker, { 11 | width: "100%", 12 | flex: "1 0 auto", 13 | }) 14 | 15 | export const Content = styled("main", { 16 | flex: "1 1 auto", 17 | display: "flex", 18 | flexDirection: "column", 19 | maxWidth: "100%", 20 | 21 | "> :first-child": { 22 | marginTop: 0, 23 | }, 24 | 25 | "> :last-child": { 26 | marginBottom: 0, 27 | }, 28 | }) 29 | 30 | export const Footer = styled("footer", { 31 | marginTop: "20px", 32 | borderTop: "1px solid $fadedBlue300", 33 | padding: "16px 0", 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/components/ui/LayoutSidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./layout-sidebar.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/LayoutSidebar/layout-sidebar.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | import Stacker from "../Stacker" 3 | 4 | export const BodyWrapper = styled(Stacker, { 5 | width: "100%", 6 | flex: "1 0 auto", 7 | }) 8 | 9 | export const ContentWrapper = styled("div", { 10 | display: "flex", 11 | flexDirection: "column", 12 | flex: "1 0 auto", 13 | }) 14 | 15 | export const Content = styled("main", { 16 | flex: "1 1 auto", 17 | display: "flex", 18 | flexDirection: "column", 19 | maxWidth: "100%", 20 | marginBottom: "20px", 21 | 22 | "> :first-child": { 23 | marginTop: 0, 24 | }, 25 | 26 | "> :last-child": { 27 | marginBottom: 0, 28 | }, 29 | }) 30 | 31 | export const Backdrop = styled("div", { 32 | zIndex: 1, 33 | background: "rgba(0, 0, 0, 0.7)", 34 | position: "absolute", 35 | top: 0, 36 | left: 0, 37 | right: 0, 38 | bottom: 0, 39 | }) 40 | -------------------------------------------------------------------------------- /frontend/components/ui/Links/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./links.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Links/links.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Link from "next/link" 3 | import GitHub from "../../../icons/github" 4 | import { Line } from "../../../icons/line" 5 | import * as S from "./links.styles" 6 | 7 | interface ILinksProps { 8 | orientation: "horizontal" | "vertical" 9 | } 10 | 11 | const Links = (props: ILinksProps): JSX.Element => { 12 | return ( 13 | 14 | 18 | 19 | GitHub 20 | 21 | 22 | 23 | 24 | {props.orientation === "vertical" && } 25 | About 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default Links 33 | -------------------------------------------------------------------------------- /frontend/components/ui/Links/links.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | import Stacker from "../Stacker" 3 | 4 | export const LinksWrapper = styled(Stacker, { 5 | alignItems: "flex-start", 6 | }) 7 | 8 | export const StyledLink = styled("a", { 9 | cursor: "pointer", 10 | display: "flex", 11 | alignItems: "center", 12 | color: "$fadedBlue700", 13 | fontWeight: 700, 14 | textDecoration: "none", 15 | 16 | "&:hover": { 17 | textDecoration: "underline", 18 | }, 19 | 20 | svg: { 21 | marginRight: "8px", 22 | width: "16px", 23 | height: "16px", 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /frontend/components/ui/RichText/__tests__/__snapshots__/rich-text.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` parses a title with item icon 1`] = ` 4 |
5 | 8 | 9 | 16 | 17 | build name 18 | 19 | 20 |
21 | `; 22 | 23 | exports[` parses a title with multiple item icons 1`] = ` 24 |
25 | 28 | 29 | 36 | 37 | build name 38 | 39 | 46 | 47 |
48 | `; 49 | -------------------------------------------------------------------------------- /frontend/components/ui/RichText/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./rich-text.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/RichText/rich-text.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ItemIcon from "../ItemIcon" 3 | import * as S from "./rich-text.styles" 4 | import useParseRichText, { IParsedRichTextNode } from "./useParseRichText.hook" 5 | 6 | interface IRichText { 7 | input: string 8 | prefix?: JSX.Element 9 | } 10 | 11 | interface IRichTextNodeProps { 12 | node: IParsedRichTextNode 13 | index: number 14 | } 15 | 16 | function RichTextNode({ node, index }: IRichTextNodeProps): JSX.Element { 17 | if (node.type === "text") { 18 | return {node.value} 19 | } 20 | 21 | if (node.type === "item") { 22 | return 23 | } 24 | 25 | return ( 26 | 27 | {node.children.map((child, i) => { 28 | return 29 | })} 30 | 31 | ) 32 | } 33 | 34 | function RichText(props: IRichText): JSX.Element { 35 | const parts = useParseRichText(props.input) 36 | 37 | const formatted = React.useMemo(() => { 38 | if (!props.input) { 39 | return "[unnamed]" 40 | } 41 | 42 | return parts.map((part, index) => { 43 | return 44 | }) 45 | }, [props.input]) 46 | 47 | return ( 48 | 49 | {props.prefix} {formatted} 50 | 51 | ) 52 | } 53 | 54 | export default RichText 55 | -------------------------------------------------------------------------------- /frontend/components/ui/RichText/rich-text.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const RichTextWrapper = styled("span", { 4 | display: "inline-block", 5 | 6 | "&::after": { 7 | content: "", 8 | clear: "both", 9 | display: "table", 10 | }, 11 | 12 | "> :first-child": { 13 | marginLeft: 0, 14 | }, 15 | 16 | "> :last-child": { 17 | marginRight: 0, 18 | }, 19 | 20 | img: { 21 | float: "left", 22 | width: "auto", 23 | height: "1em", 24 | margin: "0 0.25em", 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /frontend/components/ui/Search/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./search.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Search/search.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { useDebouncedEffect } from "../../../hooks/useDebouncedEffect" 3 | import { searchBuildsAsync } from "../../../redux/reducers/search" 4 | import { useAppDispatch, useAppSelector } from "../../../redux/store" 5 | import Input from "../../form/Input" 6 | import * as S from "./search.styles" 7 | 8 | const Search = (): JSX.Element => { 9 | const dispatch = useAppDispatch() 10 | const query = useAppSelector((state) => state.filters.query) 11 | const [input, setInput] = useState(query) 12 | 13 | useDebouncedEffect( 14 | () => { 15 | dispatch({ type: "SET_QUERY", payload: input }) 16 | dispatch(searchBuildsAsync()) 17 | }, 18 | 250, 19 | [input] 20 | ) 21 | 22 | function handleOnChange(event: React.ChangeEvent): void { 23 | setInput(event.target.value) 24 | } 25 | 26 | return ( 27 | } 33 | onChange={handleOnChange} 34 | /> 35 | ) 36 | } 37 | 38 | export default Search 39 | -------------------------------------------------------------------------------- /frontend/components/ui/Search/search.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | import SearchIcon from "../../../icons/search-icon" 3 | 4 | export const StyledSearchIcon = styled(SearchIcon, { 5 | fill: "$fadedBlue500", 6 | width: "18px", 7 | flex: "0 0 18px", 8 | marginRight: "8px", 9 | height: "auto", 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/components/ui/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./sidebar.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Sidebar/sidebar.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { HEADER_HEIGHT } from "../../../design/tokens/layout" 3 | import { useAppSelector } from "../../../redux/store" 4 | import * as S from "./sidebar.styles" 5 | 6 | interface ISidebarProps { 7 | children: React.ReactNode 8 | } 9 | 10 | function Sidebar(props: ISidebarProps): JSX.Element { 11 | const headerHeight = useAppSelector((state) => 12 | state.layout.header.init ? state.layout.header.height : HEADER_HEIGHT 13 | ) 14 | 15 | return ( 16 | 20 | {props.children} 21 | 22 | 23 | ) 24 | } 25 | 26 | export default Sidebar 27 | -------------------------------------------------------------------------------- /frontend/components/ui/Sidebar/sidebar.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const SidebarWrapper = styled("aside", { 4 | width: "300px", 5 | flex: "0 0 300px", 6 | padding: "20px 20px 20px 0", 7 | background: "$sidebar", 8 | borderRight: "1px solid $fadedBlue300", 9 | height: "calc(100vh - var(--headerHeight) - 40px)", 10 | position: "sticky", 11 | top: 0, 12 | bottom: 0, 13 | overflowY: "scroll", 14 | 15 | "&::-webkit-scrollbar-track": { 16 | borderRadius: "10px", 17 | backgroundColor: "$sidebar", 18 | }, 19 | 20 | "&::-webkit-scrollbar": { 21 | width: "12px", 22 | backgroundColor: "$sidebar", 23 | }, 24 | 25 | "&::-webkit-scrollbar-thumb": { 26 | borderRadius: "10px", 27 | boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", 28 | backgroundColor: "$fadedBlue300", 29 | border: "3px solid $sidebar", 30 | }, 31 | 32 | "@media screen and (max-width: 767px)": { 33 | position: "absolute", 34 | top: "$headerHeight", 35 | left: 0, 36 | maxWidth: "400px", 37 | right: "40px", 38 | padding: "20px", 39 | width: "auto", 40 | zIndex: 5, 41 | height: "calc(100vh - $headerHeight - 40px)", 42 | overflowY: "scroll", 43 | }, 44 | }) 45 | 46 | export const SidebarContent = styled("div", { 47 | position: "relative", 48 | zIndex: 1, 49 | }) 50 | 51 | export const SidebarBG = styled("div", { 52 | position: "absolute", 53 | background: "$sidebar", 54 | top: 0, 55 | bottom: 0, 56 | right: 0, 57 | width: "100vw", 58 | }) 59 | -------------------------------------------------------------------------------- /frontend/components/ui/Spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./spinner.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Spinner/spinner.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as S from "./spinner.styles" 3 | 4 | const Spinner: React.FC = () => ( 5 |
6 | 7 |
8 | ) 9 | 10 | export default Spinner 11 | -------------------------------------------------------------------------------- /frontend/components/ui/Spinner/spinner.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled, keyframes } from "../../../design/stitches.config" 2 | 3 | const loading = keyframes({ 4 | "0%, 80%, 100%": { 5 | boxShadow: "0 2.5em 0 -1.3em", 6 | "40%": { 7 | boxShadow: "0 2.5em 0 0", 8 | }, 9 | }, 10 | }) 11 | 12 | export const SpinnerWrapper = styled("div", { 13 | "&": { 14 | color: "#fff", 15 | fontSize: "3px", 16 | position: "relative", 17 | transform: "translateZ(0)", 18 | animationDelay: "-0.16s", 19 | margin: "0 3.5em", 20 | }, 21 | 22 | "&, &:before, &:after": { 23 | borderRadius: "50%", 24 | width: "2.5em", 25 | height: "2.5em", 26 | animationFillMode: "both", 27 | animation: `${loading} 1.8s infinite ease-in-out`, 28 | }, 29 | 30 | "&:before, &:after": { 31 | content: "", 32 | position: "absolute", 33 | top: 0, 34 | }, 35 | 36 | "&:before": { 37 | left: "-3.5em", 38 | animationDelay: "-0.32s", 39 | }, 40 | 41 | "&:after": { 42 | left: "3.5em", 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /frontend/components/ui/Stacker/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./stacker.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Stacker/stacker.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as S from "./stacker.styles" 3 | 4 | interface IStackerProps extends React.ComponentPropsWithoutRef<"div"> { 5 | children: React.ReactNode 6 | gutter?: number 7 | orientation?: "horizontal" | "vertical" 8 | } 9 | 10 | function Stacker({ 11 | children, 12 | gutter = 16, 13 | orientation = "vertical", 14 | ...restProps 15 | }: IStackerProps): JSX.Element { 16 | return ( 17 | 25 | {children} 26 | 27 | ) 28 | } 29 | 30 | export default Stacker 31 | -------------------------------------------------------------------------------- /frontend/components/ui/Stacker/stacker.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const StackerWrapper = styled("div", { 4 | display: "flex", 5 | gap: "var(--gutter)", 6 | 7 | variants: { 8 | orientation: { 9 | horizontal: { 10 | flexDirection: "row", 11 | }, 12 | 13 | vertical: { 14 | flexDirection: "column", 15 | }, 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /frontend/components/ui/Subheader/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./subheader.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Subheader/subheader.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Container from "../Container" 3 | import Stacker from "../Stacker" 4 | import * as S from "./subheader.styles" 5 | 6 | interface ISubheader { 7 | title: JSX.Element | string 8 | subtitle?: JSX.Element 9 | } 10 | 11 | function Subheader(props: ISubheader): JSX.Element { 12 | return ( 13 | 14 | 15 | 16 | {props.title} 17 | {props.subtitle} 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default Subheader 25 | -------------------------------------------------------------------------------- /frontend/components/ui/Subheader/subheader.styles.tsx: -------------------------------------------------------------------------------- 1 | import { getTypo } from "../../../design/helpers/typo" 2 | import { styled } from "../../../design/stitches.config" 3 | import { ETypo } from "../../../design/tokens/typo" 4 | import Container from "../Container" 5 | 6 | export const HeaderWrapper = styled("div", { 7 | display: "flex", 8 | alignItems: "center", 9 | background: "$subHeader", 10 | color: "$fadedBlue900", 11 | padding: "25px 20px", 12 | 13 | [`${Container}`]: { 14 | flexDirection: "column", 15 | justifyContent: "center", 16 | }, 17 | }) 18 | 19 | export const Title = styled("h1", getTypo(ETypo.PAGE_HEADER), { 20 | display: "flex", 21 | alignItems: "center", 22 | color: "#fff", 23 | margin: 0, 24 | 25 | img: { 26 | width: "32px", 27 | height: "auto", 28 | marginRight: "8px", 29 | }, 30 | }) 31 | 32 | export const Subtitle = styled("div", getTypo(ETypo.PAGE_SUBTITLE), { 33 | color: "#a392b5", 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/components/ui/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./tooltip.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/Tooltip/tooltip.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as S from "./tooltip.styles" 3 | 4 | interface ITooltipProps { 5 | children: React.ReactNode 6 | content: React.ReactNode 7 | } 8 | 9 | const Tooltip: React.FC = ({ children, content }) => { 10 | return ( 11 | 12 | {children} 13 | {content} 14 | 15 | ) 16 | } 17 | 18 | export default Tooltip 19 | -------------------------------------------------------------------------------- /frontend/components/ui/Tooltip/tooltip.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const TooltipWrapper = styled("div", { 4 | display: "inline-flex", 5 | position: "relative", 6 | cursor: "help", 7 | borderBottom: "1px dashed rgba(255, 255, 255, 0.3)", 8 | whiteSpace: "nowrap", 9 | }) 10 | 11 | export const TooltipContent = styled("div", { 12 | opacity: 0, 13 | position: "absolute", 14 | left: 0, 15 | top: "100%", 16 | marginTop: "-10px", 17 | transition: "all 0.15s", 18 | pointerEvents: "none", 19 | background: "rgba(0, 0, 0, 0.7)", 20 | backdropFilter: "blur(7px)", 21 | padding: "7px", 22 | borderRadius: "5px", 23 | width: "max-content", 24 | maxWidth: "400px", 25 | 26 | [`${TooltipWrapper}:hover &`]: { 27 | opacity: 1, 28 | marginTop: "5px", 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/components/ui/UserDropdown/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./user-dropdown.component" 2 | -------------------------------------------------------------------------------- /frontend/components/ui/UserDropdown/user-dropdown.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react" 2 | import { useRouter } from "next/router" 3 | import { Media } from "../../../design/styles/media" 4 | import { useAppDispatch } from "../../../redux/store" 5 | import { IStoreUser } from "../../../types/models" 6 | import { logout } from "../../../utils/auth" 7 | import Avatar from "../Avatar" 8 | import Dropdown from "../Dropdown" 9 | import * as S from "./user-dropdown.styles" 10 | 11 | interface IUserDropdownProps { 12 | user: IStoreUser 13 | } 14 | 15 | function UserDropdown(props: IUserDropdownProps): JSX.Element { 16 | const router = useRouter() 17 | const dispatch = useAppDispatch() 18 | 19 | const links = useMemo( 20 | () => [ 21 | { 22 | action: () => { 23 | router.push(`/${props.user.username}/builds`) 24 | }, 25 | body: my builds, 26 | }, 27 | { 28 | action: () => { 29 | logout(dispatch) 30 | router.push("/api/auth/logout") 31 | }, 32 | body: log off, 33 | }, 34 | ], 35 | [props.user.username] 36 | ) 37 | 38 | return ( 39 | 40 | 41 | 42 | {(mcx, renderChildren) => { 43 | return renderChildren ? ( 44 | {props.user.username} 45 | ) : null 46 | }} 47 | 48 | 49 | ) 50 | } 51 | 52 | export default UserDropdown 53 | -------------------------------------------------------------------------------- /frontend/components/ui/UserDropdown/user-dropdown.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "../../../design/stitches.config" 2 | 3 | export const InnerLink = styled("div", { 4 | display: "inline-block", 5 | cursor: "pointer", 6 | color: "$fadedBlue900", 7 | textDecoration: "none", 8 | }) 9 | 10 | export const InnerLinkLogOff = styled(InnerLink, { 11 | color: "$danger", 12 | }) 13 | -------------------------------------------------------------------------------- /frontend/design/helpers/typo.ts: -------------------------------------------------------------------------------- 1 | import { CssComponent } from "@stitches/react/types/styled-component" 2 | import { css } from "../stitches.config" 3 | import { EFontScale, ETypo, TYPO } from "../tokens/typo" 4 | 5 | function typoMapper(typo: ETypo): EFontScale { 6 | switch (typo) { 7 | case ETypo.PAGE_HEADER: 8 | return EFontScale.F1 9 | case ETypo.FORM_LABEL: 10 | case ETypo.BODY_TITLE: 11 | case ETypo.CARD_TITLE: 12 | return EFontScale.F2B 13 | case ETypo.BODY: 14 | case ETypo.FORM_INPUT: 15 | return EFontScale.F2 16 | case ETypo.BUTTON: 17 | case ETypo.HEADER: 18 | return EFontScale.F3 19 | case ETypo.PAGE_SUBTITLE: 20 | return EFontScale.F4 21 | case ETypo.METADATA_TITLE: 22 | return EFontScale.F5B 23 | case ETypo.METADATA: 24 | return EFontScale.F5 25 | } 26 | } 27 | 28 | function getStyles(fontScale: EFontScale) { 29 | const styles = TYPO[fontScale] 30 | 31 | return css({ 32 | fontSize: styles.SIZE, 33 | lineHeight: styles.LINE_HEIGHT, 34 | fontWeight: styles.WEIGHT, 35 | fontFamily: styles.FAMILY, 36 | }) 37 | } 38 | 39 | export function getTypo(typo: ETypo): CssComponent { 40 | const mappedTypo = typoMapper(typo) 41 | return getStyles(mappedTypo) 42 | } 43 | -------------------------------------------------------------------------------- /frontend/design/styles/media.ts: -------------------------------------------------------------------------------- 1 | import { createMedia } from "@artsy/fresnel" 2 | 3 | const AppMedia = createMedia({ 4 | breakpoints: { 5 | xs: 0, 6 | sm: 768, 7 | md: 1024, 8 | lg: 1400, 9 | }, 10 | }) 11 | 12 | // Make styles for injection into the header of the page 13 | export const mediaStyles = AppMedia.createMediaStyle() 14 | 15 | export const { Media, MediaContextProvider } = AppMedia 16 | -------------------------------------------------------------------------------- /frontend/design/tokens/color.ts: -------------------------------------------------------------------------------- 1 | export const COLOR = { 2 | FADEDBLUE900: "#EDEFF2", 3 | FADEDBLUE700: "#D0D3DC", 4 | FADEDBLUE500: "#888DA2", 5 | FADEDBLUE300: "#4F566E", 6 | FADEDBLUE100: "#242732", 7 | 8 | BLUE900: "#E5F1FF", 9 | BLUE700: "#AAD1FF", 10 | BLUE500: "#4299FF", 11 | BLUE300: "#003D85", 12 | BLUE100: "#001833", 13 | 14 | SELECTED: "#4299FF", 15 | FOCUSED: "#BDD8F7", 16 | 17 | SUCCESS: "#8FCD5B", 18 | DANGER: "#F24439", 19 | 20 | HEADER: "#111216", 21 | SUBHEADER: "#111216", 22 | SIDEBAR: "#282A33", 23 | BACKGROUND: "#282A33", 24 | LINK: "#28D8FF", 25 | SUB: "#1a1c23", 26 | CODE: "#1a1c23", 27 | INPUT: "#1a1c23", 28 | CARD: "#111216", 29 | } 30 | -------------------------------------------------------------------------------- /frontend/design/tokens/layout.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_HEIGHT = 74 2 | -------------------------------------------------------------------------------- /frontend/design/tokens/typo.ts: -------------------------------------------------------------------------------- 1 | export enum EFontScale { 2 | F1 = "F1", 3 | F2B = "F2B", 4 | F2 = "F2", 5 | F3 = "F3", 6 | F4 = "F4", 7 | F5B = "F5B", 8 | F5 = "F5", 9 | } 10 | 11 | export enum ETypo { 12 | HEADER = "HEADER", 13 | PAGE_HEADER = "PAGE_HEADER", 14 | PAGE_SUBTITLE = "PAGE_SUBTITLE", 15 | BODY = "BODY", 16 | BODY_TITLE = "BODY_TITLE", 17 | FORM_LABEL = "FORM_LABEL", 18 | FORM_INPUT = "FORM_INPUT", 19 | CARD_TITLE = "CARD_TITLE", 20 | BUTTON = "BUTTON", 21 | METADATA = "METADATA", 22 | METADATA_TITLE = "METADATA_TITLE", 23 | } 24 | 25 | export const FONT_FAMILY = { 26 | HEADING: "DM Sans, sans-serif", 27 | BODY: "DM Sans, sans-serif", 28 | MONO: "JetBrains Mono, monospace", 29 | } 30 | 31 | export const TYPO = { 32 | [EFontScale.F1]: { 33 | SIZE: "24px", 34 | LINE_HEIGHT: 1.3, 35 | WEIGHT: 700, 36 | FAMILY: FONT_FAMILY.HEADING, 37 | }, 38 | [EFontScale.F2B]: { 39 | SIZE: "18px", 40 | LINE_HEIGHT: 1.3, 41 | WEIGHT: 700, 42 | FAMILY: FONT_FAMILY.HEADING, 43 | }, 44 | [EFontScale.F2]: { 45 | SIZE: "18px", 46 | LINE_HEIGHT: 1.3, 47 | WEIGHT: 400, 48 | FAMILY: FONT_FAMILY.BODY, 49 | }, 50 | [EFontScale.F3]: { 51 | SIZE: "17px", 52 | LINE_HEIGHT: 1.3, 53 | WEIGHT: 400, 54 | FAMILY: FONT_FAMILY.BODY, 55 | }, 56 | [EFontScale.F4]: { 57 | SIZE: "16px", 58 | LINE_HEIGHT: 1.3, 59 | WEIGHT: 700, 60 | FAMILY: FONT_FAMILY.HEADING, 61 | }, 62 | [EFontScale.F5B]: { 63 | SIZE: "13px", 64 | LINE_HEIGHT: 1.3, 65 | WEIGHT: 700, 66 | FAMILY: FONT_FAMILY.BODY, 67 | }, 68 | [EFontScale.F5]: { 69 | SIZE: "13px", 70 | LINE_HEIGHT: 1.3, 71 | WEIGHT: 400, 72 | FAMILY: FONT_FAMILY.BODY, 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /frontend/hooks/useApi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosRequestHeaders } from "axios" 2 | import useAxios, { Options } from "axios-hooks" 3 | import getConfig from "next/config" 4 | import { useAppSelector } from "../redux/store" 5 | import { IProblemDetails } from "../types/models" 6 | 7 | const { publicRuntimeConfig } = getConfig() 8 | 9 | const isManual = (config: AxiosRequestConfig, options?: Options) => { 10 | if (options && options.manual) { 11 | return options.manual 12 | } 13 | 14 | if (!config.method || config.method.toUpperCase() === "GET") { 15 | return false 16 | } 17 | 18 | return true 19 | } 20 | 21 | export function useApi(config: AxiosRequestConfig, options?: Options) { 22 | const accessToken = useAppSelector((store) => store.auth?.user?.accessToken) 23 | 24 | const headers: AxiosRequestHeaders = { 25 | ...config.headers, 26 | "content-type": config?.headers?.["content-type"] || "application/json", 27 | } 28 | 29 | if (accessToken) { 30 | headers.Authorization = `Bearer ${accessToken}` 31 | } 32 | 33 | return useAxios( 34 | { 35 | ...config, 36 | baseURL: publicRuntimeConfig.apiUrl, 37 | headers, 38 | }, 39 | { 40 | manual: isManual(config, options), 41 | } 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /frontend/hooks/useDebouncedEffect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useDebouncedEffect( 4 | effect: () => void, 5 | delay: number, 6 | deps: React.DependencyList 7 | ): void { 8 | const callback = React.useCallback(effect, deps) 9 | 10 | React.useEffect(() => { 11 | const handler = setTimeout(() => { 12 | callback() 13 | }, delay) 14 | 15 | return () => { 16 | clearTimeout(handler) 17 | } 18 | }, [callback, delay]) 19 | } 20 | -------------------------------------------------------------------------------- /frontend/hooks/useImage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | 3 | const useImage = (src?: string) => { 4 | const [loaded, setLoaded] = useState(false) 5 | const [error, setError] = useState(false) 6 | const [fetching, setFetching] = useState(false) 7 | 8 | useEffect(() => { 9 | if (!src) { 10 | return 11 | } 12 | 13 | setFetching(true) 14 | setLoaded(false) 15 | setError(false) 16 | 17 | const image = new Image() 18 | image.src = src 19 | 20 | const handleError = () => { 21 | setError(true) 22 | setFetching(false) 23 | } 24 | 25 | const handleLoad = () => { 26 | setLoaded(true) 27 | setError(false) 28 | setFetching(false) 29 | } 30 | 31 | image.onerror = handleError 32 | image.onload = handleLoad 33 | 34 | return () => { 35 | image.removeEventListener("error", handleError) 36 | image.removeEventListener("load", handleLoad) 37 | } 38 | }, [src]) 39 | 40 | return { loaded, error, fetching } 41 | } 42 | 43 | export default useImage 44 | -------------------------------------------------------------------------------- /frontend/icons/burger.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface IBurgerProps extends React.SVGProps { 5 | color?: string 6 | } 7 | 8 | const Burger = ({ 9 | color = COLOR.FADEDBLUE700, 10 | ...restProps 11 | }: IBurgerProps): JSX.Element => { 12 | return ( 13 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default Burger 26 | -------------------------------------------------------------------------------- /frontend/icons/caret.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface ICaretProps extends React.SVGProps { 5 | inverted?: boolean 6 | color?: string 7 | } 8 | 9 | const Caret = ({ 10 | inverted, 11 | color = COLOR.LINK, 12 | ...restProps 13 | }: ICaretProps): JSX.Element => { 14 | return ( 15 | 24 | 28 | 29 | ) 30 | } 31 | 32 | export default Caret 33 | -------------------------------------------------------------------------------- /frontend/icons/copy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const Copy = (props: React.SVGProps): JSX.Element => { 4 | return ( 5 | 12 | 16 | 17 | ) 18 | } 19 | 20 | export default Copy 21 | -------------------------------------------------------------------------------- /frontend/icons/editor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface IEditorProps extends React.SVGProps { 5 | color?: string 6 | } 7 | 8 | const Editor = ({ 9 | color = COLOR.FADEDBLUE700, 10 | ...restProps 11 | }: IEditorProps): JSX.Element => { 12 | return ( 13 | 20 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Editor 31 | -------------------------------------------------------------------------------- /frontend/icons/github.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface IGithubProps extends React.SVGProps { 5 | color?: string 6 | } 7 | 8 | const GitHub = ({ 9 | color = COLOR.FADEDBLUE700, 10 | ...restProps 11 | }: IGithubProps): JSX.Element => { 12 | const uid = React.useId() 13 | const id = `github-${uid}` 14 | 15 | return ( 16 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | export default GitHub 41 | -------------------------------------------------------------------------------- /frontend/icons/line.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface ILineProps extends React.SVGProps { 5 | color?: string 6 | } 7 | 8 | export const Line = ({ 9 | color = COLOR.FADEDBLUE900, 10 | ...restProps 11 | }: ILineProps): JSX.Element => ( 12 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /frontend/icons/plus.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | interface IPlusProps extends React.SVGProps { 4 | color?: string 5 | } 6 | 7 | const Plus = ({ color = "#000", ...restProps }: IPlusProps): JSX.Element => ( 8 | 9 | 16 | 22 | 23 | ) 24 | 25 | export default Plus 26 | -------------------------------------------------------------------------------- /frontend/icons/raw.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { COLOR } from "../design/tokens/color" 3 | 4 | interface IRawProps extends React.SVGProps { 5 | color?: string 6 | } 7 | 8 | const Raw = ({ 9 | color = COLOR.FADEDBLUE700, 10 | ...restProps 11 | }: IRawProps): JSX.Element => { 12 | const uid = React.useId() 13 | const id = `raw-${uid}` 14 | 15 | return ( 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default Raw 37 | -------------------------------------------------------------------------------- /frontend/icons/search-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const Search = (props: React.SVGProps): JSX.Element => { 4 | return ( 5 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default Search 16 | -------------------------------------------------------------------------------- /frontend/icons/thumbs-up.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const ThumbsUp = (props: React.SVGProps): JSX.Element => { 4 | return ( 5 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default ThumbsUp 17 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | module.exports = { 4 | setupFiles: ["/jest.setup.js"], 5 | testRegex: "/__tests__/.*.test.tsx?$", 6 | testEnvironment: "jsdom", 7 | } 8 | -------------------------------------------------------------------------------- /frontend/jest.setup.js: -------------------------------------------------------------------------------- 1 | import { setConfig } from "next/config" 2 | import { publicRuntimeConfig } from "./next.config" 3 | 4 | setConfig({ publicRuntimeConfig }) 5 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | module.exports = { 3 | serverRuntimeConfig: { 4 | clientId: process.env.CLIENT_ID || "frontend", 5 | clientSecret: process.env.CLIENT_SECRET || "511536EF-F270-4058-80CA-1C89C192F69A", 6 | }, 7 | publicRuntimeConfig: { 8 | webUrl: process.env.WEB_URL || "http://localhost:3000", 9 | apiUrl: process.env.API_URL || "https://api.local.factorio.tech", 10 | identityUrl: process.env.IDENTITY_URL || "https://identity.local.factorio.tech", 11 | cdnUrl: process.env.CDN_URL || "https://api.local.factorio.tech/assets", 12 | enableApplicationInsights: process.env.ENABLE_APPLICATION_INSIGHTS || false, 13 | instrumentationKey: process.env.INSTRUMENTATION_KEY, 14 | }, 15 | images: { 16 | domains: [ 17 | new URL(process.env.API_URL || "https://api.local.factorio.tech").hostname, 18 | new URL(process.env.CDN_URL || "https://api.local.factorio.tech/assets").hostname, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/pages/[user]/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, NextPage } from "next" 2 | import { WithRouterProps } from "next/dist/client/with-router" 3 | import { withRouter } from "next/router" 4 | import BuildPage from "../../components/pages/BuildPage" 5 | import LayoutDefault from "../../components/ui/LayoutDefault" 6 | import { IFullBuild } from "../../types/models" 7 | import { axios } from "../../utils/axios" 8 | 9 | interface IBuildsPageProps extends WithRouterProps { 10 | build?: IFullBuild 11 | errors?: string 12 | } 13 | 14 | const BuildsPage: NextPage = ({ build, errors, router }) => { 15 | if (errors || !build) { 16 | return ( 17 | 18 |

19 | Error: {errors} 20 |

21 |
22 | ) 23 | } 24 | 25 | return 26 | } 27 | 28 | export default withRouter(BuildsPage) 29 | 30 | export const getServerSideProps: GetServerSideProps = async ({ params }) => { 31 | const { user, slug } = params! 32 | 33 | try { 34 | const build = await axios 35 | .get(`/builds/${user}/${slug}`) 36 | .then((response) => response.data) 37 | .catch((err) => { 38 | console.error(err) 39 | }) 40 | 41 | if (!build) throw new Error("Build not found") 42 | 43 | return { 44 | props: { build: JSON.parse(JSON.stringify(build)) }, 45 | } 46 | } catch (err: any) { 47 | return { props: { errors: err.message } } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/pages/[user]/[slug]/edit.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, NextPage } from "next" 2 | import BuildFormPage from "../../../components/pages/BuildFormPage" 3 | import LayoutDefault from "../../../components/ui/LayoutDefault" 4 | import { IFullBuild } from "../../../types/models" 5 | import { axios } from "../../../utils/axios" 6 | 7 | interface IBuildsEditPageProps { 8 | build?: IFullBuild 9 | errors?: string 10 | } 11 | 12 | const BuildsEditPage: NextPage = ({ build, errors }) => { 13 | if (errors || !build) { 14 | return ( 15 | 16 |

17 | Error: {errors} 18 |

19 |
20 | ) 21 | } 22 | 23 | return 24 | } 25 | 26 | export default BuildsEditPage 27 | 28 | export const getServerSideProps: GetServerSideProps = async ({ params }) => { 29 | const { user, slug } = params! 30 | 31 | try { 32 | const build = await axios 33 | .get(`/builds/${user}/${slug}`) 34 | .then((response) => response.data) 35 | .catch((err) => { 36 | console.error(err) 37 | }) 38 | 39 | if (!build) throw new Error("Build not found") 40 | 41 | return { 42 | props: { build: JSON.parse(JSON.stringify(build)) }, 43 | } 44 | } catch (err: any) { 45 | return { props: { errors: err.message } } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/pages/[user]/builds.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, NextPage } from "next" 2 | import UserBuildListPage from "../../components/pages/UserBuildListPage" 3 | import { IUserBuildListPageProps } from "../../components/pages/UserBuildListPage/user-build-list-page.component" 4 | import { wrapper } from "../../redux/store" 5 | import { axios } from "../../utils/axios" 6 | 7 | const UserBuildsPage: NextPage = (props) => { 8 | return 9 | } 10 | 11 | export const getServerSideProps: GetServerSideProps = 12 | wrapper.getServerSideProps(() => async (ctx) => { 13 | const { user } = ctx.params! 14 | 15 | if (!user) { 16 | return { props: {} } 17 | } 18 | 19 | const data = await axios 20 | .get(`/users/${user}/builds`) 21 | .then((response) => response.data) 22 | .catch((err) => { 23 | console.error(err) 24 | }) 25 | 26 | const deserializedData = JSON.parse(JSON.stringify(data)) 27 | 28 | return { 29 | props: deserializedData, 30 | } 31 | }) 32 | 33 | export default UserBuildsPage 34 | -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document from "next/document" 2 | import { Html, Head, Main, NextScript } from "next/document" 3 | import { mediaStyles } from "../design/styles/media" 4 | 5 | export default class MyDocument extends Document { 6 | render(): JSX.Element { 7 | return ( 8 | 9 | 10 | 15 | 20 | 27 | 33 |