From 696ef4f1fa3d966752ce65180245d8abc70f1610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Pradel?= Date: Wed, 13 Jul 2022 12:10:22 +0200 Subject: [PATCH] feat: show creator + badge on user profile (#597) --- .changeset/bright-kids-juggle.md | 5 ++ server/src/api/modules/users/[userId].test.ts | 60 +++++++++++++++ server/src/api/modules/users/[userId].ts | 74 +++++++++++++++++++ server/src/api/modules/users/me.ts | 1 - server/src/server.ts | 3 + sigle/next.config.js | 5 ++ sigle/public/img/badges/creatorPlusDark.svg | 7 ++ sigle/public/img/badges/creatorPlusLight.svg | 7 ++ sigle/src/hooks/users.ts | 31 ++++++++ .../publicHome/components/PublicHome.tsx | 37 ++++++++++ sigle/src/pages/api/feed/[username].test.ts | 2 +- 11 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 .changeset/bright-kids-juggle.md create mode 100644 server/src/api/modules/users/[userId].test.ts create mode 100644 server/src/api/modules/users/[userId].ts create mode 100644 sigle/public/img/badges/creatorPlusDark.svg create mode 100644 sigle/public/img/badges/creatorPlusLight.svg diff --git a/.changeset/bright-kids-juggle.md b/.changeset/bright-kids-juggle.md new file mode 100644 index 000000000..156a5358c --- /dev/null +++ b/.changeset/bright-kids-juggle.md @@ -0,0 +1,5 @@ +--- +'@sigle/app': minor +--- + +Display The Explorer Guild creator + badge on the profile page of a user. diff --git a/server/src/api/modules/users/[userId].test.ts b/server/src/api/modules/users/[userId].test.ts new file mode 100644 index 000000000..771cf40ba --- /dev/null +++ b/server/src/api/modules/users/[userId].test.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FastifyInstance } from 'fastify'; +import { TestBaseDB, TestDBUser } from '../../../jest/db'; +import { prisma } from '../../../prisma'; +import { redis } from '../../../redis'; +import { buildFastifyServer } from '../../../server'; + +let server: FastifyInstance; + +beforeAll(() => { + server = buildFastifyServer(); +}); + +afterAll(async () => { + await TestBaseDB.cleanup(); + await redis.quit(); + await prisma.$disconnect(); +}); + +beforeEach(async () => { + await prisma.user.deleteMany({}); + await prisma.subscription.deleteMany({}); +}); + +it('Should return public user without subscription', async () => { + const stacksAddress = 'SP3VCX5NFQ8VCHFS9M6N40ZJNVTRT4HZ62WFH5C4Q'; + const user = await TestDBUser.seedUser({ + stacksAddress, + }); + + const response = await server.inject({ + method: 'GET', + url: `/api/users/${stacksAddress}`, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ id: user.id, stacksAddress }); +}); + +it('Should return public user with subscription', async () => { + const stacksAddress = 'SP3VCX5NFQ8VCHFS9M6N40ZJNVTRT4HZ62WFH5C4Q'; + const user = await TestDBUser.seedUserWithSubscription({ + stacksAddress, + }); + + const response = await server.inject({ + method: 'GET', + url: `/api/users/${stacksAddress}`, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + id: user.id, + stacksAddress, + subscription: { + id: expect.any(String), + nftId: expect.any(Number), + }, + }); +}); diff --git a/server/src/api/modules/users/[userId].ts b/server/src/api/modules/users/[userId].ts new file mode 100644 index 000000000..0a4bcc753 --- /dev/null +++ b/server/src/api/modules/users/[userId].ts @@ -0,0 +1,74 @@ +import { FastifyInstance } from 'fastify'; +import { prisma } from '../../../prisma'; + +type GetUserByAddressResponse = { + id: string; + stacksAddress: string; + subscription?: { + id: string; + nftId: number; + }; +} | null; +const getUserByAddressResponseSchema = { + type: 'object', + nullable: true, + properties: { + id: { type: 'string' }, + stacksAddress: { type: 'string' }, + subscription: { + type: 'object', + nullable: true, + properties: { + id: { type: 'string' }, + nftId: { type: 'number' }, + }, + }, + }, +}; + +export async function createGetUserByAddressEndpoint(fastify: FastifyInstance) { + return fastify.get<{ + Reply: GetUserByAddressResponse; + Params: { userAddress: string }; + }>( + '/api/users/:userAddress', + { + schema: { + response: { + 200: getUserByAddressResponseSchema, + }, + }, + }, + async (req, res) => { + const { userAddress } = req.params; + + const user = await prisma.user.findUnique({ + where: { stacksAddress: userAddress }, + select: { + id: true, + stacksAddress: true, + subscriptions: { + select: { + id: true, + nftId: true, + }, + where: { + status: 'ACTIVE', + }, + take: 1, + }, + }, + }); + + return res.send( + user + ? { + id: user.id, + stacksAddress: user.stacksAddress, + subscription: user.subscriptions[0], + } + : null + ); + } + ); +} diff --git a/server/src/api/modules/users/me.ts b/server/src/api/modules/users/me.ts index 287ca21ba..228d3f194 100644 --- a/server/src/api/modules/users/me.ts +++ b/server/src/api/modules/users/me.ts @@ -7,7 +7,6 @@ type GetUserMeResponse = { }; const getUserMeResponseSchema = { type: 'object', - nullable: true, properties: { id: { type: 'string' }, stacksAddress: { type: 'string' }, diff --git a/server/src/server.ts b/server/src/server.ts index b138ec6e2..f494ed9d5 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -12,6 +12,7 @@ import { fastifyAuthPlugin } from './api/plugins/auth'; import { createSubscriptionCreatorPlusEndpoint } from './api/modules/subscriptions/creatorPlus'; import { createGetSubscriptionEndpoint } from './api/modules/subscriptions/getSubscription'; import { createGetUserMeEndpoint } from './api/modules/users/me'; +import { createGetUserByAddressEndpoint } from './api/modules/users/[userId]'; export const buildFastifyServer = ( opts: FastifyServerOptions = {} @@ -103,6 +104,8 @@ export const buildFastifyServer = ( }); }); + createGetUserByAddressEndpoint(fastify); + /** * All the protected routes must be placed there. */ diff --git a/sigle/next.config.js b/sigle/next.config.js index c7f49c842..2bf16aeb4 100644 --- a/sigle/next.config.js +++ b/sigle/next.config.js @@ -9,6 +9,11 @@ const { withPlausibleProxy } = require('next-plausible'); dotenv.config(); const nextConfig = { + experimental: { + images: { + allowFutureImage: true, + }, + }, swcMinify: true, env: { APP_URL: process.env.APP_URL, diff --git a/sigle/public/img/badges/creatorPlusDark.svg b/sigle/public/img/badges/creatorPlusDark.svg new file mode 100644 index 000000000..8df88245c --- /dev/null +++ b/sigle/public/img/badges/creatorPlusDark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/sigle/public/img/badges/creatorPlusLight.svg b/sigle/public/img/badges/creatorPlusLight.svg new file mode 100644 index 000000000..b2143ac78 --- /dev/null +++ b/sigle/public/img/badges/creatorPlusLight.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/sigle/src/hooks/users.ts b/sigle/src/hooks/users.ts index 8db4ca195..3b48796c3 100644 --- a/sigle/src/hooks/users.ts +++ b/sigle/src/hooks/users.ts @@ -27,3 +27,34 @@ export const useGetUserMe = ( }, options ); + +type UserByAddressResponse = { + id: string; + stacksAddress: string; + subscription?: { + id: string; + nftId: number; + }; +} | null; + +export const useGetUserByAddress = ( + stacksAddress: string, + options: UseQueryOptions = {} +) => + useQuery( + ['get-user-by-address', stacksAddress], + async () => { + const res = await fetch( + `${sigleConfig.apiUrl}/api/users/${stacksAddress}`, + { + method: 'GET', + } + ); + const json = await res.json(); + if (!res.ok) { + throw json; + } + return json; + }, + options + ); diff --git a/sigle/src/modules/publicHome/components/PublicHome.tsx b/sigle/src/modules/publicHome/components/PublicHome.tsx index 33a572090..c69b34d32 100644 --- a/sigle/src/modules/publicHome/components/PublicHome.tsx +++ b/sigle/src/modules/publicHome/components/PublicHome.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { NextSeo } from 'next-seo'; +import Image from 'next/future/image'; +import { useTheme } from 'next-themes'; import { StoryFile, SettingsFile } from '../../../types'; import { PoweredBy } from '../../publicStory/PoweredBy'; import { AppHeader } from '../../layout/components/AppHeader'; @@ -13,6 +15,9 @@ import { TabsContent, TabsList, Typography, + Tooltip, + TooltipTrigger, + TooltipContent, } from '../../../ui'; import { sigleConfig } from '../../../config'; import { styled } from '../../../stitches.config'; @@ -25,6 +30,7 @@ import { import { generateAvatar } from '../../../utils/boringAvatar'; import { useFeatureFlags } from '../../../utils/featureFlags'; import { StoryCard } from '../../storyCard/StoryCard'; +import { useGetUserByAddress } from '../../../hooks/users'; const ExtraInfoLink = styled('a', { color: '$gray9', @@ -102,8 +108,10 @@ interface PublicHomeProps { } export const PublicHome = ({ file, settings, userInfo }: PublicHomeProps) => { + const { resolvedTheme } = useTheme(); const { user } = useAuth(); const { isExperimentalFollowEnabled } = useFeatureFlags(); + const { data: userInfoByAddress } = useGetUserByAddress(userInfo.address); const { data: userFollowing } = useGetUserFollowing({ enabled: !!user && userInfo.username !== user.username, }); @@ -208,6 +216,35 @@ export const PublicHome = ({ file, settings, userInfo }: PublicHomeProps) => { > {userInfo.username} + {userInfoByAddress?.subscription && ( + + + + Creator + badge + + + + Creator + Explorer #{userInfoByAddress.subscription.nftId} + + + )} {settings.siteUrl && ( diff --git a/sigle/src/pages/api/feed/[username].test.ts b/sigle/src/pages/api/feed/[username].test.ts index a0097cffd..67bdb3697 100644 --- a/sigle/src/pages/api/feed/[username].test.ts +++ b/sigle/src/pages/api/feed/[username].test.ts @@ -51,7 +51,7 @@ describe('test feed api', () => { item: expect.any(Array), lastBuildDate: expect.any(String), link: 'https://app.sigle.io/sigleapp.id.blockstack', - title: 'Sigle official blog', + title: 'Sigle', }); // Last items should never change expect(