WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add GraphQL API service

+124
+124
src/services/grain-api.js
··· 1 + class GrainApiService { 2 + #endpoint = '/api/graphql'; // Configure based on environment 3 + 4 + async getTimeline({ first = 10, after = null } = {}) { 5 + const query = ` 6 + query Timeline($first: Int, $after: String) { 7 + socialGrainGallery( 8 + first: $first 9 + after: $after 10 + sortBy: [{ field: createdAt, direction: DESC }] 11 + ) { 12 + edges { 13 + node { 14 + uri 15 + title 16 + description 17 + createdAt 18 + actorHandle 19 + appBskyActorProfileByDid { 20 + avatar { url } 21 + displayName 22 + } 23 + socialGrainGalleryItemViaGallery(first: 10) { 24 + edges { 25 + node { 26 + item 27 + } 28 + } 29 + totalCount 30 + } 31 + socialGrainFavoriteViaSubject { 32 + totalCount 33 + } 34 + socialGrainCommentViaSubject { 35 + totalCount 36 + } 37 + } 38 + } 39 + pageInfo { 40 + hasNextPage 41 + endCursor 42 + } 43 + } 44 + } 45 + `; 46 + 47 + const response = await this.#execute(query, { first, after }); 48 + return this.#transformTimelineResponse(response); 49 + } 50 + 51 + async getPhotosByUris(uris) { 52 + if (!uris.length) return []; 53 + 54 + const query = ` 55 + query Photos($uris: [String!]) { 56 + socialGrainPhoto(filter: { uri: { in: $uris } }) { 57 + edges { 58 + node { 59 + uri 60 + alt 61 + aspectRatio { width height } 62 + photo { url } 63 + } 64 + } 65 + } 66 + } 67 + `; 68 + 69 + const response = await this.#execute(query, { uris }); 70 + return response.data?.socialGrainPhoto?.edges?.map(e => e.node) || []; 71 + } 72 + 73 + #transformTimelineResponse(response) { 74 + const connection = response.data?.socialGrainGallery; 75 + if (!connection) return { galleries: [], pageInfo: { hasNextPage: false } }; 76 + 77 + const galleries = connection.edges.map(edge => { 78 + const node = edge.node; 79 + const profile = node.appBskyActorProfileByDid; 80 + const items = node.socialGrainGalleryItemViaGallery?.edges || []; 81 + 82 + return { 83 + uri: node.uri, 84 + title: node.title, 85 + description: node.description, 86 + createdAt: node.createdAt, 87 + handle: node.actorHandle, 88 + displayName: profile?.displayName || '', 89 + avatarUrl: profile?.avatar?.url || '', 90 + photoUris: items.map(i => i.node.item), 91 + photos: [], // Will be populated by getPhotosByUris 92 + favoriteCount: node.socialGrainFavoriteViaSubject?.totalCount || 0, 93 + commentCount: node.socialGrainCommentViaSubject?.totalCount || 0 94 + }; 95 + }); 96 + 97 + return { 98 + galleries, 99 + pageInfo: connection.pageInfo 100 + }; 101 + } 102 + 103 + async #execute(query, variables = {}) { 104 + const response = await fetch(this.#endpoint, { 105 + method: 'POST', 106 + headers: { 107 + 'Content-Type': 'application/json' 108 + }, 109 + body: JSON.stringify({ query, variables }) 110 + }); 111 + 112 + if (!response.ok) { 113 + throw new Error(`GraphQL request failed: ${response.status}`); 114 + } 115 + 116 + return response.json(); 117 + } 118 + 119 + setEndpoint(endpoint) { 120 + this.#endpoint = endpoint; 121 + } 122 + } 123 + 124 + export const grainApi = new GrainApiService();