diff --git a/api-schema.graphql b/api-schema.graphql index d4daccc..45cdf61 100644 --- a/api-schema.graphql +++ b/api-schema.graphql @@ -415,6 +415,7 @@ type Query { appConfig: AppConfig! currencies: [Currency!]! me: User + metadataAll(account: String!): JSON solanaGetBalance(account: String!): String solanaGetTokenAccounts(account: String!): JSON solanaGetTransactions(account: String!): JSON diff --git a/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts b/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts new file mode 100644 index 0000000..38cf4a3 --- /dev/null +++ b/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts @@ -0,0 +1,41 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common' +import { ApiCoreService } from '@tokengator/api-core-data-access' +import * as fs from 'node:fs/promises' +import { join } from 'node:path' +import { CantFindTheRightTypeScrewItHackathonMode } from './api-metadata.service' + +@Injectable() +export class ApiMetadataImageService implements OnModuleInit { + private readonly logger = new Logger(ApiMetadataImageService.name) + private readonly assetPath = join(__dirname, 'assets') + private readonly imagePath = join(this.assetPath, 'images') + private readonly fontPath = join(this.assetPath, 'fonts') + private readonly templatePath = join(this.imagePath, 'templates') + + private readonly templateMap = new Map() + + constructor(private readonly core: ApiCoreService) {} + + async onModuleInit(): Promise { + await this.listTemplates() + } + + async listTemplates() { + const files = await fs.readdir(this.templatePath) + this.logger.verbose(`Found ${files.length} templates in ${this.templatePath}`) + + for (const file of files) { + const buffer = await fs.readFile(join(this.templatePath, file)) + this.templateMap.set(file, buffer) + this.logger.verbose(` -> Loaded template: ${file}`) + } + } + + generate(accountMetadata: CantFindTheRightTypeScrewItHackathonMode) { + const rand = Math.floor(Math.random() * this.templateMap.size) + const template = Array.from(this.templateMap.values())[rand] + // Do something with the template + + return template + } +} diff --git a/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts b/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts index 4553f7a..040a159 100644 --- a/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts +++ b/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common' import { ApiCoreDataAccessModule } from '@tokengator/api-core-data-access' import { ApiSolanaDataAccessModule } from '@tokengator/api-solana-data-access' +import { ApiMetadataImageService } from './api-metadata-image.service' import { ApiMetadataService } from './api-metadata.service' @Module({ imports: [ApiCoreDataAccessModule, ApiSolanaDataAccessModule], - providers: [ApiMetadataService], + providers: [ApiMetadataService, ApiMetadataImageService], exports: [ApiMetadataService], }) export class ApiMetadataDataAccessModule {} diff --git a/libs/api/metadata/data-access/src/lib/api-metadata.service.ts b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts index 0fd9e88..b4eeb12 100644 --- a/libs/api/metadata/data-access/src/lib/api-metadata.service.ts +++ b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts @@ -3,6 +3,7 @@ import { PublicKey } from '@solana/web3.js' import { ApiCoreService } from '@tokengator/api-core-data-access' import { ApiSolanaService, SolanaAccountInfo } from '@tokengator/api-solana-data-access' import { LRUCache } from 'lru-cache' +import { ApiMetadataImageService } from './api-metadata-image.service' export interface CantFindTheRightTypeScrewItHackathonMode { extension: 'tokenMetadata' @@ -18,6 +19,7 @@ export interface CantFindTheRightTypeScrewItHackathonMode { export interface ExternalMetadata { image: string + description: string [key: string]: string } @@ -35,7 +37,6 @@ const TEN_MINUTES = 1000 * 60 * 10 @Injectable() export class ApiMetadataService { private readonly logger = new Logger(ApiMetadataService.name) - private readonly externalMetadataCache = new LRUCache({ max: 1000, ttl: ONE_HOUR, @@ -83,8 +84,7 @@ export class ApiMetadataService { fetchMethod: async (account: string) => { const image = `${this.core.config.apiUrl}/metadata/image/${account}` const metadata = await this.accountMetadataCache.fetch(account) - - const { name, symbol, description, external_url } = defaults() + const external_url = `${this.core.config.apiUrl}/metadata/redirect/${account}` if (metadata) { // The metadata is external, fetch it and return @@ -95,62 +95,80 @@ export class ApiMetadataService { if (!externalMetadata) { throw new Error(`Failed to fetch external metadata for ${metadata.state.uri}`) } + if (externalMetadata.image) { return { - name: metadata.state.name ?? name, - symbol: metadata.state.symbol ?? symbol, - description: metadata.state.uri ?? description, - external_url: metadata.state.uri ?? external_url, + name: metadata.state.name, + symbol: metadata.state.symbol, image: externalMetadata.image, + description: externalMetadata.description, + external_url, } } } // The metadata is on the account, return it return { - name: metadata.state.name ?? name, - symbol: metadata.state.symbol ?? symbol, - description: metadata.state.uri ?? description, - external_url: metadata.state.uri ?? external_url, + name: metadata.state.name, + symbol: metadata.state.symbol, + description: metadata.state.name, image, + external_url, + attributes: metadata.state.additionalMetadata?.map(([trait_type, value]) => ({ + trait_type, + value, + })), } } - - return { - name, - symbol, - description, - external_url, - image, - } + throw new Error(`Failed to fetch metadata for ${account}`) }, }) - constructor(private readonly core: ApiCoreService, private readonly solana: ApiSolanaService) {} + constructor( + private readonly core: ApiCoreService, + private readonly image: ApiMetadataImageService, + private readonly solana: ApiSolanaService, + ) {} - async getImage(account: string): Promise { + async getImage(account: string): Promise { + this.logger.verbose(`Fetching metadata image for ${account}`) const accountMetadata = await this.accountMetadataCache.fetch(account) if (!accountMetadata) { throw new Error(`Failed to fetch metadata for ${account}`) } - const externalMetadata = await this.externalMetadataCache.fetch(accountMetadata.state.uri) - if (!externalMetadata) { - throw new Error(`Failed to fetch external metadata for ${accountMetadata.state.uri}`) + + if (!accountMetadata.state.uri?.startsWith(this.core.config.apiUrl)) { + const externalMetadata = await this.externalMetadataCache.fetch(accountMetadata.state.uri) + if (!externalMetadata) { + throw new Error(`Failed to fetch external metadata for ${accountMetadata.state.uri}`) + } + + return externalMetadata.image } - return externalMetadata.image + return this.image.generate(accountMetadata) } async getJson(account: string) { + this.logger.verbose(`Fetching metadata json for ${account}`) return this.jsonCache.fetch(account) } -} -function defaults() { - const name = `Unknown Name` - const symbol = `Unknown Symbol` - const description = `Unknown Description` - const external_url = `https://example.com` + async getRedirect(account: string) { + return `${this.core.config.webUrl}/assets/${account}` + } - return { name, symbol, description, external_url } + async getAll(account: string) { + const [json, accountMetadata, accountInfo] = await Promise.all([ + this.getJson(account), + this.accountMetadataCache.fetch(account), + this.accountCache.fetch(account), + ]) + + return { + json, + accountMetadata, + accountInfo, + } + } } diff --git a/libs/api/metadata/feature/src/lib/api-metadata.controller.ts b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts index 13a6021..0106e2a 100644 --- a/libs/api/metadata/feature/src/lib/api-metadata.controller.ts +++ b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts @@ -8,13 +8,29 @@ export class ApiMetadataController { @Get('image/:account') async image(@Param('account') account: string, @Res() res: Response) { - const imageUrl = await this.service.getImage(account) + const result = await this.service.getImage(account) - return res.redirect(imageUrl) + if (!result) { + return res.status(404).send('Not found') + } + + if (typeof result === 'string') { + return res.redirect(result) + } + + res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': result.length }) + res.end(result) } @Get('json/:account') async json(@Param('account') account: string) { return this.service.getJson(account) } + + @Get('redirect/:account') + async redirect(@Param('account') account: string, @Res() res: Response) { + const url = await this.service.getRedirect(account) + + res.redirect(url) + } } diff --git a/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts b/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts index 0de019a..6137475 100644 --- a/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts +++ b/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common' import { ApiMetadataDataAccessModule } from '@tokengator/api-metadata-data-access' import { ApiMetadataController } from './api-metadata.controller' +import { ApiMetadataResolver } from './api-metadata.resolver' @Module({ controllers: [ApiMetadataController], imports: [ApiMetadataDataAccessModule], + providers: [ApiMetadataResolver], }) export class ApiMetadataFeatureModule {} diff --git a/libs/api/metadata/feature/src/lib/api-metadata.resolver.ts b/libs/api/metadata/feature/src/lib/api-metadata.resolver.ts new file mode 100644 index 0000000..6001327 --- /dev/null +++ b/libs/api/metadata/feature/src/lib/api-metadata.resolver.ts @@ -0,0 +1,16 @@ +import { UseGuards } from '@nestjs/common' +import { Args, Query, Resolver } from '@nestjs/graphql' +import { ApiAuthGraphQLUserGuard } from '@tokengator/api-auth-data-access' +import { ApiMetadataService } from '@tokengator/api-metadata-data-access' +import { GraphQLJSON } from 'graphql-scalars' + +@Resolver() +@UseGuards(ApiAuthGraphQLUserGuard) +export class ApiMetadataResolver { + constructor(private readonly service: ApiMetadataService) {} + + @Query(() => GraphQLJSON, { nullable: true }) + async metadataAll(@Args('account') account: string) { + return this.service.getAll(account) + } +} diff --git a/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts b/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts index e947861..e0e59b6 100644 --- a/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts +++ b/libs/api/preset/data-access/src/lib/api-preset-minter.service.ts @@ -36,6 +36,10 @@ export class ApiPresetMinterService { this.logger.debug(`Program ID: ${this.programId.toString()}`) } + getMetadataUrl(account: string) { + return `${this.core.config.apiUrl}/metadata/json/${account}` + } + getProgramTokenMinter(provider: AnchorProvider = this.solana.getAnchorProvider()) { return new Program(TokengatorMinterIDL, this.programId, provider) } @@ -66,7 +70,7 @@ export class ApiPresetMinterService { applicationConfig: { paymentConfig: appPaymentConfig }, metadataConfig, }, - } = getPresetConfig({ communitySlug, mintPublicKey: mintKeypair.publicKey.toString(), preset }) + } = getPresetConfig({ communitySlug, url: this.getMetadataUrl(mintKeypair.publicKey.toString()), preset }) // BELOW HERE WILL MOVE TO THE SDK AT SOME POINT const [minter] = getMinterPda({ name, mint: mintKeypair.publicKey, programId: this.programId }) @@ -153,7 +157,7 @@ export class ApiPresetMinterService { const [manager] = getWNSManagerPda(WEN_NEW_STANDARD_PROGRAM_ID) const { name, symbol, uri } = { - uri: `https://devnet.tokengator.app/api/metadata/json/${memberMintKeypair.publicKey.toString()}`, + uri: this.getMetadataUrl(memberMintKeypair.publicKey.toString()), name: 'test', symbol: 'HI', } @@ -272,15 +276,7 @@ export class ApiPresetMinterService { } } -function getPresetConfig({ - communitySlug, - mintPublicKey, - preset, -}: { - communitySlug: string - mintPublicKey: string - preset: Preset -}) { +function getPresetConfig({ communitySlug, url, preset }: { communitySlug: string; url: string; preset: Preset }) { return { name: preset.name, description: preset.description ?? '', @@ -292,7 +288,7 @@ function getPresetConfig({ }), minterConfig: { metadataConfig: { - uri: `https://devnet.tokengator.app/api/metadata/json/${mintPublicKey}.json`, + uri: url, name: preset.name, symbol: 'TGC', metadata: [ diff --git a/libs/sdk/src/generated/graphql-sdk.ts b/libs/sdk/src/generated/graphql-sdk.ts index c705586..ad989cc 100644 --- a/libs/sdk/src/generated/graphql-sdk.ts +++ b/libs/sdk/src/generated/graphql-sdk.ts @@ -640,6 +640,7 @@ export type Query = { appConfig: AppConfig currencies: Array me?: Maybe + metadataAll?: Maybe solanaGetBalance?: Maybe solanaGetTokenAccounts?: Maybe solanaGetTransactions?: Maybe @@ -739,6 +740,10 @@ export type QueryAnonRequestIdentityChallengeArgs = { input: RequestIdentityChallengeInput } +export type QueryMetadataAllArgs = { + account: Scalars['String']['input'] +} + export type QuerySolanaGetBalanceArgs = { account: Scalars['String']['input'] } @@ -2942,6 +2947,12 @@ export type AnonVerifyIdentityChallengeMutation = { } | null } +export type MetadataAllQueryVariables = Exact<{ + account: Scalars['String']['input'] +}> + +export type MetadataAllQuery = { __typename?: 'Query'; item?: any | null } + export type PresetDetailsFragment = { __typename?: 'Preset' createdAt?: Date | null @@ -4632,6 +4643,11 @@ export const AnonVerifyIdentityChallengeDocument = gql` } ${IdentityChallengeDetailsFragmentDoc} ` +export const MetadataAllDocument = gql` + query metadataAll($account: String!) { + item: metadataAll(account: $account) + } +` export const AdminFindManyPresetDocument = gql` query adminFindManyPreset($input: PresetAdminFindManyInput!) { paging: adminFindManyPreset(input: $input) { @@ -5031,6 +5047,7 @@ const UserVerifyIdentityChallengeDocumentString = print(UserVerifyIdentityChalle const UserLinkIdentityDocumentString = print(UserLinkIdentityDocument) const AnonRequestIdentityChallengeDocumentString = print(AnonRequestIdentityChallengeDocument) const AnonVerifyIdentityChallengeDocumentString = print(AnonVerifyIdentityChallengeDocument) +const MetadataAllDocumentString = print(MetadataAllDocument) const AdminFindManyPresetDocumentString = print(AdminFindManyPresetDocument) const AdminFindOnePresetDocumentString = print(AdminFindOnePresetDocument) const AdminCreatePresetDocumentString = print(AdminCreatePresetDocument) @@ -6086,6 +6103,27 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = variables, ) }, + metadataAll( + variables: MetadataAllQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: MetadataAllQuery + errors?: GraphQLError[] + extensions?: any + headers: Headers + status: number + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(MetadataAllDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'metadataAll', + 'query', + variables, + ) + }, adminFindManyPreset( variables: AdminFindManyPresetQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, diff --git a/libs/sdk/src/graphql/feature-metadata.graphql b/libs/sdk/src/graphql/feature-metadata.graphql new file mode 100644 index 0000000..bc349ca --- /dev/null +++ b/libs/sdk/src/graphql/feature-metadata.graphql @@ -0,0 +1,3 @@ +query metadataAll($account: String!) { + item: metadataAll(account: $account) +} diff --git a/libs/web/asset/data-access/.babelrc b/libs/web/asset/data-access/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/web/asset/data-access/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/web/asset/data-access/.eslintrc.json b/libs/web/asset/data-access/.eslintrc.json new file mode 100644 index 0000000..772a43d --- /dev/null +++ b/libs/web/asset/data-access/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/web/asset/data-access/README.md b/libs/web/asset/data-access/README.md new file mode 100644 index 0000000..c110467 --- /dev/null +++ b/libs/web/asset/data-access/README.md @@ -0,0 +1,7 @@ +# web-asset-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-asset-data-access` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/web/asset/data-access/project.json b/libs/web/asset/data-access/project.json new file mode 100644 index 0000000..1622c81 --- /dev/null +++ b/libs/web/asset/data-access/project.json @@ -0,0 +1,13 @@ +{ + "name": "web-asset-data-access", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/web/asset/data-access/src", + "projectType": "library", + "tags": ["app:web", "type:data-access"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/libs/web/asset/data-access/src/index.ts b/libs/web/asset/data-access/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/web/asset/data-access/tsconfig.json b/libs/web/asset/data-access/tsconfig.json new file mode 100644 index 0000000..d8c59fe --- /dev/null +++ b/libs/web/asset/data-access/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../tsconfig.base.json" +} diff --git a/libs/web/asset/data-access/tsconfig.lib.json b/libs/web/asset/data-access/tsconfig.lib.json new file mode 100644 index 0000000..45b2297 --- /dev/null +++ b/libs/web/asset/data-access/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/web/asset/feature/.babelrc b/libs/web/asset/feature/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/web/asset/feature/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/web/asset/feature/.eslintrc.json b/libs/web/asset/feature/.eslintrc.json new file mode 100644 index 0000000..772a43d --- /dev/null +++ b/libs/web/asset/feature/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/web/asset/feature/README.md b/libs/web/asset/feature/README.md new file mode 100644 index 0000000..8666de5 --- /dev/null +++ b/libs/web/asset/feature/README.md @@ -0,0 +1,7 @@ +# web-asset-feature + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-asset-feature` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/web/asset/feature/project.json b/libs/web/asset/feature/project.json new file mode 100644 index 0000000..c0525ad --- /dev/null +++ b/libs/web/asset/feature/project.json @@ -0,0 +1,13 @@ +{ + "name": "web-asset-feature", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/web/asset/feature/src", + "projectType": "library", + "tags": ["app:web", "type:feature"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/libs/web/asset/feature/src/index.ts b/libs/web/asset/feature/src/index.ts new file mode 100644 index 0000000..efcc663 --- /dev/null +++ b/libs/web/asset/feature/src/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react' + +export const UserAssetFeature = lazy(() => import('./lib/user-asset.routes')) diff --git a/libs/web/asset/feature/src/lib/user-asset.routes.tsx b/libs/web/asset/feature/src/lib/user-asset.routes.tsx new file mode 100644 index 0000000..8227464 --- /dev/null +++ b/libs/web/asset/feature/src/lib/user-asset.routes.tsx @@ -0,0 +1,48 @@ +import { AspectRatio, Image, SimpleGrid } from '@mantine/core' +import { UiCard, UiDebug, UiInfo, UiLoader, UiPage } from '@pubkey-ui/core' +import { useQuery } from '@tanstack/react-query' +import { useSdk } from '@tokengator/web-core-data-access' +import { useParams, useRoutes } from 'react-router-dom' + +export default function UserAssetRoutes() { + return useRoutes([ + { path: '', element: }, + { path: ':account/*', element: }, + ]) +} + +function UserAssetListFeature() { + return +} + +function useMetadataAll(account: string) { + const sdk = useSdk() + + return useQuery({ + queryKey: [], + queryFn: async () => sdk.metadataAll({ account }).then((res) => res.data?.item), + }) +} + +function UserAssetDetailFeature() { + const { account } = useParams() as { account: string } + const query = useMetadataAll(account) + + const item = query.data + + return query.isLoading ? ( + + ) : ( + + + + + + + + ACTIVITY LISTS + + + + ) +} diff --git a/libs/web/asset/feature/tsconfig.json b/libs/web/asset/feature/tsconfig.json new file mode 100644 index 0000000..d8c59fe --- /dev/null +++ b/libs/web/asset/feature/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../tsconfig.base.json" +} diff --git a/libs/web/asset/feature/tsconfig.lib.json b/libs/web/asset/feature/tsconfig.lib.json new file mode 100644 index 0000000..45b2297 --- /dev/null +++ b/libs/web/asset/feature/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/web/asset/ui/.babelrc b/libs/web/asset/ui/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/web/asset/ui/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/web/asset/ui/.eslintrc.json b/libs/web/asset/ui/.eslintrc.json new file mode 100644 index 0000000..772a43d --- /dev/null +++ b/libs/web/asset/ui/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/web/asset/ui/README.md b/libs/web/asset/ui/README.md new file mode 100644 index 0000000..fc6cf6e --- /dev/null +++ b/libs/web/asset/ui/README.md @@ -0,0 +1,7 @@ +# web-asset-ui + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-asset-ui` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/web/asset/ui/project.json b/libs/web/asset/ui/project.json new file mode 100644 index 0000000..9c6211c --- /dev/null +++ b/libs/web/asset/ui/project.json @@ -0,0 +1,13 @@ +{ + "name": "web-asset-ui", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/web/asset/ui/src", + "projectType": "library", + "tags": ["app:web", "type:ui"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/libs/web/asset/ui/src/index.ts b/libs/web/asset/ui/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/web/asset/ui/tsconfig.json b/libs/web/asset/ui/tsconfig.json new file mode 100644 index 0000000..d8c59fe --- /dev/null +++ b/libs/web/asset/ui/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../tsconfig.base.json" +} diff --git a/libs/web/asset/ui/tsconfig.lib.json b/libs/web/asset/ui/tsconfig.lib.json new file mode 100644 index 0000000..45b2297 --- /dev/null +++ b/libs/web/asset/ui/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/web/core/feature/src/lib/web-core-routes-user.tsx b/libs/web/core/feature/src/lib/web-core-routes-user.tsx index 6bca025..8a3bf0b 100644 --- a/libs/web/core/feature/src/lib/web-core-routes-user.tsx +++ b/libs/web/core/feature/src/lib/web-core-routes-user.tsx @@ -1,5 +1,6 @@ import { UiContainer, UiDashboardGrid, UiDashboardItem, UiNotFound } from '@pubkey-ui/core' import { IconAdjustmentsX, IconSettings, IconUsersGroup } from '@tabler/icons-react' +import { UserAssetFeature } from '@tokengator/web-asset-feature' import { UserClaimPageFeature } from '@tokengator/web-claim-feature' import { UserCommunityFeature } from '@tokengator/web-community-feature' import { UserPresetFeature } from '@tokengator/web-preset-feature' @@ -20,6 +21,7 @@ const routes: RouteObject[] = [ // User Dashboard Routes are added by the web-crud generator { path: '/dashboard', element: }, { path: '/settings/*', element: }, + { path: '/assets/*', element: }, { path: '/solana/*', element: }, { path: '/u/*', element: }, { path: '/claims/*', element: }, diff --git a/package.json b/package.json index b539386..11596a5 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "prestart": "pnpm prisma db push", "setup": "pnpm nx generate setup", "start": "NODE_ENV=production node dist/apps/api/main.js", - "test:all": "nx run-many --target=test --all" + "test:all": "nx run-many --target=test --all", + "tunnel": "npx localtunnel --port 3000 -s tokengator --print-requests" }, "private": true, "dependencies": { @@ -174,6 +175,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-environment-node": "^29.7.0", "lint-staged": "^15.2.0", + "localtunnel": "^2.0.2", "nx": "17.2.8", "pg": "^8.11.3", "pluralize": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bec6839..5399661 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,6 +436,9 @@ devDependencies: lint-staged: specifier: ^15.2.0 version: 15.2.0 + localtunnel: + specifier: ^2.0.2 + version: 2.0.2 nx: specifier: 17.2.8 version: 17.2.8(@swc-node/register@1.6.8)(@swc/core@1.3.102) @@ -8530,10 +8533,18 @@ packages: engines: {node: '>=4'} dev: true + /axios@0.21.4(debug@4.3.2): + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + dependencies: + follow-redirects: 1.15.4(debug@4.3.2) + transitivePeerDependencies: + - debug + dev: true + /axios@1.6.5: resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} dependencies: - follow-redirects: 1.15.4 + follow-redirects: 1.15.4(debug@4.3.2) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -9331,6 +9342,14 @@ packages: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -10160,6 +10179,17 @@ packages: ms: 2.1.3 supports-color: 8.1.1 + /debug@4.3.2: + resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -11523,7 +11553,7 @@ packages: engines: {node: '>=0.4.0'} dev: false - /follow-redirects@1.15.4: + /follow-redirects@1.15.4(debug@4.3.2): resolution: {integrity: sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==} engines: {node: '>=4.0'} peerDependencies: @@ -11531,6 +11561,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.2 /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -12177,7 +12209,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.4 + follow-redirects: 1.15.4(debug@4.3.2) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -13875,6 +13907,19 @@ packages: engines: {node: '>= 12.13.0'} dev: true + /localtunnel@2.0.2: + resolution: {integrity: sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==} + engines: {node: '>=8.3.0'} + hasBin: true + dependencies: + axios: 0.21.4(debug@4.3.2) + debug: 4.3.2 + openurl: 1.1.1 + yargs: 17.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -15182,6 +15227,10 @@ packages: tiny-inflate: 1.0.3 dev: false + /openurl@1.1.1: + resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} + dev: true + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -19487,6 +19536,11 @@ packages: camelcase: 5.3.1 decamelize: 1.2.0 + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -19507,6 +19561,19 @@ packages: y18n: 4.0.3 yargs-parser: 18.1.3 + /yargs@17.1.1: + resolution: {integrity: sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==} + engines: {node: '>=12'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0106ebf..35dae68 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -43,6 +43,9 @@ "@tokengator/api-wallet-feature": ["libs/api/wallet/feature/src/index.ts"], "@tokengator/sdk": ["libs/sdk/src/index.ts"], "@tokengator/tools": ["libs/tools/src/index.ts"], + "@tokengator/web-asset-data-access": ["libs/web/asset/data-access/src/index.ts"], + "@tokengator/web-asset-feature": ["libs/web/asset/feature/src/index.ts"], + "@tokengator/web-asset-ui": ["libs/web/asset/ui/src/index.ts"], "@tokengator/web-auth-data-access": ["libs/web/auth/data-access/src/index.ts"], "@tokengator/web-auth-feature": ["libs/web/auth/feature/src/index.ts"], "@tokengator/web-auth-ui": ["libs/web/auth/ui/src/index.ts"],