Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices

Compare changes

Choose any two refs to compare.

+4 -4
README.md
···
2. **Fetch the GraphQL schema**
```bash
-
npm run schema
+
npm run schema:prod
```
3. **Generate Relay types**
···
The project connects to the Slices API. To update the schema:
```bash
-
npm run schema
+
npm run schema:prod
npx relay-compiler
```
···
- **Production API**: `https://api.slices.network/graphql`
- **Slice**:
-
`at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a`
+
`at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26`
## Scripts
···
- `npm run build` - Build for production
- `npm run preview` - Preview production build
- `npm run lint` - Run ESLint
-
- `npm run schema` - Fetch GraphQL schema from production API
+
- `npm run schema:prod` - Fetch GraphQL schema from production API
## Features in Detail
+1
_redirects
···
+
/* /index.html 200
+8
deno.lock
···
"npm:eslint-plugin-react-refresh@~0.4.22": "0.4.23_eslint@9.36.0",
"npm:eslint@^9.36.0": "9.36.0",
"npm:globals@^16.4.0": "16.4.0",
+
"npm:graphql-ws@^6.0.6": "6.0.6_graphql@16.11.0",
"npm:graphql@^16.11.0": "16.11.0",
"npm:postcss@^8.5.6": "8.5.6",
"npm:react-dom@^19.1.1": "19.2.0_react@19.2.0",
···
"graphemer@1.4.0": {
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
+
"graphql-ws@6.0.6_graphql@16.11.0": {
+
"integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==",
+
"dependencies": [
+
"graphql@16.11.0"
+
]
+
},
"graphql@15.3.0": {
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
},
···
"npm:eslint-plugin-react-refresh@~0.4.22",
"npm:eslint@^9.36.0",
"npm:globals@^16.4.0",
+
"npm:graphql-ws@^6.0.6",
"npm:graphql@^16.11.0",
"npm:postcss@^8.5.6",
"npm:react-dom@^19.1.1",
+4 -3
package.json
···
"type": "module",
"scripts": {
"dev": "vite",
-
"build": "tsc -b && vite build",
+
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
-
"schema:dev": "npx get-graphql-schema 'http://localhost:3000/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql",
-
"schema:prod": "npx get-graphql-schema 'https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql"
+
"schema:dev": "npx get-graphql-schema 'http://localhost:3000/graphql?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26' > schema.graphql",
+
"schema:prod": "npx get-graphql-schema 'https://api.slices.network/graphql?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26' > schema.graphql"
},
"dependencies": {
+
"graphql-ws": "^6.0.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-relay": "^20.1.1",
+977 -125
schema.graphql
···
}
type AppBskyActorProfile {
+
id: ID!
uri: String!
cid: String!
did: String!
···
joinedViaStarterPack: JSON
labels: JSON
pinnedPost: JSON
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
-
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyActorProfileAggregated {
-
avatar: String
-
banner: String
-
createdAt: String
-
description: String
-
displayName: String
-
joinedViaStarterPack: String
-
labels: String
-
pinnedPost: String
+
avatar: JSON
+
banner: JSON
+
createdAt: JSON
+
description: JSON
+
displayName: JSON
+
joinedViaStarterPack: JSON
+
labels: JSON
+
pinnedPost: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyActorProfileGroupByField {
+
indexedAt
+
avatar
+
banner
+
createdAt
+
description
+
displayName
+
joinedViaStarterPack
+
labels
+
pinnedPost
+
}
+
+
input AppBskyActorProfileGroupByFieldInput {
+
field: AppBskyActorProfileGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyActorProfileInput {
+
avatar: JSON
+
banner: JSON
+
createdAt: String
+
description: String
+
displayName: String
+
joinedViaStarterPack: JSON
+
labels: JSON
+
pinnedPost: JSON
+
}
+
+
input AppBskyActorProfileSortFieldInput {
+
field: AppBskyActorProfileGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyActorProfileWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
avatar: StringFilter
+
banner: StringFilter
+
createdAt: StringFilter
+
description: StringFilter
+
displayName: StringFilter
+
joinedViaStarterPack: StringFilter
+
labels: StringFilter
+
pinnedPost: StringFilter
+
json: StringFilter
+
and: [AppBskyActorProfileWhereInput]
+
or: [AppBskyActorProfileWhereInput]
+
}
+
+
type AppBskyEmbedDefsAspectRatio {
+
height: Int
+
width: Int
+
}
+
type AppBskyEmbedExternal {
+
id: ID!
uri: String!
cid: String!
did: String!
indexedAt: String!
actorHandle: String
-
external: JSON!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
external: AppBskyEmbedExternalExternal!
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyEmbedExternalAggregated {
-
external: String
+
external: JSON
count: Int!
}
···
cursor: String!
}
+
type AppBskyEmbedExternalExternal {
+
description: String
+
thumb: Blob
+
title: String
+
uri: String
+
}
+
+
enum AppBskyEmbedExternalGroupByField {
+
indexedAt
+
external
+
}
+
+
input AppBskyEmbedExternalGroupByFieldInput {
+
field: AppBskyEmbedExternalGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyEmbedExternalInput {
+
external: JSON!
+
}
+
+
input AppBskyEmbedExternalSortFieldInput {
+
field: AppBskyEmbedExternalGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyEmbedExternalWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
external: StringFilter
+
json: StringFilter
+
and: [AppBskyEmbedExternalWhereInput]
+
or: [AppBskyEmbedExternalWhereInput]
+
}
+
type AppBskyEmbedImages {
+
id: ID!
uri: String!
cid: String!
did: String!
indexedAt: String!
actorHandle: String
-
images: JSON!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
images: [AppBskyEmbedImagesImage!]
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyEmbedImagesAggregated {
-
images: String
+
images: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyEmbedImagesGroupByField {
+
indexedAt
+
images
+
}
+
+
input AppBskyEmbedImagesGroupByFieldInput {
+
field: AppBskyEmbedImagesGroupByField!
+
interval: DateInterval
+
}
+
+
type AppBskyEmbedImagesImage {
+
alt: String
+
aspectRatio: JSON
+
image: Blob
+
}
+
+
input AppBskyEmbedImagesInput {
+
images: JSON!
+
}
+
+
input AppBskyEmbedImagesSortFieldInput {
+
field: AppBskyEmbedImagesGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyEmbedImagesWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
images: StringFilter
+
json: StringFilter
+
and: [AppBskyEmbedImagesWhereInput]
+
or: [AppBskyEmbedImagesWhereInput]
+
}
+
type AppBskyEmbedRecord {
-
uri: String!
-
cid: String!
-
did: String!
-
indexedAt: String!
-
actorHandle: String
-
record: JSON!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
-
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
-
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
-
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
-
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
record: JSON
}
type AppBskyEmbedRecordAggregated {
-
record: String
+
record: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyEmbedRecordGroupByField {
+
indexedAt
+
record
+
}
+
+
input AppBskyEmbedRecordGroupByFieldInput {
+
field: AppBskyEmbedRecordGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyEmbedRecordInput {
+
record: JSON!
+
}
+
+
input AppBskyEmbedRecordSortFieldInput {
+
field: AppBskyEmbedRecordGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyEmbedRecordWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
record: StringFilter
+
json: StringFilter
+
and: [AppBskyEmbedRecordWhereInput]
+
or: [AppBskyEmbedRecordWhereInput]
+
}
+
type AppBskyEmbedRecordWithMedia {
+
id: ID!
uri: String!
cid: String!
did: String!
indexedAt: String!
actorHandle: String
media: JSON!
-
record: JSON!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
record: AppBskyEmbedRecord!
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyEmbedRecordWithMediaAggregated {
-
media: String
-
record: String
+
media: JSON
+
record: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyEmbedRecordWithMediaGroupByField {
+
indexedAt
+
media
+
record
+
}
+
+
input AppBskyEmbedRecordWithMediaGroupByFieldInput {
+
field: AppBskyEmbedRecordWithMediaGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyEmbedRecordWithMediaInput {
+
media: JSON!
+
record: JSON!
+
}
+
+
input AppBskyEmbedRecordWithMediaSortFieldInput {
+
field: AppBskyEmbedRecordWithMediaGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyEmbedRecordWithMediaWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
media: StringFilter
+
record: StringFilter
+
json: StringFilter
+
and: [AppBskyEmbedRecordWithMediaWhereInput]
+
or: [AppBskyEmbedRecordWithMediaWhereInput]
+
}
+
type AppBskyEmbedVideo {
+
id: ID!
uri: String!
cid: String!
did: String!
indexedAt: String!
actorHandle: String
alt: String
-
aspectRatio: JSON
-
captions: JSON
-
video: Blob!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
aspectRatio: AppBskyEmbedDefsAspectRatio
+
captions: [AppBskyEmbedVideoCaption]
+
video: Blob
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyEmbedVideoAggregated {
-
alt: String
-
aspectRatio: String
-
captions: String
-
video: String
+
alt: JSON
+
aspectRatio: JSON
+
captions: JSON
+
video: JSON
count: Int!
+
}
+
+
type AppBskyEmbedVideoCaption {
+
file: Blob
+
lang: String
}
type AppBskyEmbedVideoConnection {
···
cursor: String!
}
+
enum AppBskyEmbedVideoGroupByField {
+
indexedAt
+
alt
+
aspectRatio
+
captions
+
video
+
}
+
+
input AppBskyEmbedVideoGroupByFieldInput {
+
field: AppBskyEmbedVideoGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyEmbedVideoInput {
+
alt: String
+
aspectRatio: JSON
+
captions: JSON
+
video: JSON!
+
}
+
+
input AppBskyEmbedVideoSortFieldInput {
+
field: AppBskyEmbedVideoGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyEmbedVideoWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
alt: StringFilter
+
aspectRatio: StringFilter
+
captions: StringFilter
+
video: StringFilter
+
json: StringFilter
+
and: [AppBskyEmbedVideoWhereInput]
+
or: [AppBskyEmbedVideoWhereInput]
+
}
+
type AppBskyFeedPostgate {
+
id: ID!
uri: String!
cid: String!
did: String!
···
detachedEmbeddingUris: [String]
embeddingRules: JSON
post: String!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgate: AppBskyFeedThreadgate
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlay: FmTealAlphaFeedPlay
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyFeedPostgateAggregated {
-
createdAt: String
-
detachedEmbeddingUris: String
-
embeddingRules: String
-
post: String
+
createdAt: JSON
+
detachedEmbeddingUris: JSON
+
embeddingRules: JSON
+
post: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyFeedPostgateGroupByField {
+
indexedAt
+
createdAt
+
detachedEmbeddingUris
+
embeddingRules
+
post
+
}
+
+
input AppBskyFeedPostgateGroupByFieldInput {
+
field: AppBskyFeedPostgateGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyFeedPostgateInput {
+
createdAt: String!
+
detachedEmbeddingUris: [String]
+
embeddingRules: JSON
+
post: String!
+
}
+
+
input AppBskyFeedPostgateSortFieldInput {
+
field: AppBskyFeedPostgateGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyFeedPostgateWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
createdAt: StringFilter
+
detachedEmbeddingUris: StringFilter
+
embeddingRules: StringFilter
+
post: StringFilter
+
json: StringFilter
+
and: [AppBskyFeedPostgateWhereInput]
+
or: [AppBskyFeedPostgateWhereInput]
+
}
+
type AppBskyFeedThreadgate {
+
id: ID!
uri: String!
cid: String!
did: String!
···
createdAt: String!
hiddenReplies: [String]
post: String!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedPostgate: AppBskyFeedPostgate
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlay: FmTealAlphaFeedPlay
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyFeedThreadgateAggregated {
-
allow: String
-
createdAt: String
-
hiddenReplies: String
-
post: String
+
allow: JSON
+
createdAt: JSON
+
hiddenReplies: JSON
+
post: JSON
count: Int!
}
···
cursor: String!
}
+
enum AppBskyFeedThreadgateGroupByField {
+
indexedAt
+
allow
+
createdAt
+
hiddenReplies
+
post
+
}
+
+
input AppBskyFeedThreadgateGroupByFieldInput {
+
field: AppBskyFeedThreadgateGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyFeedThreadgateInput {
+
allow: JSON
+
createdAt: String!
+
hiddenReplies: [String]
+
post: String!
+
}
+
+
input AppBskyFeedThreadgateSortFieldInput {
+
field: AppBskyFeedThreadgateGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyFeedThreadgateWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
allow: StringFilter
+
createdAt: StringFilter
+
hiddenReplies: StringFilter
+
post: StringFilter
+
json: StringFilter
+
and: [AppBskyFeedThreadgateWhereInput]
+
or: [AppBskyFeedThreadgateWhereInput]
+
}
+
type AppBskyRichtextFacet {
+
id: ID!
uri: String!
cid: String!
did: String!
indexedAt: String!
actorHandle: String
features: JSON!
-
index: JSON!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
index: AppBskyRichtextFacetByteSlice!
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type AppBskyRichtextFacetAggregated {
-
features: String
-
index: String
+
features: JSON
+
index: JSON
count: Int!
}
+
type AppBskyRichtextFacetByteSlice {
+
byteEnd: Int
+
byteStart: Int
+
}
+
type AppBskyRichtextFacetConnection {
totalCount: Int!
pageInfo: PageInfo!
···
cursor: String!
}
+
enum AppBskyRichtextFacetGroupByField {
+
indexedAt
+
features
+
index
+
}
+
+
input AppBskyRichtextFacetGroupByFieldInput {
+
field: AppBskyRichtextFacetGroupByField!
+
interval: DateInterval
+
}
+
+
input AppBskyRichtextFacetInput {
+
features: JSON!
+
index: JSON!
+
}
+
+
input AppBskyRichtextFacetSortFieldInput {
+
field: AppBskyRichtextFacetGroupByField!
+
direction: SortDirection
+
}
+
+
input AppBskyRichtextFacetWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
features: StringFilter
+
index: StringFilter
+
json: StringFilter
+
and: [AppBskyRichtextFacetWhereInput]
+
or: [AppBskyRichtextFacetWhereInput]
+
}
+
type Blob {
ref: String!
mimeType: String!
···
url(preset: String): String!
}
+
type BlobUploadResponse {
+
blob: Blob!
+
}
+
+
type CollectionSummary {
+
collection: String!
+
estimatedRepos: Int!
+
isExternal: Boolean!
+
}
+
type ComAtprotoRepoStrongRef {
+
id: ID!
did: String!
indexedAt: String!
actorHandle: String
cid: String!
uri: String!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedPostgate: AppBskyFeedPostgate
+
appBskyFeedThreadgate: AppBskyFeedThreadgate
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlay: FmTealAlphaFeedPlay
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type ComAtprotoRepoStrongRefAggregated {
-
cid: String
-
uri: String
+
cid: JSON
+
uri: JSON
count: Int!
}
···
cursor: String!
}
+
enum ComAtprotoRepoStrongRefGroupByField {
+
indexedAt
+
cid
+
uri
+
}
+
+
input ComAtprotoRepoStrongRefGroupByFieldInput {
+
field: ComAtprotoRepoStrongRefGroupByField!
+
interval: DateInterval
+
}
+
+
input ComAtprotoRepoStrongRefInput {
+
cid: String!
+
uri: String!
+
}
+
+
input ComAtprotoRepoStrongRefSortFieldInput {
+
field: ComAtprotoRepoStrongRefGroupByField!
+
direction: SortDirection
+
}
+
+
input ComAtprotoRepoStrongRefWhereInput {
+
indexedAt: DateTimeFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
cid: StringFilter
+
uri: StringFilter
+
json: StringFilter
+
and: [ComAtprotoRepoStrongRefWhereInput]
+
or: [ComAtprotoRepoStrongRefWhereInput]
+
}
+
+
enum DateInterval {
+
second
+
minute
+
hour
+
day
+
week
+
month
+
quarter
+
year
+
}
+
+
input DateTimeFilter {
+
eq: String
+
gt: String
+
gte: String
+
lt: String
+
lte: String
+
}
+
+
type DeleteSliceRecordsOutput {
+
message: String!
+
recordsDeleted: Int!
+
actorsDeleted: Int!
+
}
+
+
type FmTealAlphaFeedDefsArtist {
+
artistMbId: String
+
artistName: String
+
}
+
type FmTealAlphaFeedPlay {
+
id: ID!
uri: String!
cid: String!
did: String!
···
actorHandle: String
artistMbIds: [String]
artistNames: [String]
-
artists: JSON
+
artists: [FmTealAlphaFeedDefsArtist]
duration: Int
isrc: String
musicServiceBaseDomain: String
···
submissionClientAgent: String
trackMbId: String
trackName: String!
-
appBskyFeedPostgate(limit: Int): [AppBskyFeedPostgate!]!
-
appBskyFeedThreadgate(limit: Int): [AppBskyFeedThreadgate!]!
appBskyActorProfile: AppBskyActorProfile
-
fmTealAlphaFeedPlay(limit: Int): [FmTealAlphaFeedPlay!]!
appBskyFeedPostgates(limit: Int): [AppBskyFeedPostgate!]!
+
appBskyFeedPostgatesCount: Int!
appBskyFeedThreadgates(limit: Int): [AppBskyFeedThreadgate!]!
+
appBskyFeedThreadgatesCount: Int!
appBskyActorProfiles(limit: Int): [AppBskyActorProfile!]!
+
appBskyActorProfilesCount: Int!
fmTealAlphaFeedPlays(limit: Int): [FmTealAlphaFeedPlay!]!
+
fmTealAlphaFeedPlaysCount: Int!
}
type FmTealAlphaFeedPlayAggregated {
-
artistMbIds: String
-
artistNames: String
-
artists: String
-
duration: String
-
isrc: String
-
musicServiceBaseDomain: String
-
originUrl: String
-
playedTime: String
-
recordingMbId: String
-
releaseMbId: String
-
releaseName: String
-
submissionClientAgent: String
-
trackMbId: String
-
trackName: String
+
artistMbIds: JSON
+
artistNames: JSON
+
artists: JSON
+
duration: JSON
+
isrc: JSON
+
musicServiceBaseDomain: JSON
+
originUrl: JSON
+
playedTime: JSON
+
recordingMbId: JSON
+
releaseMbId: JSON
+
releaseName: JSON
+
submissionClientAgent: JSON
+
trackMbId: JSON
+
trackName: JSON
count: Int!
}
···
cursor: String!
}
+
enum FmTealAlphaFeedPlayGroupByField {
+
indexedAt
+
artistMbIds
+
artistNames
+
artists
+
duration
+
isrc
+
musicServiceBaseDomain
+
originUrl
+
playedTime
+
recordingMbId
+
releaseMbId
+
releaseName
+
submissionClientAgent
+
trackMbId
+
trackName
+
}
+
+
input FmTealAlphaFeedPlayGroupByFieldInput {
+
field: FmTealAlphaFeedPlayGroupByField!
+
interval: DateInterval
+
}
+
+
input FmTealAlphaFeedPlayInput {
+
artistMbIds: [String]
+
artistNames: [String]
+
artists: JSON
+
duration: Int
+
isrc: String
+
musicServiceBaseDomain: String
+
originUrl: String
+
playedTime: String
+
recordingMbId: String
+
releaseMbId: String
+
releaseName: String
+
submissionClientAgent: String
+
trackMbId: String
+
trackName: String!
+
}
+
+
input FmTealAlphaFeedPlaySortFieldInput {
+
field: FmTealAlphaFeedPlayGroupByField!
+
direction: SortDirection
+
}
+
+
input FmTealAlphaFeedPlayWhereInput {
+
indexedAt: DateTimeFilter
+
uri: StringFilter
+
cid: StringFilter
+
did: StringFilter
+
collection: StringFilter
+
actorHandle: StringFilter
+
artistMbIds: StringFilter
+
artistNames: StringFilter
+
artists: StringFilter
+
duration: IntFilter
+
isrc: StringFilter
+
musicServiceBaseDomain: StringFilter
+
originUrl: StringFilter
+
playedTime: StringFilter
+
recordingMbId: StringFilter
+
releaseMbId: StringFilter
+
releaseName: StringFilter
+
submissionClientAgent: StringFilter
+
trackMbId: StringFilter
+
trackName: StringFilter
+
json: StringFilter
+
and: [FmTealAlphaFeedPlayWhereInput]
+
or: [FmTealAlphaFeedPlayWhereInput]
+
}
+
+
input IntFilter {
+
eq: Int
+
in: [Int]
+
gt: Int
+
gte: Int
+
lt: Int
+
lte: Int
+
}
+
+
type JetstreamLogEntry {
+
id: String!
+
createdAt: String!
+
logType: String!
+
jobId: String
+
userDid: String
+
sliceUri: String
+
level: String!
+
message: String!
+
metadata: JSON
+
}
+
scalar JSON
type Mutation {
"""Sync user collections for a given DID"""
syncUserCollections(did: String!): SyncResult!
+
+
"""Create a new app.bsky.embed.record record"""
+
createAppBskyEmbedRecord(input: AppBskyEmbedRecordInput!, rkey: String): AppBskyEmbedRecord!
+
+
"""Update a app.bsky.embed.record record"""
+
updateAppBskyEmbedRecord(rkey: String!, input: AppBskyEmbedRecordInput!): AppBskyEmbedRecord!
+
+
"""Delete a app.bsky.embed.record record"""
+
deleteAppBskyEmbedRecord(rkey: String!): AppBskyEmbedRecord!
+
+
"""Create a new app.bsky.embed.images record"""
+
createAppBskyEmbedImages(input: AppBskyEmbedImagesInput!, rkey: String): AppBskyEmbedImages!
+
+
"""Update a app.bsky.embed.images record"""
+
updateAppBskyEmbedImages(rkey: String!, input: AppBskyEmbedImagesInput!): AppBskyEmbedImages!
+
+
"""Delete a app.bsky.embed.images record"""
+
deleteAppBskyEmbedImages(rkey: String!): AppBskyEmbedImages!
+
+
"""Create a new app.bsky.embed.recordWithMedia record"""
+
createAppBskyEmbedRecordWithMedia(input: AppBskyEmbedRecordWithMediaInput!, rkey: String): AppBskyEmbedRecordWithMedia!
+
+
"""Update a app.bsky.embed.recordWithMedia record"""
+
updateAppBskyEmbedRecordWithMedia(rkey: String!, input: AppBskyEmbedRecordWithMediaInput!): AppBskyEmbedRecordWithMedia!
+
+
"""Delete a app.bsky.embed.recordWithMedia record"""
+
deleteAppBskyEmbedRecordWithMedia(rkey: String!): AppBskyEmbedRecordWithMedia!
+
+
"""Create a new app.bsky.embed.video record"""
+
createAppBskyEmbedVideo(input: AppBskyEmbedVideoInput!, rkey: String): AppBskyEmbedVideo!
+
+
"""Update a app.bsky.embed.video record"""
+
updateAppBskyEmbedVideo(rkey: String!, input: AppBskyEmbedVideoInput!): AppBskyEmbedVideo!
+
+
"""Delete a app.bsky.embed.video record"""
+
deleteAppBskyEmbedVideo(rkey: String!): AppBskyEmbedVideo!
+
+
"""Create a new app.bsky.embed.external record"""
+
createAppBskyEmbedExternal(input: AppBskyEmbedExternalInput!, rkey: String): AppBskyEmbedExternal!
+
+
"""Update a app.bsky.embed.external record"""
+
updateAppBskyEmbedExternal(rkey: String!, input: AppBskyEmbedExternalInput!): AppBskyEmbedExternal!
+
+
"""Delete a app.bsky.embed.external record"""
+
deleteAppBskyEmbedExternal(rkey: String!): AppBskyEmbedExternal!
+
+
"""Create a new app.bsky.feed.postgate record"""
+
createAppBskyFeedPostgate(input: AppBskyFeedPostgateInput!, rkey: String): AppBskyFeedPostgate!
+
+
"""Update a app.bsky.feed.postgate record"""
+
updateAppBskyFeedPostgate(rkey: String!, input: AppBskyFeedPostgateInput!): AppBskyFeedPostgate!
+
+
"""Delete a app.bsky.feed.postgate record"""
+
deleteAppBskyFeedPostgate(rkey: String!): AppBskyFeedPostgate!
+
+
"""Create a new app.bsky.feed.threadgate record"""
+
createAppBskyFeedThreadgate(input: AppBskyFeedThreadgateInput!, rkey: String): AppBskyFeedThreadgate!
+
+
"""Update a app.bsky.feed.threadgate record"""
+
updateAppBskyFeedThreadgate(rkey: String!, input: AppBskyFeedThreadgateInput!): AppBskyFeedThreadgate!
+
+
"""Delete a app.bsky.feed.threadgate record"""
+
deleteAppBskyFeedThreadgate(rkey: String!): AppBskyFeedThreadgate!
+
+
"""Create a new app.bsky.richtext.facet record"""
+
createAppBskyRichtextFacet(input: AppBskyRichtextFacetInput!, rkey: String): AppBskyRichtextFacet!
+
+
"""Update a app.bsky.richtext.facet record"""
+
updateAppBskyRichtextFacet(rkey: String!, input: AppBskyRichtextFacetInput!): AppBskyRichtextFacet!
+
+
"""Delete a app.bsky.richtext.facet record"""
+
deleteAppBskyRichtextFacet(rkey: String!): AppBskyRichtextFacet!
+
+
"""Create a new app.bsky.actor.profile record"""
+
createAppBskyActorProfile(input: AppBskyActorProfileInput!, rkey: String): AppBskyActorProfile!
+
+
"""Update a app.bsky.actor.profile record"""
+
updateAppBskyActorProfile(rkey: String!, input: AppBskyActorProfileInput!): AppBskyActorProfile!
+
+
"""Delete a app.bsky.actor.profile record"""
+
deleteAppBskyActorProfile(rkey: String!): AppBskyActorProfile!
+
+
"""Create a new com.atproto.repo.strongRef record"""
+
createComAtprotoRepoStrongRef(input: ComAtprotoRepoStrongRefInput!, rkey: String): ComAtprotoRepoStrongRef!
+
+
"""Update a com.atproto.repo.strongRef record"""
+
updateComAtprotoRepoStrongRef(rkey: String!, input: ComAtprotoRepoStrongRefInput!): ComAtprotoRepoStrongRef!
+
+
"""Delete a com.atproto.repo.strongRef record"""
+
deleteComAtprotoRepoStrongRef(rkey: String!): ComAtprotoRepoStrongRef!
+
+
"""Create a new fm.teal.alpha.feed.play record"""
+
createFmTealAlphaFeedPlay(input: FmTealAlphaFeedPlayInput!, rkey: String): FmTealAlphaFeedPlay!
+
+
"""Update a fm.teal.alpha.feed.play record"""
+
updateFmTealAlphaFeedPlay(rkey: String!, input: FmTealAlphaFeedPlayInput!): FmTealAlphaFeedPlay!
+
+
"""Delete a fm.teal.alpha.feed.play record"""
+
deleteFmTealAlphaFeedPlay(rkey: String!): FmTealAlphaFeedPlay!
+
+
"""Start a sync job to backfill collections from the ATProto relay"""
+
startSync(slice: String, collections: [String], externalCollections: [String], repos: [String], limitPerRepo: Int, skipValidation: Boolean, maxRepos: Int): StartSyncOutput!
+
+
"""Cancel a pending or running sync job"""
+
cancelJob(jobId: String!): Boolean!
+
+
"""Delete a sync job from the database"""
+
deleteJob(id: ID!): ID
+
+
"""Upload a blob to the user's AT Protocol repository"""
+
uploadBlob(data: String!, mimeType: String!): BlobUploadResponse!
+
+
"""Register a new OAuth client for a slice"""
+
createOAuthClient(sliceUri: String!, clientName: String!, redirectUris: [String!]!, scope: String!, clientUri: String, logoUri: String, tosUri: String, policyUri: String): OAuthClient!
+
+
"""Update an OAuth client"""
+
updateOAuthClient(clientId: String!, clientName: String, redirectUris: [String], scope: String, clientUri: String, logoUri: String, tosUri: String, policyUri: String): OAuthClient!
+
+
"""Delete an OAuth client"""
+
deleteOAuthClient(clientId: String!): Boolean!
+
+
"""
+
Delete all records and actors from a slice index. Requires authentication and slice ownership.
+
"""
+
deleteSliceRecords(slice: String): DeleteSliceRecordsOutput!
+
}
+
+
type OAuthClient {
+
clientId: String!
+
clientSecret: String
+
clientName: String!
+
redirectUris: [String!]!
+
grantTypes: [String!]!
+
responseTypes: [String!]!
+
scope: String
+
clientUri: String
+
logoUri: String
+
tosUri: String
+
policyUri: String
+
createdAt: String!
+
createdByDid: String!
}
type PageInfo {
···
type Query {
"""Query app.bsky.embed.record records"""
-
appBskyEmbedRecords(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedRecordConnection!
+
appBskyEmbedRecords(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyEmbedRecordSortFieldInput], where: AppBskyEmbedRecordWhereInput): AppBskyEmbedRecordConnection!
"""
Aggregated query for app.bsky.embed.record records with GROUP BY support
"""
-
appBskyEmbedRecordsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordAggregated!]!
+
appBskyEmbedRecordsAggregated(groupBy: [AppBskyEmbedRecordGroupByFieldInput!], where: AppBskyEmbedRecordWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordAggregated!]!
"""Query app.bsky.embed.images records"""
-
appBskyEmbedImageses(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedImagesConnection!
+
appBskyEmbedImageses(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyEmbedImagesSortFieldInput], where: AppBskyEmbedImagesWhereInput): AppBskyEmbedImagesConnection!
"""
Aggregated query for app.bsky.embed.images records with GROUP BY support
"""
-
appBskyEmbedImagesesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedImagesAggregated!]!
+
appBskyEmbedImagesesAggregated(groupBy: [AppBskyEmbedImagesGroupByFieldInput!], where: AppBskyEmbedImagesWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedImagesAggregated!]!
-
"""Query app.bsky.embed.video records"""
-
appBskyEmbedVideos(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedVideoConnection!
+
"""Query app.bsky.embed.recordWithMedia records"""
+
appBskyEmbedRecordWithMedias(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyEmbedRecordWithMediaSortFieldInput], where: AppBskyEmbedRecordWithMediaWhereInput): AppBskyEmbedRecordWithMediaConnection!
"""
-
Aggregated query for app.bsky.embed.video records with GROUP BY support
+
Aggregated query for app.bsky.embed.recordWithMedia records with GROUP BY support
"""
-
appBskyEmbedVideosAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedVideoAggregated!]!
+
appBskyEmbedRecordWithMediasAggregated(groupBy: [AppBskyEmbedRecordWithMediaGroupByFieldInput!], where: AppBskyEmbedRecordWithMediaWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordWithMediaAggregated!]!
-
"""Query app.bsky.embed.recordWithMedia records"""
-
appBskyEmbedRecordWithMedias(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedRecordWithMediaConnection!
+
"""Query app.bsky.embed.video records"""
+
appBskyEmbedVideos(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyEmbedVideoSortFieldInput], where: AppBskyEmbedVideoWhereInput): AppBskyEmbedVideoConnection!
"""
-
Aggregated query for app.bsky.embed.recordWithMedia records with GROUP BY support
+
Aggregated query for app.bsky.embed.video records with GROUP BY support
"""
-
appBskyEmbedRecordWithMediasAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedRecordWithMediaAggregated!]!
+
appBskyEmbedVideosAggregated(groupBy: [AppBskyEmbedVideoGroupByFieldInput!], where: AppBskyEmbedVideoWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedVideoAggregated!]!
"""Query app.bsky.embed.external records"""
-
appBskyEmbedExternals(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyEmbedExternalConnection!
+
appBskyEmbedExternals(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyEmbedExternalSortFieldInput], where: AppBskyEmbedExternalWhereInput): AppBskyEmbedExternalConnection!
"""
Aggregated query for app.bsky.embed.external records with GROUP BY support
"""
-
appBskyEmbedExternalsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedExternalAggregated!]!
+
appBskyEmbedExternalsAggregated(groupBy: [AppBskyEmbedExternalGroupByFieldInput!], where: AppBskyEmbedExternalWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyEmbedExternalAggregated!]!
"""Query app.bsky.feed.postgate records"""
-
appBskyFeedPostgates(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyFeedPostgateConnection!
+
appBskyFeedPostgates(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyFeedPostgateSortFieldInput], where: AppBskyFeedPostgateWhereInput): AppBskyFeedPostgateConnection!
"""
Aggregated query for app.bsky.feed.postgate records with GROUP BY support
"""
-
appBskyFeedPostgatesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedPostgateAggregated!]!
+
appBskyFeedPostgatesAggregated(groupBy: [AppBskyFeedPostgateGroupByFieldInput!], where: AppBskyFeedPostgateWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedPostgateAggregated!]!
"""Query app.bsky.feed.threadgate records"""
-
appBskyFeedThreadgates(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyFeedThreadgateConnection!
+
appBskyFeedThreadgates(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyFeedThreadgateSortFieldInput], where: AppBskyFeedThreadgateWhereInput): AppBskyFeedThreadgateConnection!
"""
Aggregated query for app.bsky.feed.threadgate records with GROUP BY support
"""
-
appBskyFeedThreadgatesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedThreadgateAggregated!]!
+
appBskyFeedThreadgatesAggregated(groupBy: [AppBskyFeedThreadgateGroupByFieldInput!], where: AppBskyFeedThreadgateWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyFeedThreadgateAggregated!]!
"""Query app.bsky.richtext.facet records"""
-
appBskyRichtextFacets(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyRichtextFacetConnection!
+
appBskyRichtextFacets(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyRichtextFacetSortFieldInput], where: AppBskyRichtextFacetWhereInput): AppBskyRichtextFacetConnection!
"""
Aggregated query for app.bsky.richtext.facet records with GROUP BY support
"""
-
appBskyRichtextFacetsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyRichtextFacetAggregated!]!
+
appBskyRichtextFacetsAggregated(groupBy: [AppBskyRichtextFacetGroupByFieldInput!], where: AppBskyRichtextFacetWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyRichtextFacetAggregated!]!
"""Query app.bsky.actor.profile records"""
-
appBskyActorProfiles(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): AppBskyActorProfileConnection!
+
appBskyActorProfiles(first: Int, after: String, last: Int, before: String, sortBy: [AppBskyActorProfileSortFieldInput], where: AppBskyActorProfileWhereInput): AppBskyActorProfileConnection!
"""
Aggregated query for app.bsky.actor.profile records with GROUP BY support
"""
-
appBskyActorProfilesAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [AppBskyActorProfileAggregated!]!
+
appBskyActorProfilesAggregated(groupBy: [AppBskyActorProfileGroupByFieldInput!], where: AppBskyActorProfileWhereInput, orderBy: AggregationOrderBy, limit: Int): [AppBskyActorProfileAggregated!]!
"""Query com.atproto.repo.strongRef records"""
-
comAtprotoRepoStrongRefs(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): ComAtprotoRepoStrongRefConnection!
+
comAtprotoRepoStrongRefs(first: Int, after: String, last: Int, before: String, sortBy: [ComAtprotoRepoStrongRefSortFieldInput], where: ComAtprotoRepoStrongRefWhereInput): ComAtprotoRepoStrongRefConnection!
"""
Aggregated query for com.atproto.repo.strongRef records with GROUP BY support
"""
-
comAtprotoRepoStrongRefsAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [ComAtprotoRepoStrongRefAggregated!]!
+
comAtprotoRepoStrongRefsAggregated(groupBy: [ComAtprotoRepoStrongRefGroupByFieldInput!], where: ComAtprotoRepoStrongRefWhereInput, orderBy: AggregationOrderBy, limit: Int): [ComAtprotoRepoStrongRefAggregated!]!
"""Query fm.teal.alpha.feed.play records"""
-
fmTealAlphaFeedPlays(first: Int, after: String, last: Int, before: String, sortBy: [SortField], where: JSON): FmTealAlphaFeedPlayConnection!
+
fmTealAlphaFeedPlays(first: Int, after: String, last: Int, before: String, sortBy: [FmTealAlphaFeedPlaySortFieldInput], where: FmTealAlphaFeedPlayWhereInput): FmTealAlphaFeedPlayConnection!
"""
Aggregated query for fm.teal.alpha.feed.play records with GROUP BY support
"""
-
fmTealAlphaFeedPlaysAggregated(groupBy: [String!]!, where: JSON, orderBy: AggregationOrderBy, limit: Int): [FmTealAlphaFeedPlayAggregated!]!
+
fmTealAlphaFeedPlaysAggregated(groupBy: [FmTealAlphaFeedPlayGroupByFieldInput!], where: FmTealAlphaFeedPlayWhereInput, orderBy: AggregationOrderBy, limit: Int): [FmTealAlphaFeedPlayAggregated!]!
+
+
"""
+
Get logs from the Jetstream real-time indexing service, optionally filtered by slice
+
"""
+
jetstreamLogs(slice: String, limit: Int): [JetstreamLogEntry!]!
+
+
"""Get status of a specific sync job"""
+
syncJob(jobId: String!): SyncJob
+
+
"""Get sync job history for a slice"""
+
syncJobs(slice: String, limit: Int): [SyncJob!]!
+
+
"""Get logs for a specific sync job"""
+
syncJobLogs(jobId: String!, limit: Int): [JetstreamLogEntry!]!
+
+
"""Get summary of repos that would be synced based on collection filters"""
+
getSyncSummary(slice: String!, collections: [String], externalCollections: [String], repos: [String]): SyncSummary!
+
+
"""
+
Get sparkline data for multiple slices showing record indexing activity over time
+
"""
+
sparklines(slices: [String!]!, interval: String, duration: String): [SliceSparkline!]!
+
+
"""
+
Query records across all collections in a slice with filtering and pagination.
+
Provide either sliceUri or both actorHandle and rkey.
+
"""
+
sliceRecords(sliceUri: String, actorHandle: String, rkey: String, first: Int, after: String, where: SliceRecordsWhereInput): SliceRecordsConnection!
+
+
"""Get all OAuth clients for a slice"""
+
oauthClients(slice: String): [OAuthClient!]!
+
}
+
+
type SliceRecord {
+
uri: String!
+
cid: String!
+
did: String!
+
collection: String!
+
value: String!
+
indexedAt: String!
+
}
+
+
type SliceRecordEdge {
+
node: SliceRecord!
+
cursor: String!
+
}
+
+
type SliceRecordsConnection {
+
totalCount: Int!
+
edges: [SliceRecordEdge!]!
+
pageInfo: PageInfo!
+
}
+
+
input SliceRecordsWhereInput {
+
collection: StringFilter
+
did: StringFilter
+
uri: StringFilter
+
cid: StringFilter
+
indexedAt: DateTimeFilter
+
json: StringFilter
+
or: [SliceRecordsWhereInput]
+
}
+
+
type SliceSparkline {
+
sliceUri: String!
+
points: [SparklinePoint!]!
}
enum SortDirection {
···
desc
}
-
input SortField {
-
field: String!
-
direction: SortDirection!
+
type SparklinePoint {
+
timestamp: String!
+
count: Int!
+
}
+
+
type StartSyncOutput {
+
jobId: String!
+
message: String!
+
}
+
+
input StringFilter {
+
eq: String
+
in: [String]
+
contains: String
+
fuzzy: String
+
gt: String
+
gte: String
+
lt: String
+
lte: String
+
}
+
+
type Subscription {
+
"""Subscribe to app.bsky.feed.postgate record creation events"""
+
appBskyFeedPostgateCreated: AppBskyFeedPostgate!
+
+
"""Subscribe to app.bsky.feed.postgate record update events"""
+
appBskyFeedPostgateUpdated: AppBskyFeedPostgate!
+
+
"""
+
Subscribe to app.bsky.feed.postgate record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyFeedPostgateDeleted: String!
+
+
"""Subscribe to app.bsky.feed.threadgate record creation events"""
+
appBskyFeedThreadgateCreated: AppBskyFeedThreadgate!
+
+
"""Subscribe to app.bsky.feed.threadgate record update events"""
+
appBskyFeedThreadgateUpdated: AppBskyFeedThreadgate!
+
+
"""
+
Subscribe to app.bsky.feed.threadgate record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyFeedThreadgateDeleted: String!
+
+
"""Subscribe to app.bsky.actor.profile record creation events"""
+
appBskyActorProfileCreated: AppBskyActorProfile!
+
+
"""Subscribe to app.bsky.actor.profile record update events"""
+
appBskyActorProfileUpdated: AppBskyActorProfile!
+
+
"""
+
Subscribe to app.bsky.actor.profile record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyActorProfileDeleted: String!
+
+
"""Subscribe to fm.teal.alpha.feed.play record creation events"""
+
fmTealAlphaFeedPlayCreated: FmTealAlphaFeedPlay!
+
+
"""Subscribe to fm.teal.alpha.feed.play record update events"""
+
fmTealAlphaFeedPlayUpdated: FmTealAlphaFeedPlay!
+
+
"""
+
Subscribe to fm.teal.alpha.feed.play record deletion events. Returns the URI of deleted records.
+
"""
+
fmTealAlphaFeedPlayDeleted: String!
+
+
"""Subscribe to new Jetstream log entries, optionally filtered by slice"""
+
jetstreamLogsCreated(slice: String): JetstreamLogEntry!
+
+
"""Subscribe to sync job status updates"""
+
syncJobUpdated(jobId: String, slice: String): SyncJob!
+
}
+
+
type SyncJob {
+
id: ID!
+
jobId: String!
+
sliceUri: String!
+
status: String!
+
createdAt: String!
+
startedAt: String
+
completedAt: String
+
result: SyncJobResult
+
error: String
+
retryCount: Int!
+
}
+
+
type SyncJobResult {
+
success: Boolean!
+
totalRecords: Int!
+
collectionsSynced: [String!]!
+
reposProcessed: Int!
+
message: String!
}
type SyncResult {
···
message: String!
}
+
type SyncSummary {
+
totalRepos: Int!
+
cappedRepos: Int!
+
wouldBeCapped: Boolean!
+
appliedLimit: Int!
+
collectionsSummary: [CollectionSummary!]!
+
}
+
+4 -1
src/AlbumItem.tsx
···
import AlbumArt from "./AlbumArt";
+
import MusicBrainzLink from "./MusicBrainzLink";
interface Artist {
artistName: string;
···
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-zinc-100 truncate">
-
{releaseName}
+
<MusicBrainzLink releaseMbId={releaseMbId}>
+
{releaseName}
+
</MusicBrainzLink>
</h3>
<p className="text-xs text-zinc-500 truncate">{artistNames}</p>
</div>
+97 -7
src/App.tsx
···
-
import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay";
-
import { useEffect, useRef } from "react";
+
import {
+
graphql,
+
useLazyLoadQuery,
+
usePaginationFragment,
+
useSubscription,
+
} from "react-relay";
+
import { useEffect, useRef, useMemo } from "react";
import type { AppQuery } from "./__generated__/AppQuery.graphql";
import type { App_plays$key } from "./__generated__/App_plays.graphql";
+
import type { AppSubscription } from "./__generated__/AppSubscription.graphql";
import TrackItem from "./TrackItem";
import Layout from "./Layout";
+
import ScrobbleChart from "./ScrobbleChart";
+
import {
+
ConnectionHandler,
+
type GraphQLSubscriptionConfig,
+
} from "relay-runtime";
export default function App() {
+
const queryVariables = useMemo(() => {
+
// Round to start of day to keep timestamp stable
+
const now = new Date();
+
now.setHours(0, 0, 0, 0);
+
const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
+
+
return {
+
chartWhere: {
+
playedTime: {
+
gte: ninetyDaysAgo.toISOString(),
+
},
+
},
+
};
+
}, []);
+
const queryData = useLazyLoadQuery<AppQuery>(
graphql`
-
query AppQuery {
+
query AppQuery($chartWhere: FmTealAlphaFeedPlayWhereInput!) {
...App_plays
+
...ScrobbleChart_data
}
`,
-
{}
+
queryVariables
);
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
···
fmTealAlphaFeedPlays(
first: $count
after: $cursor
-
sortBy: [{ field: "playedTime", direction: desc }]
+
sortBy: [{ field: playedTime, direction: desc }]
) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) {
totalCount
edges {
···
const loadMoreRef = useRef<HTMLDivElement>(null);
+
// Subscribe to new plays
+
const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = {
+
subscription: graphql`
+
subscription AppSubscription {
+
fmTealAlphaFeedPlayCreated {
+
uri
+
playedTime
+
...TrackItem_play
+
}
+
}
+
`,
+
variables: {},
+
updater: (store) => {
+
const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated");
+
if (!newPlay) return;
+
+
// Only add plays from the last 24 hours
+
const playedTime = newPlay.getValue("playedTime") as string | null;
+
if (!playedTime) return;
+
+
const playDate = new Date(playedTime);
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
+
+
if (playDate < cutoff) {
+
// Play is too old, don't add it to the feed
+
return;
+
}
+
+
const root = store.getRoot();
+
const connection = ConnectionHandler.getConnection(
+
root,
+
"App_fmTealAlphaFeedPlays",
+
{ sortBy: [{ field: "playedTime", direction: "desc" }] }
+
);
+
+
if (!connection) return;
+
+
const edge = ConnectionHandler.createEdge(
+
store,
+
connection,
+
newPlay,
+
"FmTealAlphaFeedPlayEdge"
+
);
+
+
ConnectionHandler.insertEdgeBefore(connection, edge);
+
+
// Update totalCount
+
const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", {
+
sortBy: [{ field: "playedTime", direction: "desc" }],
+
});
+
if (totalCountRecord) {
+
const currentCount = totalCountRecord.getValue("totalCount") as number;
+
if (typeof currentCount === "number") {
+
totalCountRecord.setValue(currentCount + 1, "totalCount");
+
}
+
}
+
},
+
};
+
+
useSubscription(subscriptionConfig);
+
useEffect(() => {
window.scrollTo(0, 0);
}, []);
···
});
return (
-
<Layout>
+
<Layout headerChart={<ScrobbleChart queryRef={queryData} />}>
<div className="mb-8">
<p className="text-xs text-zinc-500 uppercase tracking-wider">
{data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles
···
{hasNext && (
<div ref={loadMoreRef} className="py-12 text-center">
{isLoadingNext ? (
-
<p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
+
<p className="text-xs text-zinc-600 uppercase tracking-wider">
+
Loading...
+
</p>
) : (
<p className="text-xs text-zinc-700 uppercase tracking-wider">ยท</p>
)}
+61
src/ArtistItem.tsx
···
+
interface Artist {
+
artistName: string;
+
}
+
+
interface ArtistItemProps {
+
artists: string | null | undefined;
+
count: number;
+
rank: number;
+
maxCount: number;
+
}
+
+
export default function ArtistItem({
+
artists,
+
count,
+
rank,
+
maxCount,
+
}: ArtistItemProps) {
+
const barWidth = maxCount > 0 ? (count / maxCount) * 100 : 0;
+
+
// Parse artists JSON
+
let artistNames = "Unknown Artist";
+
if (artists) {
+
try {
+
const parsed = typeof artists === 'string' ? JSON.parse(artists) : artists;
+
if (Array.isArray(parsed)) {
+
artistNames = parsed.map((a: Artist) => a.artistName).join(", ");
+
} else if (typeof parsed === 'string') {
+
artistNames = parsed;
+
}
+
} catch (e) {
+
console.log('Failed to parse artists:', artists, e);
+
artistNames = String(artists);
+
}
+
}
+
+
return (
+
<div className="group py-3 px-4 hover:bg-zinc-900/50 transition-colors relative overflow-hidden">
+
<div
+
className="absolute inset-y-0 left-0 bg-violet-500/10 transition-all"
+
style={{ width: `${barWidth}%` }}
+
/>
+
<div className="flex items-center gap-4 relative">
+
<div className="text-xs text-zinc-600 w-8 text-right flex-shrink-0 font-medium">
+
{rank}
+
</div>
+
+
<div className="flex-1 min-w-0">
+
<h3 className="text-sm font-medium text-zinc-100 truncate">
+
{artistNames}
+
</h3>
+
</div>
+
+
<div className="text-right flex-shrink-0">
+
<p className="text-xs text-zinc-400 font-medium">
+
{count.toLocaleString()}
+
</p>
+
</div>
+
</div>
+
</div>
+
);
+
}
+122 -20
src/Layout.tsx
···
interface LayoutProps {
children: React.ReactNode;
+
headerChart?: React.ReactNode;
}
-
export default function Layout({ children }: LayoutProps) {
+
export default function Layout({ children, headerChart }: LayoutProps) {
const location = useLocation();
+
const isTracksPage = location.pathname.startsWith("/tracks");
+
const isAlbumsPage = location.pathname.startsWith("/albums");
return (
<div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
<div className="max-w-4xl mx-auto px-6 py-12">
-
<div className="mb-12 flex items-end justify-between border-b border-zinc-800 pb-6">
-
<div>
-
<h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">Listening History</h1>
-
<p className="text-xs text-zinc-600 mt-1">fm.teal.alpha.feed.play</p>
+
<div className="mb-4 border-b border-zinc-800 pb-4 relative">
+
{headerChart && (
+
<div className="absolute inset-0 pointer-events-none opacity-40">
+
{headerChart}
+
</div>
+
)}
+
<div className="flex items-end justify-between relative">
+
<div>
+
<h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">Listening History</h1>
+
<p className="text-xs text-zinc-600 mt-1">fm.teal.alpha.feed.play</p>
+
</div>
+
+
<div className="flex gap-4 text-xs">
+
<Link
+
to="/"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/"
+
? "text-zinc-400"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Recent
+
</Link>
+
<Link
+
to="/tracks"
+
className={`px-2 py-1 transition-colors ${
+
isTracksPage
+
? "text-zinc-400"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Top Tracks
+
</Link>
+
<Link
+
to="/albums"
+
className={`px-2 py-1 transition-colors ${
+
isAlbumsPage
+
? "text-zinc-400"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Top Albums
+
</Link>
+
</div>
</div>
+
</div>
-
<div className="flex gap-4 text-xs">
+
{isTracksPage && (
+
<div className="flex gap-3 text-xs mb-8 pb-4 border-b border-zinc-800">
<Link
-
to="/"
+
to="/tracks"
className={`px-2 py-1 transition-colors ${
-
location.pathname === "/"
-
? "text-zinc-400"
-
: "text-zinc-500 hover:text-zinc-300"
+
location.pathname === "/tracks"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
}`}
>
-
Recent
+
All Time
</Link>
<Link
-
to="/tracks"
+
to="/tracks/daily"
className={`px-2 py-1 transition-colors ${
-
location.pathname === "/tracks"
-
? "text-zinc-400"
-
: "text-zinc-500 hover:text-zinc-300"
+
location.pathname === "/tracks/daily"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
}`}
>
-
Top Tracks
+
Daily
</Link>
<Link
+
to="/tracks/weekly"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/tracks/weekly"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
+
}`}
+
>
+
Weekly
+
</Link>
+
<Link
+
to="/tracks/monthly"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/tracks/monthly"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
+
}`}
+
>
+
Monthly
+
</Link>
+
</div>
+
)}
+
+
{isAlbumsPage && (
+
<div className="flex gap-3 text-xs mb-8 pb-4 border-b border-zinc-800">
+
<Link
to="/albums"
className={`px-2 py-1 transition-colors ${
location.pathname === "/albums"
-
? "text-zinc-400"
-
: "text-zinc-500 hover:text-zinc-300"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
+
}`}
+
>
+
All Time
+
</Link>
+
<Link
+
to="/albums/daily"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/albums/daily"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
+
}`}
+
>
+
Daily
+
</Link>
+
<Link
+
to="/albums/weekly"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/albums/weekly"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
+
}`}
+
>
+
Weekly
+
</Link>
+
<Link
+
to="/albums/monthly"
+
className={`px-2 py-1 transition-colors ${
+
location.pathname === "/albums/monthly"
+
? "text-zinc-300"
+
: "text-zinc-600 hover:text-zinc-400"
}`}
>
-
Top Albums
+
Monthly
</Link>
</div>
-
</div>
+
)}
+
+
{!isTracksPage && !isAlbumsPage && <div className="mb-8"></div>}
{children}
</div>
+24
src/MusicBrainzLink.tsx
···
+
interface MusicBrainzLinkProps {
+
releaseMbId: string | null | undefined;
+
children: React.ReactNode;
+
}
+
+
export default function MusicBrainzLink({
+
releaseMbId,
+
children,
+
}: MusicBrainzLinkProps) {
+
if (!releaseMbId) {
+
return <>{children}</>;
+
}
+
+
return (
+
<a
+
href={`https://musicbrainz.org/release/${releaseMbId}`}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="hover:text-violet-400 transition-colors"
+
>
+
{children}
+
</a>
+
);
+
}
+78
src/Overall.tsx
···
+
import { NavLink, Outlet, useLocation, useParams } from "react-router-dom";
+
import ProfileLayout from "./ProfileLayout";
+
+
const periods = [
+
{ id: "daily", label: "24 hours" },
+
{ id: "weekly", label: "7 days" },
+
{ id: "monthly", label: "30 days" },
+
{ id: "all", label: "All time" },
+
];
+
+
export default function Overall() {
+
const { handle, period = "all" } = useParams<
+
{ handle: string; period?: string }
+
>();
+
const location = useLocation();
+
+
const activeTab = location.pathname.split("/")[4] || "artists";
+
+
return (
+
//@ts-expect-error: idk
+
<ProfileLayout handle={handle!}>
+
<div className="flex items-center justify-between mb-8">
+
<div className="flex items-center border-b border-zinc-800">
+
<NavLink
+
to={`/profile/${handle}/overall/artists/${period}`}
+
className={({ isActive }) =>
+
`px-4 py-2 text-xs uppercase tracking-wider ${
+
isActive
+
? "text-zinc-100 border-b-2 border-zinc-100"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Artists
+
</NavLink>
+
<NavLink
+
to={`/profile/${handle}/overall/albums/${period}`}
+
className={({ isActive }) =>
+
`px-4 py-2 text-xs uppercase tracking-wider ${
+
isActive
+
? "text-zinc-100 border-b-2 border-zinc-100"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Albums
+
</NavLink>
+
<NavLink
+
to={`/profile/${handle}/overall/tracks/${period}`}
+
className={({ isActive }) =>
+
`px-4 py-2 text-xs uppercase tracking-wider ${
+
isActive
+
? "text-zinc-100 border-b-2 border-zinc-100"
+
: "text-zinc-500 hover:text-zinc-300"
+
}`}
+
>
+
Tracks
+
</NavLink>
+
</div>
+
<div className="flex items-center gap-2">
+
{periods.map((p) => (
+
<NavLink
+
key={p.id}
+
to={`/profile/${handle}/overall/${activeTab}/${p.id}`}
+
className={() =>
+
`px-3 py-1 text-xs rounded-md ${
+
period === p.id
+
? "bg-zinc-800 text-zinc-100"
+
: "text-zinc-500 hover:bg-zinc-800/50"
+
}`}
+
>
+
{p.label}
+
</NavLink>
+
))}
+
</div>
+
</div>
+
<Outlet />
+
</ProfileLayout>
+
);
+
}
+51 -68
src/Profile.tsx
···
import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay";
-
import { useParams, Link } from "react-router-dom";
-
import { useEffect, useRef } from "react";
+
import { useParams } from "react-router-dom";
+
import { useEffect, useMemo, useRef } from "react";
import type { ProfileQuery as ProfileQueryType } from "./__generated__/ProfileQuery.graphql";
import type { Profile_plays$key } from "./__generated__/Profile_plays.graphql";
import TrackItem from "./TrackItem";
+
import ProfileLayout from "./ProfileLayout";
export default function Profile() {
const { handle } = useParams<{ handle: string }>();
+
const queryVariables = useMemo(() => {
+
return {
+
where: { actorHandle: { eq: handle } },
+
};
+
}, [handle]);
+
const queryData = useLazyLoadQuery<ProfileQueryType>(
graphql`
-
query ProfileQuery($where: JSON!) {
+
query ProfileQuery($where: FmTealAlphaFeedPlayWhereInput!) {
...Profile_plays @arguments(where: $where)
}
`,
-
{
-
where: { actorHandle: { eq: handle } },
-
}
+
queryVariables,
);
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
···
@argumentDefinitions(
cursor: { type: "String" }
count: { type: "Int", defaultValue: 20 }
-
where: { type: "JSON!" }
+
where: { type: "FmTealAlphaFeedPlayWhereInput!" }
) {
fmTealAlphaFeedPlays(
first: $count
after: $cursor
-
sortBy: [{ field: "playedTime", direction: desc }]
+
sortBy: [{ field: playedTime, direction: desc }]
where: $where
)
@connection(
···
edges {
node {
...TrackItem_play
-
actorHandle
-
appBskyActorProfile {
-
displayName
-
description
-
avatar {
-
url(preset: "avatar")
-
}
-
}
}
}
}
}
`,
-
queryData
+
queryData,
);
const loadMoreRef = useRef<HTMLDivElement>(null);
-
const plays = data?.fmTealAlphaFeedPlays?.edges?.map((edge) => edge.node).filter((n) => n != null) || [];
-
const profile = plays?.[0]?.appBskyActorProfile;
+
const plays = useMemo(
+
() =>
+
data?.fmTealAlphaFeedPlays?.edges?.map((edge) => edge.node).filter((n) =>
+
n != null
+
) || [],
+
[data?.fmTealAlphaFeedPlays?.edges],
+
);
useEffect(() => {
window.scrollTo(0, 0);
···
loadNext(20);
}
},
-
{ threshold: 0.1 }
+
{ threshold: 0.1 },
);
observer.observe(loadMoreRef.current);
···
}, [hasNext, isLoadingNext, loadNext]);
return (
-
<div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
-
<div className="max-w-4xl mx-auto px-6 py-12">
-
<Link
-
to="/"
-
className="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors inline-block mb-8"
-
>
-
โ† Back
-
</Link>
+
//@ts-expect-error: idk
+
<ProfileLayout handle={handle!}>
+
<div className="mb-8">
+
<p className="text-xs text-zinc-500 uppercase tracking-wider">
+
{(data?.fmTealAlphaFeedPlays?.totalCount ?? 0).toLocaleString()}{" "}
+
scrobbles
+
</p>
+
</div>
-
<div className="mb-12 flex items-start gap-6 border-b border-zinc-800 pb-6">
-
{profile?.avatar?.url && (
-
<img
-
src={profile.avatar.url}
-
alt={profile.displayName ?? handle ?? "User"}
-
className="w-16 h-16 flex-shrink-0 object-cover"
-
/>
-
)}
-
<div className="flex-1">
-
<h1 className="text-lg font-medium mb-1 text-zinc-100">
-
{profile?.displayName ?? handle}
-
</h1>
-
<p className="text-xs text-zinc-500 mb-2">@{handle}</p>
-
{profile?.description && (
-
<p className="text-xs text-zinc-400">{profile.description}</p>
-
)}
-
</div>
-
</div>
-
-
<div className="mb-8">
-
<h2 className="text-sm font-medium uppercase tracking-wider text-zinc-400 mb-2">Recent Tracks</h2>
-
<p className="text-xs text-zinc-500 uppercase tracking-wider">
-
{(data?.fmTealAlphaFeedPlays?.totalCount ?? 0).toLocaleString()} scrobbles
-
</p>
-
</div>
-
-
<div className="space-y-1">
-
{plays && plays.length > 0 ? (
+
<div className="space-y-1">
+
{plays && plays.length > 0
+
? (
plays.map((play, index) => <TrackItem key={index} play={play} />)
-
) : (
+
)
+
: (
<p className="text-zinc-600 text-center py-8 text-xs uppercase tracking-wider">
No tracks found for this user
</p>
)}
-
</div>
+
</div>
-
{hasNext && (
-
<div ref={loadMoreRef} className="py-12 text-center">
-
{isLoadingNext ? (
-
<p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
-
) : (
-
<p className="text-xs text-zinc-700 uppercase tracking-wider">ยท</p>
+
{hasNext && (
+
<div ref={loadMoreRef} className="py-12 text-center">
+
{isLoadingNext
+
? (
+
<p className="text-xs text-zinc-600 uppercase tracking-wider">
+
Loading...
+
</p>
+
)
+
: (
+
<p className="text-xs text-zinc-700 uppercase tracking-wider">
+
ยท
+
</p>
)}
-
</div>
-
)}
-
</div>
-
</div>
+
</div>
+
)}
+
</ProfileLayout>
);
}
+117
src/ProfileLayout.tsx
···
+
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { Link, NavLink, useParams } from "react-router-dom";
+
import { useMemo, type PropsWithChildren } from "react";
+
import type { ProfileLayoutQuery as ProfileLayoutQueryType } from "./__generated__/ProfileLayoutQuery.graphql";
+
import ScrobbleChart from "./ScrobbleChart";
+
+
export default function ProfileLayout({ children }: PropsWithChildren) {
+
const { handle } = useParams<{ handle: string }>();
+
+
const queryVariables = useMemo(() => {
+
// Round to start of day to keep timestamp stable
+
const now = new Date();
+
now.setHours(0, 0, 0, 0);
+
const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
+
+
return {
+
where: { actorHandle: { eq: handle } },
+
chartWhere: {
+
actorHandle: { eq: handle },
+
playedTime: {
+
gte: ninetyDaysAgo.toISOString(),
+
},
+
},
+
};
+
}, [handle]);
+
+
const queryData = useLazyLoadQuery<ProfileLayoutQueryType>(
+
graphql`
+
query ProfileLayoutQuery($where: FmTealAlphaFeedPlayWhereInput!, $chartWhere: FmTealAlphaFeedPlayWhereInput!) {
+
...ScrobbleChart_data
+
fmTealAlphaFeedPlays(
+
first: 1
+
sortBy: [{ field: playedTime, direction: desc }]
+
where: $where
+
) {
+
edges {
+
node {
+
actorHandle
+
appBskyActorProfile {
+
displayName
+
description
+
avatar {
+
url(preset: "avatar")
+
}
+
}
+
}
+
}
+
}
+
}
+
`,
+
queryVariables
+
);
+
+
const profile = queryData?.fmTealAlphaFeedPlays?.edges?.[0]?.node?.appBskyActorProfile;
+
+
return (
+
<div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
+
<div className="max-w-4xl mx-auto px-6 py-12">
+
<Link
+
to="/"
+
className="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors inline-block mb-8"
+
>
+
โ† Back
+
</Link>
+
+
<div className="mb-12 border-b border-zinc-800 pb-6 relative">
+
<div className="absolute inset-0 pointer-events-none opacity-40">
+
<ScrobbleChart queryRef={queryData} />
+
</div>
+
<div className="relative flex items-start gap-6">
+
{profile?.avatar?.url && (
+
<img
+
src={profile.avatar.url}
+
alt={profile.displayName ?? handle ?? "User"}
+
className="w-16 h-16 flex-shrink-0 object-cover"
+
/>
+
)}
+
<div className="flex-1">
+
<h1 className="text-lg font-medium mb-1 text-zinc-100">
+
{profile?.displayName ?? handle}
+
</h1>
+
<p className="text-xs text-zinc-500 mb-2">@{handle}</p>
+
{profile?.description && (
+
<p className="text-xs text-zinc-400">{profile.description}</p>
+
)}
+
</div>
+
</div>
+
</div>
+
+
<div className="flex items-center border-b border-zinc-800 mb-8">
+
<NavLink
+
to={`/profile/${handle}/scrobbles`}
+
className={({ isActive }) =>
+
`px-4 py-2 text-xs uppercase tracking-wider ${
+
isActive ? "text-zinc-100 border-b-2 border-zinc-100" : "text-zinc-500 hover:text-zinc-300"
+
}`
+
}
+
>
+
Scrobbles
+
</NavLink>
+
<NavLink
+
to={`/profile/${handle}/overall`}
+
className={({ isActive }) =>
+
`px-4 py-2 text-xs uppercase tracking-wider ${
+
isActive ? "text-zinc-100 border-b-2 border-zinc-100" : "text-zinc-500 hover:text-zinc-300"
+
}`
+
}
+
>
+
Overall
+
</NavLink>
+
</div>
+
+
{children}
+
</div>
+
</div>
+
);
+
}
+82
src/ProfileTopAlbums.tsx
···
+
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
+
import type { ProfileTopAlbumsQuery } from "./__generated__/ProfileTopAlbumsQuery.graphql";
+
import AlbumItem from "./AlbumItem";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
+
import { useMemo } from "react";
+
+
export default function ProfileTopAlbums() {
+
const { handle, period } = useParams<{ handle: string; period?: string }>();
+
const dateRangeVariables = useDateRangeFilter(period);
+
+
const queryVariables = useMemo(() => {
+
return {
+
where: {
+
...dateRangeVariables.where,
+
actorHandle: { eq: handle },
+
},
+
};
+
}, [handle, dateRangeVariables]);
+
+
const data = useLazyLoadQuery<ProfileTopAlbumsQuery>(
+
graphql`
+
query ProfileTopAlbumsQuery($where: FmTealAlphaFeedPlayWhereInput) {
+
fmTealAlphaFeedPlaysAggregated(
+
groupBy: [{ field: releaseMbId }, { field: releaseName }, { field: artists }]
+
orderBy: { count: desc }
+
limit: 50
+
where: $where
+
) {
+
releaseMbId
+
releaseName
+
artists
+
count
+
}
+
}
+
`,
+
queryVariables,
+
{ fetchKey: `${handle}-${period || "all"}`, fetchPolicy: "store-or-network" }
+
);
+
+
const albums = [...(data.fmTealAlphaFeedPlaysAggregated || [])];
+
+
// Deduplicate by release name, keeping the one with highest count
+
// Prefer entries with artist data
+
const seenNames = new Set<string>();
+
const dedupedAlbums = albums
+
.sort((a, b) => {
+
// First sort by count (already sorted from query)
+
if (b.count !== a.count) return b.count - a.count;
+
// Then prefer entries with artists data
+
if (a.artists && !b.artists) return -1;
+
if (!a.artists && b.artists) return 1;
+
return 0;
+
})
+
.filter((album) => {
+
const name = album.releaseName || "Unknown Album";
+
if (seenNames.has(name)) {
+
return false;
+
}
+
seenNames.add(name);
+
return true;
+
})
+
.slice(0, 10);
+
+
const maxCount = dedupedAlbums.length > 0 ? dedupedAlbums[0].count : 0;
+
+
return (
+
<div className="space-y-1">
+
{dedupedAlbums.map((album, index) => (
+
<AlbumItem
+
key={album.releaseMbId || index}
+
releaseName={album.releaseName || "Unknown Album"}
+
releaseMbId={album.releaseMbId}
+
artists={album.artists}
+
count={album.count}
+
rank={index + 1}
+
maxCount={maxCount}
+
/>
+
))}
+
</div>
+
);
+
}
+89
src/ProfileTopArtists.tsx
···
+
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
+
import type { ProfileTopArtistsQuery } from "./__generated__/ProfileTopArtistsQuery.graphql";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
+
import ArtistItem from "./ArtistItem";
+
import { useMemo } from "react";
+
+
export default function ProfileTopArtists() {
+
const { handle, period } = useParams<{ handle: string; period?: string }>();
+
const dateRangeVariables = useDateRangeFilter(period);
+
+
const queryVariables = useMemo(() => {
+
return {
+
where: {
+
...dateRangeVariables.where,
+
actorHandle: { eq: handle },
+
},
+
};
+
}, [handle, dateRangeVariables]);
+
+
const data = useLazyLoadQuery<ProfileTopArtistsQuery>(
+
graphql`
+
query ProfileTopArtistsQuery($where: FmTealAlphaFeedPlayWhereInput) {
+
fmTealAlphaFeedPlaysAggregated(
+
groupBy: [{ field: artists }]
+
orderBy: { count: desc }
+
limit: 50
+
where: $where
+
) {
+
artists
+
count
+
}
+
}
+
`,
+
queryVariables,
+
{ fetchKey: `${handle}-${period || "all"}`, fetchPolicy: "store-or-network" }
+
);
+
+
const processedArtists = useMemo(() => {
+
const artistCounts: { [key: string]: number } = {};
+
+
(data.fmTealAlphaFeedPlaysAggregated || []).forEach((row) => {
+
if (!row.artists) return;
+
+
let names: string[] = [];
+
+
try {
+
const parsed = typeof row.artists === 'string' ? JSON.parse(row.artists) : row.artists;
+
+
if (Array.isArray(parsed)) {
+
names = parsed.map((a: { artistName: string }) => a.artistName.trim());
+
} else if (typeof parsed === 'string') {
+
names = parsed.split(',').map(s => s.trim());
+
}
+
} catch (e) {
+
if (typeof row.artists === 'string') {
+
names = row.artists.split(',').map(s => s.trim());
+
}
+
}
+
+
names.forEach(name => {
+
if (name) {
+
artistCounts[name] = (artistCounts[name] || 0) + row.count;
+
}
+
});
+
});
+
+
return Object.entries(artistCounts)
+
.map(([name, count]) => ({ artists: name, count }))
+
.sort((a, b) => b.count - a.count)
+
.slice(0, 10);
+
}, [data.fmTealAlphaFeedPlaysAggregated]);
+
+
const maxCount = processedArtists.length > 0 ? processedArtists[0].count : 0;
+
+
return (
+
<div className="space-y-1">
+
{processedArtists.map((artist, index) => (
+
<ArtistItem
+
key={`${artist.artists}-${index}`}
+
artists={artist.artists || "Unknown Artist"}
+
count={artist.count}
+
rank={index + 1}
+
maxCount={maxCount}
+
/>
+
))}
+
</div>
+
);
+
}
+59
src/ProfileTopTracks.tsx
···
+
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
+
import type { ProfileTopTracksQuery } from "./__generated__/ProfileTopTracksQuery.graphql";
+
import TopTrackItem from "./TopTrackItem";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
+
import { useMemo } from "react";
+
+
export default function ProfileTopTracks() {
+
const { handle, period } = useParams<{ handle: string; period?: string }>();
+
const dateRangeVariables = useDateRangeFilter(period);
+
+
const queryVariables = useMemo(() => {
+
return {
+
where: {
+
...dateRangeVariables.where,
+
actorHandle: { eq: handle },
+
},
+
};
+
}, [handle, dateRangeVariables]);
+
+
const data = useLazyLoadQuery<ProfileTopTracksQuery>(
+
graphql`
+
query ProfileTopTracksQuery($where: FmTealAlphaFeedPlayWhereInput) {
+
fmTealAlphaFeedPlaysAggregated(
+
groupBy: [{ field: trackName }, { field: releaseMbId }, { field: artists }]
+
orderBy: { count: desc }
+
limit: 10
+
where: $where
+
) {
+
trackName
+
releaseMbId
+
artists
+
count
+
}
+
}
+
`,
+
queryVariables,
+
{ fetchKey: `${handle}-${period || "all"}`, fetchPolicy: "store-or-network" }
+
);
+
+
const tracks = data.fmTealAlphaFeedPlaysAggregated || [];
+
const maxCount = tracks.length > 0 ? tracks[0].count : 0;
+
+
return (
+
<div className="space-y-1">
+
{tracks.map((track, index) => (
+
<TopTrackItem
+
key={`${track.trackName}-${index}`}
+
trackName={track.trackName || "Unknown Track"}
+
releaseMbId={track.releaseMbId}
+
artists={track.artists || "Unknown Artist"}
+
count={track.count}
+
rank={index + 1}
+
maxCount={maxCount}
+
/>
+
))}
+
</div>
+
);
+
}
+113
src/ScrobbleChart.tsx
···
+
import { graphql, useFragment } from "react-relay";
+
import { useMemo } from "react";
+
import type { ScrobbleChart_data$key } from "./__generated__/ScrobbleChart_data.graphql";
+
+
interface ScrobbleChartProps {
+
queryRef: ScrobbleChart_data$key;
+
}
+
+
export default function ScrobbleChart({ queryRef }: ScrobbleChartProps) {
+
const data = useFragment(
+
graphql`
+
fragment ScrobbleChart_data on Query {
+
chartData: fmTealAlphaFeedPlaysAggregated(
+
groupBy: [{ field: playedTime, interval: day }]
+
where: $chartWhere
+
limit: 90
+
) {
+
playedTime
+
count
+
}
+
}
+
`,
+
queryRef
+
);
+
+
const chartData = useMemo(() => {
+
if (!data?.chartData) return [];
+
+
// Convert aggregated data to chart format
+
const aggregated = data.chartData.map((item) => {
+
// playedTime comes back as '2025-08-03 00:00:00', extract just the date part
+
const date = item.playedTime ? item.playedTime.split(' ')[0] : "";
+
return {
+
date,
+
count: item.count,
+
};
+
}).sort((a, b) => a.date.localeCompare(b.date));
+
+
// Fill in missing days with zero counts
+
const now = new Date();
+
now.setHours(0, 0, 0, 0);
+
const filledData = [];
+
+
for (let i = 89; i >= 0; i--) {
+
const date = new Date(now);
+
date.setDate(date.getDate() - i);
+
const dateStr = date.toISOString().split("T")[0];
+
+
const existing = aggregated.find((d) => d.date === dateStr);
+
filledData.push({
+
date: dateStr,
+
count: existing ? existing.count : 0,
+
});
+
}
+
+
return filledData;
+
}, [data?.chartData]);
+
+
if (!chartData || chartData.length === 0) return null;
+
+
const width = 1000;
+
const height = 100;
+
const padding = { top: 0, right: 0, bottom: 0, left: 0 };
+
const chartWidth = width - padding.left - padding.right;
+
const chartHeight = height - padding.top - padding.bottom;
+
+
const maxCount = Math.max(...chartData.map((d) => d.count));
+
const minCount = Math.min(...chartData.map((d) => d.count));
+
const range = maxCount - minCount || 1;
+
+
// Generate points for the line
+
const points = chartData.map((d, i) => {
+
const x = padding.left + (i / (chartData.length - 1)) * chartWidth;
+
const y = padding.top + chartHeight - ((d.count - minCount) / range) * chartHeight;
+
return `${x},${y}`;
+
}).join(" ");
+
+
// Generate area path
+
const areaPoints = [
+
`${padding.left},${padding.top + chartHeight}`,
+
...chartData.map((d, i) => {
+
const x = padding.left + (i / (chartData.length - 1)) * chartWidth;
+
const y = padding.top + chartHeight - ((d.count - minCount) / range) * chartHeight;
+
return `${x},${y}`;
+
}),
+
`${padding.left + chartWidth},${padding.top + chartHeight}`,
+
].join(" ");
+
+
return (
+
<svg
+
viewBox={`0 0 ${width} ${height}`}
+
className="w-full h-full"
+
preserveAspectRatio="none"
+
>
+
{/* Area fill */}
+
<polygon
+
points={areaPoints}
+
fill="rgb(139 92 246 / 0.1)"
+
stroke="none"
+
/>
+
+
{/* Line */}
+
<polyline
+
points={points}
+
fill="none"
+
stroke="rgb(139 92 246)"
+
strokeWidth="1.5"
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
/>
+
</svg>
+
);
+
}
+10 -3
src/TopAlbums.tsx
···
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
import type { TopAlbumsQuery } from "./__generated__/TopAlbumsQuery.graphql";
import AlbumItem from "./AlbumItem";
import Layout from "./Layout";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
export default function TopAlbums() {
+
const { period } = useParams<{ period?: string }>();
+
const queryVariables = useDateRangeFilter(period);
+
const data = useLazyLoadQuery<TopAlbumsQuery>(
graphql`
-
query TopAlbumsQuery {
+
query TopAlbumsQuery($where: FmTealAlphaFeedPlayWhereInput) {
fmTealAlphaFeedPlaysAggregated(
-
groupBy: ["releaseMbId", "releaseName", "artists"]
+
groupBy: [{ field: releaseMbId }, { field: releaseName }, { field: artists }]
orderBy: { count: desc }
limit: 100
+
where: $where
) {
releaseMbId
releaseName
···
}
}
`,
-
{}
+
queryVariables,
+
{ fetchKey: period || "all", fetchPolicy: "store-or-network" }
);
const albums = [...(data.fmTealAlphaFeedPlaysAggregated || [])];
+48
src/TopArtists.tsx
···
+
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
+
import type { TopArtistsQuery } from "./__generated__/TopArtistsQuery.graphql";
+
import Layout from "./Layout";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
+
import ArtistItem from "./ArtistItem";
+
+
export default function TopArtists() {
+
const { period } = useParams<{ period?: string }>();
+
const queryVariables = useDateRangeFilter(period);
+
+
const data = useLazyLoadQuery<TopArtistsQuery>(
+
graphql`
+
query TopArtistsQuery($where: FmTealAlphaFeedPlayWhereInput) {
+
fmTealAlphaFeedPlaysAggregated(
+
groupBy: [{ field: artists }]
+
orderBy: { count: desc }
+
limit: 50
+
where: $where
+
) {
+
artists
+
count
+
}
+
}
+
`,
+
queryVariables,
+
{ fetchKey: period || "all", fetchPolicy: "store-or-network" }
+
);
+
+
const artists = data.fmTealAlphaFeedPlaysAggregated || [];
+
const maxCount = artists.length > 0 ? artists[0].count : 0;
+
+
return (
+
<Layout>
+
<div className="space-y-1">
+
{artists.map((artist, index) => (
+
<ArtistItem
+
key={`${artist.artists}-${index}`}
+
artists={artist.artists || "Unknown Artist"}
+
count={artist.count}
+
rank={index + 1}
+
maxCount={maxCount}
+
/>
+
))}
+
</div>
+
</Layout>
+
);
+
}
+4 -1
src/TopTrackItem.tsx
···
import AlbumArt from "./AlbumArt";
+
import MusicBrainzLink from "./MusicBrainzLink";
interface Artist {
artistName: string;
···
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-zinc-100 truncate">
-
{trackName}
+
<MusicBrainzLink releaseMbId={releaseMbId}>
+
{trackName}
+
</MusicBrainzLink>
</h3>
<p className="text-xs text-zinc-500 truncate">{artistNames}</p>
</div>
+10 -3
src/TopTracks.tsx
···
import { graphql, useLazyLoadQuery } from "react-relay";
+
import { useParams } from "react-router-dom";
import type { TopTracksQuery } from "./__generated__/TopTracksQuery.graphql";
import TopTrackItem from "./TopTrackItem";
import Layout from "./Layout";
+
import { useDateRangeFilter } from "./useDateRangeFilter";
export default function TopTracks() {
+
const { period } = useParams<{ period?: string }>();
+
const queryVariables = useDateRangeFilter(period);
+
const data = useLazyLoadQuery<TopTracksQuery>(
graphql`
-
query TopTracksQuery {
+
query TopTracksQuery($where: FmTealAlphaFeedPlayWhereInput) {
fmTealAlphaFeedPlaysAggregated(
-
groupBy: ["trackName", "releaseMbId", "artists"]
+
groupBy: [{ field: trackName }, { field: releaseMbId }, { field: artists }]
orderBy: { count: desc }
limit: 50
+
where: $where
) {
trackName
releaseMbId
···
}
}
`,
-
{}
+
queryVariables,
+
{ fetchKey: period || "all", fetchPolicy: "store-or-network" },
);
const tracks = data.fmTealAlphaFeedPlaysAggregated || [];
+21 -4
src/TrackItem.tsx
···
import { graphql, useFragment } from "react-relay";
import type { TrackItem_play$key } from "./__generated__/TrackItem_play.graphql";
import AlbumArt from "./AlbumArt";
+
import MusicBrainzLink from "./MusicBrainzLink";
interface TrackItemProps {
play: TrackItem_play$key;
···
fragment TrackItem_play on FmTealAlphaFeedPlay {
trackName
playedTime
-
artists
+
artists {
+
artistName
+
artistMbId
+
}
releaseName
releaseMbId
actorHandle
+
musicServiceBaseDomain
appBskyActorProfile {
displayName
}
···
<div className="flex-1 min-w-0 grid grid-cols-2 gap-4">
<div className="min-w-0">
-
<h3 className="text-sm font-medium text-zinc-100 truncate">
-
{data.trackName}
+
<h3 className="text-sm font-medium text-zinc-100 truncate flex items-center gap-2">
+
<span className="truncate">{data.trackName}</span>
+
{data.musicServiceBaseDomain === "nts.live" && (
+
<a
+
href={`https://${data.musicServiceBaseDomain}`}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="text-[10px] px-1.5 py-0.5 bg-violet-500/20 text-violet-400 rounded flex-shrink-0 hover:bg-violet-500/30 transition-colors"
+
>
+
NTS
+
</a>
+
)}
</h3>
<p className="text-xs text-zinc-500 truncate">
{Array.isArray(data.artists)
···
<div className="text-right min-w-0">
<p className="text-xs text-zinc-400 truncate">
-
{data.releaseName}
+
<MusicBrainzLink releaseMbId={data.releaseMbId}>
+
{data.releaseName}
+
</MusicBrainzLink>
</p>
<div className="flex items-center justify-end gap-2 mt-0.5 min-w-0 overflow-hidden">
{data.playedTime && (
+41 -7
src/__generated__/AppPaginationQuery.graphql.ts
···
/**
-
* @generated SignedSource<<cef2df106afea24fa8527f2def8e9991>>
+
* @generated SignedSource<<4144e88e9b03430408917b25e498a033>>
* @lightSyntaxTransform
* @nogrep
*/
···
}
]
}
-
];
+
],
+
v2 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
···
{
"alias": null,
"args": null,
-
"kind": "ScalarField",
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
"storageKey": null
},
{
···
{
"alias": null,
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
"concreteType": "AppBskyActorProfile",
"kind": "LinkedField",
"name": "appBskyActorProfile",
···
"kind": "ScalarField",
"name": "displayName",
"storageKey": null
-
}
+
},
+
(v2/*: any*/)
],
"storageKey": null
},
+
(v2/*: any*/),
{
"alias": null,
"args": null,
···
]
},
"params": {
-
"cacheID": "e115a73de49cf6f84a35f172a7910c5c",
+
"cacheID": "d41913a8ba0cde3255c03b34e36f4baf",
"id": null,
"metadata": {},
"name": "AppPaginationQuery",
"operationKind": "query",
-
"text": "query AppPaginationQuery(\n $count: Int = 20\n $cursor: String\n) {\n ...App_plays_1G22uz\n}\n\nfragment App_plays_1G22uz on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: \"playedTime\", direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n"
+
"text": "query AppPaginationQuery(\n $count: Int = 20\n $cursor: String\n) {\n ...App_plays_1G22uz\n}\n\nfragment App_plays_1G22uz on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: playedTime, direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n artistMbId\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfile {\n displayName\n id\n }\n}\n"
}
};
})();
-
(node as any).hash = "0e4acf96fedae07af90ce6e9e3bf18d6";
+
(node as any).hash = "1e73fa97ccff20071e5a3fba0f00b48c";
export default node;
+161 -22
src/__generated__/AppQuery.graphql.ts
···
/**
-
* @generated SignedSource<<541e6114682aef7988bd233592085337>>
+
* @generated SignedSource<<4eadb1b67651fecb72943df792f91938>>
* @lightSyntaxTransform
* @nogrep
*/
···
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
-
export type AppQuery$variables = Record<PropertyKey, never>;
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type AppQuery$variables = {
+
chartWhere: FmTealAlphaFeedPlayWhereInput;
+
};
export type AppQuery$data = {
-
readonly " $fragmentSpreads": FragmentRefs<"App_plays">;
+
readonly " $fragmentSpreads": FragmentRefs<"App_plays" | "ScrobbleChart_data">;
};
export type AppQuery = {
response: AppQuery$data;
···
const node: ConcreteRequest = (function(){
var v0 = [
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "chartWhere"
+
}
+
],
+
v1 = [
+
{
"kind": "Literal",
"name": "first",
"value": 20
···
}
]
}
-
];
+
],
+
v2 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "playedTime",
+
"storageKey": null
+
},
+
v3 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
return {
"fragment": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AppQuery",
···
"args": null,
"kind": "FragmentSpread",
"name": "App_plays"
+
},
+
{
+
"args": null,
+
"kind": "FragmentSpread",
+
"name": "ScrobbleChart_data"
}
],
"type": "Query",
···
},
"kind": "Request",
"operation": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AppQuery",
"selections": [
{
"alias": null,
-
"args": (v0/*: any*/),
+
"args": (v1/*: any*/),
"concreteType": "FmTealAlphaFeedPlayConnection",
"kind": "LinkedField",
"name": "fmTealAlphaFeedPlays",
···
"name": "node",
"plural": false,
"selections": [
+
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
-
"name": "playedTime",
+
"name": "trackName",
"storageKey": null
},
{
"alias": null,
"args": null,
-
"kind": "ScalarField",
-
"name": "trackName",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
-
"name": "artists",
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
+
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
"storageKey": null
},
{
···
"args": null,
"kind": "ScalarField",
"name": "actorHandle",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
"storageKey": null
},
{
···
"kind": "ScalarField",
"name": "displayName",
"storageKey": null
-
}
+
},
+
(v3/*: any*/)
],
"storageKey": null
},
+
(v3/*: any*/),
{
"alias": null,
"args": null,
···
},
{
"alias": null,
-
"args": (v0/*: any*/),
+
"args": (v1/*: any*/),
"filters": [
"sortBy"
],
···
"key": "App_fmTealAlphaFeedPlays",
"kind": "LinkedHandle",
"name": "fmTealAlphaFeedPlays"
+
},
+
{
+
"alias": "chartData",
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "playedTime",
+
"interval": "day"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 90
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "chartWhere"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
(v2/*: any*/),
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
}
]
},
"params": {
-
"cacheID": "1cacfb0aa5545cf84688b8396079b855",
+
"cacheID": "ab3827e5716f65074a802ddb36f66d2b",
"id": null,
"metadata": {},
"name": "AppQuery",
"operationKind": "query",
-
"text": "query AppQuery {\n ...App_plays\n}\n\nfragment App_plays on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: \"playedTime\", direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n"
+
"text": "query AppQuery(\n $chartWhere: FmTealAlphaFeedPlayWhereInput!\n) {\n ...App_plays\n ...ScrobbleChart_data\n}\n\nfragment App_plays on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: playedTime, direction: desc}]) {\n totalCount\n edges {\n node {\n playedTime\n ...TrackItem_play\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment ScrobbleChart_data on Query {\n chartData: fmTealAlphaFeedPlaysAggregated(groupBy: [{field: playedTime, interval: day}], where: $chartWhere, limit: 90) {\n playedTime\n count\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n artistMbId\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfile {\n displayName\n id\n }\n}\n"
}
};
})();
-
(node as any).hash = "4b1837f6cd874e31461fbead77c1b012";
+
(node as any).hash = "7266612861cb55b740623549f1a03f26";
export default node;
+191
src/__generated__/AppSubscription.graphql.ts
···
+
/**
+
* @generated SignedSource<<f0666e46fa4693a3d7869f9de0266bab>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
import { FragmentRefs } from "relay-runtime";
+
export type AppSubscription$variables = Record<PropertyKey, never>;
+
export type AppSubscription$data = {
+
readonly fmTealAlphaFeedPlayCreated: {
+
readonly playedTime: string | null | undefined;
+
readonly uri: string;
+
readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">;
+
};
+
};
+
export type AppSubscription = {
+
response: AppSubscription$data;
+
variables: AppSubscription$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "uri",
+
"storageKey": null
+
},
+
v1 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "playedTime",
+
"storageKey": null
+
},
+
v2 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
+
return {
+
"fragment": {
+
"argumentDefinitions": [],
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "AppSubscription",
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlayCreated",
+
"plural": false,
+
"selections": [
+
(v0/*: any*/),
+
(v1/*: any*/),
+
{
+
"args": null,
+
"kind": "FragmentSpread",
+
"name": "TrackItem_play"
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"type": "Subscription",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": [],
+
"kind": "Operation",
+
"name": "AppSubscription",
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlayCreated",
+
"plural": false,
+
"selections": [
+
(v0/*: any*/),
+
(v1/*: any*/),
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "trackName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
+
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseMbId",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "actorHandle",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "AppBskyActorProfile",
+
"kind": "LinkedField",
+
"name": "appBskyActorProfile",
+
"plural": false,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "displayName",
+
"storageKey": null
+
},
+
(v2/*: any*/)
+
],
+
"storageKey": null
+
},
+
(v2/*: any*/)
+
],
+
"storageKey": null
+
}
+
]
+
},
+
"params": {
+
"cacheID": "f547109f04ecd8d1a8679dbf0b7f98b4",
+
"id": null,
+
"metadata": {},
+
"name": "AppSubscription",
+
"operationKind": "subscription",
+
"text": "subscription AppSubscription {\n fmTealAlphaFeedPlayCreated {\n uri\n playedTime\n ...TrackItem_play\n id\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n artistMbId\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfile {\n displayName\n id\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "96fa73287dd5ff8b0c3e83b8f663f65e";
+
+
export default node;
+2 -2
src/__generated__/App_plays.graphql.ts
···
/**
-
* @generated SignedSource<<a3ae5f31f618986fb12e6c57458c9853>>
+
* @generated SignedSource<<ba0bacb4e016f0edbea67013c8694b23>>
* @lightSyntaxTransform
* @nogrep
*/
···
};
})();
-
(node as any).hash = "0e4acf96fedae07af90ce6e9e3bf18d6";
+
(node as any).hash = "1e73fa97ccff20071e5a3fba0f00b48c";
export default node;
+190
src/__generated__/OverallQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<be6b74e81f17f155aaa1449350560e4d>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type OverallQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
+
export type OverallQuery$data = {
+
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
+
readonly artists: any | null | undefined;
+
readonly count: number;
+
readonly releaseMbId: any | null | undefined;
+
readonly releaseName: any | null | undefined;
+
}>;
+
};
+
export type OverallQuery = {
+
response: OverallQuery$data;
+
variables: OverallQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = [
+
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "releaseMbId"
+
},
+
{
+
"field": "releaseName"
+
},
+
{
+
"field": "artists"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 20
+
},
+
{
+
"kind": "Literal",
+
"name": "orderBy",
+
"value": {
+
"count": "desc"
+
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseMbId",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
];
+
return {
+
"fragment": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "OverallQuery",
+
"selections": (v1/*: any*/),
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Operation",
+
"name": "OverallQuery",
+
"selections": (v1/*: any*/)
+
},
+
"params": {
+
"cacheID": "8f8e0d5f64fdc442abebba83c5108588",
+
"id": null,
+
"metadata": {},
+
"name": "OverallQuery",
+
"operationKind": "query",
+
"text": "query OverallQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: releaseMbId}, {field: releaseName}, {field: artists}], orderBy: {count: desc}, limit: 20, where: $where) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "0fe8889cb7ff66cacf0605bf0cad4cb2";
+
+
export default node;
+361
src/__generated__/ProfileLayoutQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<d13059845ad242da48faf0e8ee3fae93>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
import { FragmentRefs } from "relay-runtime";
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type ProfileLayoutQuery$variables = {
+
chartWhere: FmTealAlphaFeedPlayWhereInput;
+
where: FmTealAlphaFeedPlayWhereInput;
+
};
+
export type ProfileLayoutQuery$data = {
+
readonly fmTealAlphaFeedPlays: {
+
readonly edges: ReadonlyArray<{
+
readonly node: {
+
readonly actorHandle: string | null | undefined;
+
readonly appBskyActorProfile: {
+
readonly avatar: {
+
readonly url: string;
+
} | null | undefined;
+
readonly description: string | null | undefined;
+
readonly displayName: string | null | undefined;
+
} | null | undefined;
+
};
+
}>;
+
};
+
readonly " $fragmentSpreads": FragmentRefs<"ScrobbleChart_data">;
+
};
+
export type ProfileLayoutQuery = {
+
response: ProfileLayoutQuery$data;
+
variables: ProfileLayoutQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = {
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "chartWhere"
+
},
+
v1 = {
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
},
+
v2 = [
+
{
+
"kind": "Literal",
+
"name": "first",
+
"value": 1
+
},
+
{
+
"kind": "Literal",
+
"name": "sortBy",
+
"value": [
+
{
+
"direction": "desc",
+
"field": "playedTime"
+
}
+
]
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
v3 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "actorHandle",
+
"storageKey": null
+
},
+
v4 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "displayName",
+
"storageKey": null
+
},
+
v5 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "description",
+
"storageKey": null
+
},
+
v6 = {
+
"alias": null,
+
"args": null,
+
"concreteType": "Blob",
+
"kind": "LinkedField",
+
"name": "avatar",
+
"plural": false,
+
"selections": [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "preset",
+
"value": "avatar"
+
}
+
],
+
"kind": "ScalarField",
+
"name": "url",
+
"storageKey": "url(preset:\"avatar\")"
+
}
+
],
+
"storageKey": null
+
},
+
v7 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
+
return {
+
"fragment": {
+
"argumentDefinitions": [
+
(v0/*: any*/),
+
(v1/*: any*/)
+
],
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "ProfileLayoutQuery",
+
"selections": [
+
{
+
"args": null,
+
"kind": "FragmentSpread",
+
"name": "ScrobbleChart_data"
+
},
+
{
+
"alias": null,
+
"args": (v2/*: any*/),
+
"concreteType": "FmTealAlphaFeedPlayConnection",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlays",
+
"plural": false,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlayEdge",
+
"kind": "LinkedField",
+
"name": "edges",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "node",
+
"plural": false,
+
"selections": [
+
(v3/*: any*/),
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "AppBskyActorProfile",
+
"kind": "LinkedField",
+
"name": "appBskyActorProfile",
+
"plural": false,
+
"selections": [
+
(v4/*: any*/),
+
(v5/*: any*/),
+
(v6/*: any*/)
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": [
+
(v1/*: any*/),
+
(v0/*: any*/)
+
],
+
"kind": "Operation",
+
"name": "ProfileLayoutQuery",
+
"selections": [
+
{
+
"alias": "chartData",
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "playedTime",
+
"interval": "day"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 90
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "chartWhere"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "playedTime",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": (v2/*: any*/),
+
"concreteType": "FmTealAlphaFeedPlayConnection",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlays",
+
"plural": false,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlayEdge",
+
"kind": "LinkedField",
+
"name": "edges",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "node",
+
"plural": false,
+
"selections": [
+
(v3/*: any*/),
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "AppBskyActorProfile",
+
"kind": "LinkedField",
+
"name": "appBskyActorProfile",
+
"plural": false,
+
"selections": [
+
(v4/*: any*/),
+
(v5/*: any*/),
+
(v6/*: any*/),
+
(v7/*: any*/)
+
],
+
"storageKey": null
+
},
+
(v7/*: any*/)
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
]
+
},
+
"params": {
+
"cacheID": "df91116d25da921e74bc825710304143",
+
"id": null,
+
"metadata": {},
+
"name": "ProfileLayoutQuery",
+
"operationKind": "query",
+
"text": "query ProfileLayoutQuery(\n $where: FmTealAlphaFeedPlayWhereInput!\n $chartWhere: FmTealAlphaFeedPlayWhereInput!\n) {\n ...ScrobbleChart_data\n fmTealAlphaFeedPlays(first: 1, sortBy: [{field: playedTime, direction: desc}], where: $where) {\n edges {\n node {\n actorHandle\n appBskyActorProfile {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment ScrobbleChart_data on Query {\n chartData: fmTealAlphaFeedPlaysAggregated(groupBy: [{field: playedTime, interval: day}], where: $chartWhere, limit: 90) {\n playedTime\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "41d50b6c47174293deb76769f8f04b78";
+
+
export default node;
+91 -38
src/__generated__/ProfilePaginationQuery.graphql.ts
···
/**
-
* @generated SignedSource<<97625934b32c4079cc58877234aeac04>>
+
* @generated SignedSource<<daf74bb66a4e6d9119e3b571e872c199>>
* @lightSyntaxTransform
* @nogrep
*/
···
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
export type ProfilePaginationQuery$variables = {
count?: number | null | undefined;
cursor?: string | null | undefined;
-
where: any;
+
where: FmTealAlphaFeedPlayWhereInput;
};
export type ProfilePaginationQuery$data = {
readonly " $fragmentSpreads": FragmentRefs<"Profile_plays">;
···
]
},
(v1/*: any*/)
-
];
+
],
+
v3 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
···
{
"alias": null,
"args": null,
-
"kind": "ScalarField",
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
"storageKey": null
},
{
···
{
"alias": null,
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
"concreteType": "AppBskyActorProfile",
"kind": "LinkedField",
"name": "appBskyActorProfile",
···
"name": "displayName",
"storageKey": null
},
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
-
"name": "description",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"concreteType": "Blob",
-
"kind": "LinkedField",
-
"name": "avatar",
-
"plural": false,
-
"selections": [
-
{
-
"alias": null,
-
"args": [
-
{
-
"kind": "Literal",
-
"name": "preset",
-
"value": "avatar"
-
}
-
],
-
"kind": "ScalarField",
-
"name": "url",
-
"storageKey": "url(preset:\"avatar\")"
-
}
-
],
-
"storageKey": null
-
}
+
(v3/*: any*/)
],
"storageKey": null
},
+
(v3/*: any*/),
{
"alias": null,
"args": null,
···
]
},
"params": {
-
"cacheID": "08e603fb4052c3556739bda428413453",
+
"cacheID": "a74a6087f9fec6f56ec166df70ad6365",
"id": null,
"metadata": {},
"name": "ProfilePaginationQuery",
"operationKind": "query",
-
"text": "query ProfilePaginationQuery(\n $count: Int = 20\n $cursor: String\n $where: JSON!\n) {\n ...Profile_plays_mjR8k\n}\n\nfragment Profile_plays_mjR8k on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: \"playedTime\", direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfile {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n"
+
"text": "query ProfilePaginationQuery(\n $count: Int = 20\n $cursor: String\n $where: FmTealAlphaFeedPlayWhereInput!\n) {\n ...Profile_plays_mjR8k\n}\n\nfragment Profile_plays_mjR8k on Query {\n fmTealAlphaFeedPlays(first: $count, after: $cursor, sortBy: [{field: playedTime, direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n artistMbId\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfile {\n displayName\n id\n }\n}\n"
}
};
})();
-
(node as any).hash = "474168bb0d13417c1b7067c09a82f7a2";
+
(node as any).hash = "42b3df3f8d988503f2b08000f01b4c83";
export default node;
+91 -38
src/__generated__/ProfileQuery.graphql.ts
···
/**
-
* @generated SignedSource<<c47dafb8d21963c2a9dcbcd54d7bd8d8>>
+
* @generated SignedSource<<29d2f6473f1660ade97d5a93c8cab01a>>
* @lightSyntaxTransform
* @nogrep
*/
···
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
export type ProfileQuery$variables = {
-
where: any;
+
where: FmTealAlphaFeedPlayWhereInput;
};
export type ProfileQuery$data = {
readonly " $fragmentSpreads": FragmentRefs<"Profile_plays">;
···
]
},
(v1/*: any*/)
-
];
+
],
+
v3 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "id",
+
"storageKey": null
+
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
···
{
"alias": null,
"args": null,
-
"kind": "ScalarField",
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
"storageKey": null
},
{
···
{
"alias": null,
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
"concreteType": "AppBskyActorProfile",
"kind": "LinkedField",
"name": "appBskyActorProfile",
···
"name": "displayName",
"storageKey": null
},
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
-
"name": "description",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"concreteType": "Blob",
-
"kind": "LinkedField",
-
"name": "avatar",
-
"plural": false,
-
"selections": [
-
{
-
"alias": null,
-
"args": [
-
{
-
"kind": "Literal",
-
"name": "preset",
-
"value": "avatar"
-
}
-
],
-
"kind": "ScalarField",
-
"name": "url",
-
"storageKey": "url(preset:\"avatar\")"
-
}
-
],
-
"storageKey": null
-
}
+
(v3/*: any*/)
],
"storageKey": null
},
+
(v3/*: any*/),
{
"alias": null,
"args": null,
···
]
},
"params": {
-
"cacheID": "3137e7b6ec5148299e7a7c7f4edf07b2",
+
"cacheID": "d7aed8545b8651ae55d39d275c14fc74",
"id": null,
"metadata": {},
"name": "ProfileQuery",
"operationKind": "query",
-
"text": "query ProfileQuery(\n $where: JSON!\n) {\n ...Profile_plays_3FC4Qo\n}\n\nfragment Profile_plays_3FC4Qo on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: \"playedTime\", direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n actorHandle\n appBskyActorProfile {\n displayName\n description\n avatar {\n url(preset: \"avatar\")\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n"
+
"text": "query ProfileQuery(\n $where: FmTealAlphaFeedPlayWhereInput!\n) {\n ...Profile_plays_3FC4Qo\n}\n\nfragment Profile_plays_3FC4Qo on Query {\n fmTealAlphaFeedPlays(first: 20, sortBy: [{field: playedTime, direction: desc}], where: $where) {\n totalCount\n edges {\n node {\n ...TrackItem_play\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists {\n artistName\n artistMbId\n }\n releaseName\n releaseMbId\n actorHandle\n musicServiceBaseDomain\n appBskyActorProfile {\n displayName\n id\n }\n}\n"
}
};
})();
-
(node as any).hash = "267039e382b3b95a739ff3cdced3211e";
+
(node as any).hash = "4a0ecbad0ab4453246cdcdd753b1f84f";
export default node;
+190
src/__generated__/ProfileTopAlbumsQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<f93041e209eda64391ae31c91b5b7c79>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type ProfileTopAlbumsQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
+
export type ProfileTopAlbumsQuery$data = {
+
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
+
readonly artists: any | null | undefined;
+
readonly count: number;
+
readonly releaseMbId: any | null | undefined;
+
readonly releaseName: any | null | undefined;
+
}>;
+
};
+
export type ProfileTopAlbumsQuery = {
+
response: ProfileTopAlbumsQuery$data;
+
variables: ProfileTopAlbumsQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = [
+
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "releaseMbId"
+
},
+
{
+
"field": "releaseName"
+
},
+
{
+
"field": "artists"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 50
+
},
+
{
+
"kind": "Literal",
+
"name": "orderBy",
+
"value": {
+
"count": "desc"
+
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseMbId",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
];
+
return {
+
"fragment": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "ProfileTopAlbumsQuery",
+
"selections": (v1/*: any*/),
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Operation",
+
"name": "ProfileTopAlbumsQuery",
+
"selections": (v1/*: any*/)
+
},
+
"params": {
+
"cacheID": "e3dea5c70916481cfc7237ea694fa720",
+
"id": null,
+
"metadata": {},
+
"name": "ProfileTopAlbumsQuery",
+
"operationKind": "query",
+
"text": "query ProfileTopAlbumsQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: releaseMbId}, {field: releaseName}, {field: artists}], orderBy: {count: desc}, limit: 50, where: $where) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "61866b819fab81f5acac1c96479417da";
+
+
export default node;
+168
src/__generated__/ProfileTopArtistsQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<4d813a091c04adc8f0f29b7c8a585ca2>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type ProfileTopArtistsQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
+
export type ProfileTopArtistsQuery$data = {
+
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
+
readonly artists: any | null | undefined;
+
readonly count: number;
+
}>;
+
};
+
export type ProfileTopArtistsQuery = {
+
response: ProfileTopArtistsQuery$data;
+
variables: ProfileTopArtistsQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = [
+
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "artists"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 50
+
},
+
{
+
"kind": "Literal",
+
"name": "orderBy",
+
"value": {
+
"count": "desc"
+
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
];
+
return {
+
"fragment": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "ProfileTopArtistsQuery",
+
"selections": (v1/*: any*/),
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Operation",
+
"name": "ProfileTopArtistsQuery",
+
"selections": (v1/*: any*/)
+
},
+
"params": {
+
"cacheID": "99a68c4a41531b275e40b1f9d2ef2d85",
+
"id": null,
+
"metadata": {},
+
"name": "ProfileTopArtistsQuery",
+
"operationKind": "query",
+
"text": "query ProfileTopArtistsQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: artists}], orderBy: {count: desc}, limit: 50, where: $where) {\n artists\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "3b52f52c3db6609a5a0e18a79018dc9e";
+
+
export default node;
+190
src/__generated__/ProfileTopTracksQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<61cded348d713dac6a8c3871906a3d5c>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type ProfileTopTracksQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
+
export type ProfileTopTracksQuery$data = {
+
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
+
readonly artists: any | null | undefined;
+
readonly count: number;
+
readonly releaseMbId: any | null | undefined;
+
readonly trackName: any | null | undefined;
+
}>;
+
};
+
export type ProfileTopTracksQuery = {
+
response: ProfileTopTracksQuery$data;
+
variables: ProfileTopTracksQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = [
+
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "trackName"
+
},
+
{
+
"field": "releaseMbId"
+
},
+
{
+
"field": "artists"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 10
+
},
+
{
+
"kind": "Literal",
+
"name": "orderBy",
+
"value": {
+
"count": "desc"
+
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "trackName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseMbId",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
];
+
return {
+
"fragment": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "ProfileTopTracksQuery",
+
"selections": (v1/*: any*/),
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Operation",
+
"name": "ProfileTopTracksQuery",
+
"selections": (v1/*: any*/)
+
},
+
"params": {
+
"cacheID": "570221126c225955de88bb1dd21c9d9e",
+
"id": null,
+
"metadata": {},
+
"name": "ProfileTopTracksQuery",
+
"operationKind": "query",
+
"text": "query ProfileTopTracksQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: trackName}, {field: releaseMbId}, {field: artists}], orderBy: {count: desc}, limit: 10, where: $where) {\n trackName\n releaseMbId\n artists\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "459f108ce53bf13ea9548756bb6698d5";
+
+
export default node;
+2 -66
src/__generated__/Profile_plays.graphql.ts
···
/**
-
* @generated SignedSource<<bd63c9e74b05810076b60ad0e51cb230>>
+
* @generated SignedSource<<99fde612f90ee1d1dad1825b7b3b5f56>>
* @lightSyntaxTransform
* @nogrep
*/
···
readonly fmTealAlphaFeedPlays: {
readonly edges: ReadonlyArray<{
readonly node: {
-
readonly actorHandle: string | null | undefined;
-
readonly appBskyActorProfile: {
-
readonly avatar: {
-
readonly url: string;
-
} | null | undefined;
-
readonly description: string | null | undefined;
-
readonly displayName: string | null | undefined;
-
} | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">;
};
}>;
···
"alias": null,
"args": null,
"kind": "ScalarField",
-
"name": "actorHandle",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"concreteType": "AppBskyActorProfile",
-
"kind": "LinkedField",
-
"name": "appBskyActorProfile",
-
"plural": false,
-
"selections": [
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
-
"name": "displayName",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
-
"name": "description",
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"concreteType": "Blob",
-
"kind": "LinkedField",
-
"name": "avatar",
-
"plural": false,
-
"selections": [
-
{
-
"alias": null,
-
"args": [
-
{
-
"kind": "Literal",
-
"name": "preset",
-
"value": "avatar"
-
}
-
],
-
"kind": "ScalarField",
-
"name": "url",
-
"storageKey": "url(preset:\"avatar\")"
-
}
-
],
-
"storageKey": null
-
}
-
],
-
"storageKey": null
-
},
-
{
-
"alias": null,
-
"args": null,
-
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
···
};
})();
-
(node as any).hash = "474168bb0d13417c1b7067c09a82f7a2";
+
(node as any).hash = "42b3df3f8d988503f2b08000f01b4c83";
export default node;
+89
src/__generated__/ScrobbleChart_data.graphql.ts
···
+
/**
+
* @generated SignedSource<<7b446f8950ffde63fb0e7748bb596e66>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ReaderFragment } from 'relay-runtime';
+
import { FragmentRefs } from "relay-runtime";
+
export type ScrobbleChart_data$data = {
+
readonly chartData: ReadonlyArray<{
+
readonly count: number;
+
readonly playedTime: any | null | undefined;
+
}>;
+
readonly " $fragmentType": "ScrobbleChart_data";
+
};
+
export type ScrobbleChart_data$key = {
+
readonly " $data"?: ScrobbleChart_data$data;
+
readonly " $fragmentSpreads": FragmentRefs<"ScrobbleChart_data">;
+
};
+
+
const node: ReaderFragment = {
+
"argumentDefinitions": [
+
{
+
"kind": "RootArgument",
+
"name": "chartWhere"
+
}
+
],
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "ScrobbleChart_data",
+
"selections": [
+
{
+
"alias": "chartData",
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "playedTime",
+
"interval": "day"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 90
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "chartWhere"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "playedTime",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"type": "Query",
+
"abstractKey": null
+
};
+
+
(node as any).hash = "6d8ebfa533779947a0b3cd703929b5ba";
+
+
export default node;
+86 -16
src/__generated__/TopAlbumsQuery.graphql.ts
···
/**
-
* @generated SignedSource<<a492b6190b60e9be64d199702b76977a>>
+
* @generated SignedSource<<5b4069c82e72c33b75aaff1d16f1421f>>
* @lightSyntaxTransform
* @nogrep
*/
···
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
-
export type TopAlbumsQuery$variables = Record<PropertyKey, never>;
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type TopAlbumsQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
export type TopAlbumsQuery$data = {
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
-
readonly artists: string | null | undefined;
+
readonly artists: any | null | undefined;
readonly count: number;
-
readonly releaseMbId: string | null | undefined;
-
readonly releaseName: string | null | undefined;
+
readonly releaseMbId: any | null | undefined;
+
readonly releaseName: any | null | undefined;
}>;
};
export type TopAlbumsQuery = {
···
const node: ConcreteRequest = (function(){
var v0 = [
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "groupBy",
"value": [
-
"releaseMbId",
-
"releaseName",
-
"artists"
+
{
+
"field": "releaseMbId"
+
},
+
{
+
"field": "releaseName"
+
},
+
{
+
"field": "artists"
+
}
]
},
{
···
"value": {
"count": "desc"
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
}
],
"concreteType": "FmTealAlphaFeedPlayAggregated",
···
"storageKey": null
}
],
-
"storageKey": "fmTealAlphaFeedPlaysAggregated(groupBy:[\"releaseMbId\",\"releaseName\",\"artists\"],limit:100,orderBy:{\"count\":\"desc\"})"
+
"storageKey": null
}
];
return {
"fragment": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TopAlbumsQuery",
-
"selections": (v0/*: any*/),
+
"selections": (v1/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TopAlbumsQuery",
-
"selections": (v0/*: any*/)
+
"selections": (v1/*: any*/)
},
"params": {
-
"cacheID": "65b42ff33b8a5de6eb4785e764ae70fc",
+
"cacheID": "4bc742f9cab572a86f4956ae1325e650",
"id": null,
"metadata": {},
"name": "TopAlbumsQuery",
"operationKind": "query",
-
"text": "query TopAlbumsQuery {\n fmTealAlphaFeedPlaysAggregated(groupBy: [\"releaseMbId\", \"releaseName\", \"artists\"], orderBy: {count: desc}, limit: 100) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n"
+
"text": "query TopAlbumsQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: releaseMbId}, {field: releaseName}, {field: artists}], orderBy: {count: desc}, limit: 100, where: $where) {\n releaseMbId\n releaseName\n artists\n count\n }\n}\n"
}
};
})();
-
(node as any).hash = "b5748a3a4af3140d3cff228e7462f73d";
+
(node as any).hash = "c916cfe287c6837e7b40f0712b123f12";
export default node;
+168
src/__generated__/TopArtistsQuery.graphql.ts
···
+
/**
+
* @generated SignedSource<<f99e80895febe87f37e531bc257ce798>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type TopArtistsQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
+
export type TopArtistsQuery$data = {
+
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
+
readonly artists: any | null | undefined;
+
readonly count: number;
+
}>;
+
};
+
export type TopArtistsQuery = {
+
response: TopArtistsQuery$data;
+
variables: TopArtistsQuery$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = [
+
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
+
"alias": null,
+
"args": [
+
{
+
"kind": "Literal",
+
"name": "groupBy",
+
"value": [
+
{
+
"field": "artists"
+
}
+
]
+
},
+
{
+
"kind": "Literal",
+
"name": "limit",
+
"value": 50
+
},
+
{
+
"kind": "Literal",
+
"name": "orderBy",
+
"value": {
+
"count": "desc"
+
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
+
}
+
],
+
"concreteType": "FmTealAlphaFeedPlayAggregated",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlaysAggregated",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "count",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
];
+
return {
+
"fragment": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "TopArtistsQuery",
+
"selections": (v1/*: any*/),
+
"type": "Query",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": (v0/*: any*/),
+
"kind": "Operation",
+
"name": "TopArtistsQuery",
+
"selections": (v1/*: any*/)
+
},
+
"params": {
+
"cacheID": "2fa8fb4ebdb5d17d362eecc3da9bbcbf",
+
"id": null,
+
"metadata": {},
+
"name": "TopArtistsQuery",
+
"operationKind": "query",
+
"text": "query TopArtistsQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: artists}], orderBy: {count: desc}, limit: 50, where: $where) {\n artists\n count\n }\n}\n"
+
}
+
};
+
})();
+
+
(node as any).hash = "ca44425203547633e6fb13b8f49c5619";
+
+
export default node;
+86 -16
src/__generated__/TopTracksQuery.graphql.ts
···
/**
-
* @generated SignedSource<<ed55197dadbf24b9cf975295d63a2436>>
+
* @generated SignedSource<<28bfcfbaf324e20bbfc524afcc9ed549>>
* @lightSyntaxTransform
* @nogrep
*/
···
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
-
export type TopTracksQuery$variables = Record<PropertyKey, never>;
+
export type FmTealAlphaFeedPlayWhereInput = {
+
actorHandle?: StringFilter | null | undefined;
+
and?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
artistMbIds?: StringFilter | null | undefined;
+
artistNames?: StringFilter | null | undefined;
+
artists?: StringFilter | null | undefined;
+
cid?: StringFilter | null | undefined;
+
collection?: StringFilter | null | undefined;
+
did?: StringFilter | null | undefined;
+
duration?: IntFilter | null | undefined;
+
indexedAt?: DateTimeFilter | null | undefined;
+
isrc?: StringFilter | null | undefined;
+
json?: StringFilter | null | undefined;
+
musicServiceBaseDomain?: StringFilter | null | undefined;
+
or?: ReadonlyArray<FmTealAlphaFeedPlayWhereInput | null | undefined> | null | undefined;
+
originUrl?: StringFilter | null | undefined;
+
playedTime?: StringFilter | null | undefined;
+
recordingMbId?: StringFilter | null | undefined;
+
releaseMbId?: StringFilter | null | undefined;
+
releaseName?: StringFilter | null | undefined;
+
submissionClientAgent?: StringFilter | null | undefined;
+
trackMbId?: StringFilter | null | undefined;
+
trackName?: StringFilter | null | undefined;
+
uri?: StringFilter | null | undefined;
+
};
+
export type DateTimeFilter = {
+
eq?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type StringFilter = {
+
contains?: string | null | undefined;
+
eq?: string | null | undefined;
+
fuzzy?: string | null | undefined;
+
gt?: string | null | undefined;
+
gte?: string | null | undefined;
+
in?: ReadonlyArray<string | null | undefined> | null | undefined;
+
lt?: string | null | undefined;
+
lte?: string | null | undefined;
+
};
+
export type IntFilter = {
+
eq?: number | null | undefined;
+
gt?: number | null | undefined;
+
gte?: number | null | undefined;
+
in?: ReadonlyArray<number | null | undefined> | null | undefined;
+
lt?: number | null | undefined;
+
lte?: number | null | undefined;
+
};
+
export type TopTracksQuery$variables = {
+
where?: FmTealAlphaFeedPlayWhereInput | null | undefined;
+
};
export type TopTracksQuery$data = {
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
-
readonly artists: string | null | undefined;
+
readonly artists: any | null | undefined;
readonly count: number;
-
readonly releaseMbId: string | null | undefined;
-
readonly trackName: string | null | undefined;
+
readonly releaseMbId: any | null | undefined;
+
readonly trackName: any | null | undefined;
}>;
};
export type TopTracksQuery = {
···
const node: ConcreteRequest = (function(){
var v0 = [
{
+
"defaultValue": null,
+
"kind": "LocalArgument",
+
"name": "where"
+
}
+
],
+
v1 = [
+
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "groupBy",
"value": [
-
"trackName",
-
"releaseMbId",
-
"artists"
+
{
+
"field": "trackName"
+
},
+
{
+
"field": "releaseMbId"
+
},
+
{
+
"field": "artists"
+
}
]
},
{
···
"value": {
"count": "desc"
}
+
},
+
{
+
"kind": "Variable",
+
"name": "where",
+
"variableName": "where"
}
],
"concreteType": "FmTealAlphaFeedPlayAggregated",
···
"storageKey": null
}
],
-
"storageKey": "fmTealAlphaFeedPlaysAggregated(groupBy:[\"trackName\",\"releaseMbId\",\"artists\"],limit:50,orderBy:{\"count\":\"desc\"})"
+
"storageKey": null
}
];
return {
"fragment": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TopTracksQuery",
-
"selections": (v0/*: any*/),
+
"selections": (v1/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
-
"argumentDefinitions": [],
+
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TopTracksQuery",
-
"selections": (v0/*: any*/)
+
"selections": (v1/*: any*/)
},
"params": {
-
"cacheID": "61e9f7886dfe9eaeb599b939f2d636e5",
+
"cacheID": "d889d685b64fb19d468954bb3fb7ff7c",
"id": null,
"metadata": {},
"name": "TopTracksQuery",
"operationKind": "query",
-
"text": "query TopTracksQuery {\n fmTealAlphaFeedPlaysAggregated(groupBy: [\"trackName\", \"releaseMbId\", \"artists\"], orderBy: {count: desc}, limit: 50) {\n trackName\n releaseMbId\n artists\n count\n }\n}\n"
+
"text": "query TopTracksQuery(\n $where: FmTealAlphaFeedPlayWhereInput\n) {\n fmTealAlphaFeedPlaysAggregated(groupBy: [{field: trackName}, {field: releaseMbId}, {field: artists}], orderBy: {count: desc}, limit: 50, where: $where) {\n trackName\n releaseMbId\n artists\n count\n }\n}\n"
}
};
})();
-
(node as any).hash = "536f8ddb64daa09017abff121d7ea8ce";
+
(node as any).hash = "4b62eaeaf8a935abc28e77c8cd2907d1";
export default node;
+33 -4
src/__generated__/TrackItem_play.graphql.ts
···
/**
-
* @generated SignedSource<<1403d6e3403844ad955c2fe48c8a66c9>>
+
* @generated SignedSource<<b5b4d3bdca427eb13381e978bf98d7b9>>
* @lightSyntaxTransform
* @nogrep
*/
···
readonly appBskyActorProfile: {
readonly displayName: string | null | undefined;
} | null | undefined;
-
readonly artists: any | null | undefined;
+
readonly artists: ReadonlyArray<{
+
readonly artistMbId: string | null | undefined;
+
readonly artistName: string | null | undefined;
+
} | null | undefined> | null | undefined;
+
readonly musicServiceBaseDomain: string | null | undefined;
readonly playedTime: string | null | undefined;
readonly releaseMbId: string | null | undefined;
readonly releaseName: string | null | undefined;
···
{
"alias": null,
"args": null,
-
"kind": "ScalarField",
+
"concreteType": "FmTealAlphaFeedDefsArtist",
+
"kind": "LinkedField",
"name": "artists",
+
"plural": true,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artistMbId",
+
"storageKey": null
+
}
+
],
"storageKey": null
},
{
···
{
"alias": null,
"args": null,
+
"kind": "ScalarField",
+
"name": "musicServiceBaseDomain",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
"concreteType": "AppBskyActorProfile",
"kind": "LinkedField",
"name": "appBskyActorProfile",
···
"abstractKey": null
};
-
(node as any).hash = "08e8e2c14a894471e9a3153f8918e02e";
+
(node as any).hash = "9a70dada54e50e27bdd19183f1b16e35";
export default node;
+37
src/generateChartData.ts
···
+
export interface DataPoint {
+
date: string;
+
count: number;
+
}
+
+
export function generateChartData(
+
plays: readonly { readonly playedTime?: string | null; readonly [key: string]: any }[],
+
days = 90
+
): DataPoint[] {
+
const counts = new Map<string, number>();
+
const now = new Date();
+
+
// Initialize last N days with 0 counts
+
for (let i = days - 1; i >= 0; i--) {
+
const date = new Date(now);
+
date.setDate(date.getDate() - i);
+
date.setHours(0, 0, 0, 0);
+
const dateStr = date.toISOString().split("T")[0];
+
counts.set(dateStr, 0);
+
}
+
+
// Count plays per day
+
plays.forEach((play) => {
+
if (play?.playedTime) {
+
const date = new Date(play.playedTime);
+
date.setHours(0, 0, 0, 0);
+
const dateStr = date.toISOString().split("T")[0];
+
if (counts.has(dateStr)) {
+
counts.set(dateStr, (counts.get(dateStr) || 0) + 1);
+
}
+
}
+
});
+
+
return Array.from(counts.entries())
+
.map(([date, count]) => ({ date, count }))
+
.sort((a, b) => a.date.localeCompare(b.date));
+
}
+93 -6
src/main.tsx
···
import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
-
import { BrowserRouter, Routes, Route } from "react-router-dom";
+
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import "./index.css";
import App from "./App.tsx";
import Profile from "./Profile.tsx";
···
import TopAlbums from "./TopAlbums.tsx";
import LoadingFallback from "./LoadingFallback.tsx";
import { RelayEnvironmentProvider } from "react-relay";
-
import { Environment, Network, type FetchFunction } from "relay-runtime";
+
import {
+
Environment,
+
type FetchFunction,
+
type GraphQLResponse,
+
Network,
+
Observable,
+
type SubscribeFunction,
+
} from "relay-runtime";
+
import { createClient } from "graphql-ws";
+
import Overall from "./Overall.tsx";
+
import ProfileTopArtists from "./ProfileTopArtists.tsx";
+
import ProfileTopAlbums from "./ProfileTopAlbums.tsx";
+
import ProfileTopTracks from "./ProfileTopTracks.tsx";
+
import TopArtists from "./TopArtists.tsx";
const HTTP_ENDPOINT =
-
"https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a";
+
"https://api.slices.network/graphql?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26";
+
+
const WS_ENDPOINT =
+
"wss://api.slices.network/graphql/ws?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26";
const fetchGraphQL: FetchFunction = async (request, variables) => {
const resp = await fetch(HTTP_ENDPOINT, {
···
return await resp.json();
};
+
const wsClient = createClient({
+
url: WS_ENDPOINT,
+
retryAttempts: 5,
+
shouldRetry: () => true,
+
on: {
+
connected: () => {
+
console.log("WebSocket connected!");
+
},
+
error: (error) => {
+
console.error("WebSocket error:", error);
+
},
+
closed: (event) => {
+
console.log("WebSocket closed:", event);
+
},
+
},
+
});
+
+
const subscribe: SubscribeFunction = (operation, variables) => {
+
return Observable.create((sink) => {
+
if (!operation.text) {
+
sink.error(new Error("Missing operation text"));
+
return;
+
}
+
+
return wsClient.subscribe(
+
{
+
operationName: operation.name,
+
query: operation.text,
+
variables,
+
},
+
{
+
next: (data) => {
+
if (data.data !== null && data.data !== undefined) {
+
sink.next({ data: data.data } as GraphQLResponse);
+
}
+
},
+
error: (error) => {
+
console.error("Subscription error:", error);
+
if (error instanceof Error) {
+
sink.error(error);
+
} else if (error instanceof CloseEvent) {
+
sink.error(
+
new Error(`WebSocket closed: ${error.code} ${error.reason}`),
+
);
+
} else {
+
sink.error(new Error(JSON.stringify(error)));
+
}
+
},
+
complete: () => sink.complete(),
+
},
+
);
+
});
+
};
+
const environment = new Environment({
-
network: Network.create(fetchGraphQL),
+
network: Network.create(fetchGraphQL, subscribe),
});
createRoot(document.getElementById("root")!).render(
···
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<App />} />
+
<Route path="/artists" element={<TopArtists />} />
+
<Route path="/artists/:period" element={<TopArtists />} />
<Route path="/tracks" element={<TopTracks />} />
+
<Route path="/tracks/:period" element={<TopTracks />} />
<Route path="/albums" element={<TopAlbums />} />
-
<Route path="/profile/:handle" element={<Profile />} />
+
<Route path="/albums/:period" element={<TopAlbums />} />
+
<Route
+
path="/profile/:handle"
+
element={<Navigate to="scrobbles" replace />}
+
/>
+
<Route path="/profile/:handle/scrobbles" element={<Profile />} />
+
<Route path="/profile/:handle/overall" element={<Overall />}>
+
<Route index element={<Navigate to="artists" replace />} />
+
<Route path="artists" element={<ProfileTopArtists />} />
+
<Route path="artists/:period" element={<ProfileTopArtists />} />
+
<Route path="albums" element={<ProfileTopAlbums />} />
+
<Route path="albums/:period" element={<ProfileTopAlbums />} />
+
<Route path="tracks" element={<ProfileTopTracks />} />
+
<Route path="tracks/:period" element={<ProfileTopTracks />} />
+
</Route>
</Routes>
</Suspense>
</RelayEnvironmentProvider>
</BrowserRouter>
-
</StrictMode>
+
</StrictMode>,
);
+31
src/useDateRangeFilter.ts
···
+
import { useMemo } from "react";
+
+
export function useDateRangeFilter(period: string | undefined) {
+
return useMemo(() => {
+
if (!period || period === "all") {
+
return { where: undefined };
+
}
+
+
// Round to start of current day to keep the timestamp stable
+
const now = new Date();
+
now.setHours(0, 0, 0, 0);
+
+
let daysAgo = 0;
+
switch (period) {
+
case "daily":
+
daysAgo = 1;
+
break;
+
case "weekly":
+
daysAgo = 7;
+
break;
+
case "monthly":
+
daysAgo = 30;
+
break;
+
default:
+
return { where: undefined };
+
}
+
+
const startDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
+
return { where: { playedTime: { gte: startDate.toISOString() } } };
+
}, [period]);
+
}