├── .eslintrc.json ├── .prettierrc ├── bun.lockb ├── public ├── icons │ ├── logo-192x192.png │ ├── logo-384x384.png │ ├── logo-512x512.png │ └── logo-800x800.png ├── manifest.json ├── logo.svg ├── sw.js ├── nothing-to-show.svg └── workbox-c06b064f.js ├── vercel.json ├── postcss.config.js ├── components ├── providers │ ├── session-provider.tsx │ ├── theme-provider.tsx │ ├── modal-provider.tsx │ ├── wallet-context-provider.tsx │ ├── trpc-provider.tsx │ └── index.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── sonner.tsx │ ├── tooltip.tsx │ ├── switch.tsx │ ├── avatar.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── alert-dialog.tsx │ └── dropdown-menu.tsx ├── page-wrapper.tsx ├── page-heading.tsx ├── logo.tsx ├── page-heading-skeleton.tsx ├── user-avatar.tsx ├── nothing-to-show.tsx ├── nft-card │ ├── favorite-gradient-icon.tsx │ ├── token-details.tsx │ ├── index.tsx │ ├── toggle-visibility-widget.tsx │ └── nft-card-actions.tsx ├── navbar │ ├── auth-trigger.tsx │ ├── index.tsx │ ├── nav-route.tsx │ ├── user-action-items.tsx │ ├── theme-switch.tsx │ └── user-actions.tsx ├── footer.tsx ├── hover-tip.tsx ├── modals │ ├── comment-modal.tsx │ ├── confirm-delete-modal.tsx │ ├── auth-modal.tsx │ ├── upload-modal.tsx │ └── share-modal.tsx ├── near-wallet-selector.tsx ├── create-button.tsx └── comment-form.tsx ├── app ├── _trpc │ ├── client.ts │ └── server-client.ts ├── (root) │ ├── explore │ │ ├── loading.tsx │ │ └── page.tsx │ ├── my-space │ │ ├── loading.tsx │ │ ├── favorites │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── create │ │ ├── _components │ │ │ ├── create-asset.tsx │ │ │ ├── image-dropzone.tsx │ │ │ └── asset-form.tsx │ │ └── page.tsx │ ├── nft │ │ └── [tokenId] │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── _components │ │ │ ├── token-article.tsx │ │ │ ├── token-article-comments.tsx │ │ │ ├── token-article-header.tsx │ │ │ ├── comment.tsx │ │ │ └── comment-section.tsx │ ├── layout.tsx │ └── page.tsx ├── api │ ├── trpc │ │ └── [trpc] │ │ │ └── route.ts │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── globals.css └── layout.tsx ├── lib ├── utils.ts ├── db.ts └── config.ts ├── hooks └── use-is-mounted.ts ├── server ├── routers │ ├── user-router.ts │ ├── comment-router.ts │ ├── favorites-router.ts │ └── nft-router.ts ├── index.ts └── trpc.ts ├── .env.example ├── store ├── use-auth-modal.ts ├── use-comment-modal.ts ├── use-confirm-modal.ts ├── use-share-modal.ts └── use-upload-modal.ts ├── utils └── trpc │ ├── nft │ ├── get-published-tokens.ts │ ├── get-owned-nft.ts │ ├── get-token-by-id.ts │ ├── toggle-visibility.ts │ └── link-nft.ts │ ├── comment │ ├── get-comment-count.ts │ ├── get-comments.ts │ ├── add-comment.ts │ └── delete-comment.ts │ ├── favorites │ ├── get-favorite-count.ts │ ├── get-favorites.ts │ ├── is-favorite.ts │ └── toggle-favorite.ts │ └── user │ ├── get-auth-profile.ts │ └── link-wallet.ts ├── components.json ├── next.config.mjs ├── .gitignore ├── tsconfig.json ├── package.json ├── tailwind.config.ts ├── prisma └── schema.prisma └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naviava/minter-app/HEAD/bun.lockb -------------------------------------------------------------------------------- /public/icons/logo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naviava/minter-app/HEAD/public/icons/logo-192x192.png -------------------------------------------------------------------------------- /public/icons/logo-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naviava/minter-app/HEAD/public/icons/logo-384x384.png -------------------------------------------------------------------------------- /public/icons/logo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naviava/minter-app/HEAD/public/icons/logo-512x512.png -------------------------------------------------------------------------------- /public/icons/logo-800x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naviava/minter-app/HEAD/public/icons/logo-800x800.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "deploymentEnabled": { 4 | "dev": false 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /components/providers/session-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SessionProvider } from "next-auth/react"; 3 | export default SessionProvider; 4 | -------------------------------------------------------------------------------- /app/_trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { type AppRouter } from "~/server"; 2 | import { createTRPCReact } from "@trpc/react-query"; 3 | 4 | export const trpc = createTRPCReact({}); 5 | -------------------------------------------------------------------------------- /app/(root)/explore/loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageHeadingSkeleton } from "~/components/page-heading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(root)/my-space/loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageHeadingSkeleton } from "~/components/page-heading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/(root)/my-space/favorites/loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageHeadingSkeleton } from "~/components/page-heading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /hooks/use-is-mounted.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | export function useIsMounted() { 6 | const [isMounted, setIsMounted] = useState(false); 7 | useEffect(() => setIsMounted(true), []); 8 | return isMounted; 9 | } 10 | -------------------------------------------------------------------------------- /app/_trpc/server-client.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "~/server"; 2 | import { httpBatchLink } from "@trpc/client"; 3 | 4 | export const serverClient = appRouter.createCaller({ 5 | links: [ 6 | httpBatchLink({ url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/trpc` }), 7 | ], 8 | }); 9 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | export const db = globalThis.prisma || new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV !== "production") { 10 | globalThis.prisma = db; 11 | } 12 | -------------------------------------------------------------------------------- /server/routers/user-router.ts: -------------------------------------------------------------------------------- 1 | import { router } from "~/server/trpc"; 2 | 3 | import { linkWallet } from "~/utils/trpc/user/link-wallet"; 4 | import { getAuthProfile } from "~/utils/trpc/user/get-auth-profile"; 5 | 6 | export const userRouter = router({ 7 | getAuthProfile, 8 | linkWallet, 9 | }); 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SITE_URL="http://localhost:3000" 2 | DATABASE_URL="postgresql://dbadmin:Password123@localhost:6500/mintsaga-db?schema=public" 3 | 4 | NEXTAUTH_SECRET=[ENTER_YOUR_SECRET_HERE] 5 | GITHUB_ID=[YOUR_GITHUB_ID] 6 | GITHUB_SECRET=[YOUR_GITHUB_SECRET] 7 | GOOGLE_ID=[YOUR_GOOGLE_ID] 8 | GOOGLE_SECRET=[YOUR_GOOGLE_SECRET] -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /app/(root)/create/_components/create-asset.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ImageDropzone } from "./image-dropzone"; 4 | import { AssetForm } from "./asset-form"; 5 | 6 | export function CreateAsset() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /store/use-auth-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type AuthModalStore = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useAuthModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /components/page-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | interface IProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export function PageWrapper({ children, className }: IProps) { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /utils/trpc/nft/get-published-tokens.ts: -------------------------------------------------------------------------------- 1 | import { db } from "~/lib/db"; 2 | import { publicProcedure } from "~/server/trpc"; 3 | 4 | export const getPublishedTokens = publicProcedure.query(async () => { 5 | const publishedTokens = await db.nft.findMany({ 6 | where: { isPublished: true }, 7 | orderBy: { createdAt: "desc" }, 8 | }); 9 | return publishedTokens; 10 | }); 11 | -------------------------------------------------------------------------------- /app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "~/server"; 2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 3 | 4 | function handler(req: Request) { 5 | return fetchRequestHandler({ 6 | endpoint: "/api/trpc", 7 | req, 8 | router: appRouter, 9 | createContext: () => ({}), 10 | }); 11 | } 12 | 13 | export { handler as GET, handler as POST }; 14 | -------------------------------------------------------------------------------- /app/(root)/nft/[tokenId]/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DNA } from "react-loader-spinner"; 4 | import { PageWrapper } from "~/components/page-wrapper"; 5 | 6 | export default function ArticleLoading() { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | const APP_NAME = "MintSaga"; 2 | const APP_DEFAULT_TITLE = "MintSaga"; 3 | const APP_TITLE_TEMPLATE = "%s | MintSaga"; 4 | const APP_DESCRIPTION = 5 | "Preserve and share historical events through captivating images, titles, and descriptions on the blockchain."; 6 | 7 | export const config = { 8 | APP_NAME, 9 | APP_DEFAULT_TITLE, 10 | APP_TITLE_TEMPLATE, 11 | APP_DESCRIPTION, 12 | } as const; 13 | -------------------------------------------------------------------------------- /app/(root)/my-space/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { serverClient } from "~/app/_trpc/server-client"; 3 | 4 | interface IProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default async function MySpaceLayout({ children }: IProps) { 9 | const user = await serverClient.user.getAuthProfile(); 10 | if (!user) return redirect("/explore"); 11 | return <>{children}; 12 | } 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /store/use-comment-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type CommentModalStore = { 4 | id: string; 5 | isOpen: boolean; 6 | onOpen: (id: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useCommentModal = create((set) => ({ 11 | id: "", 12 | isOpen: false, 13 | onOpen: (id: string) => set({ isOpen: true, id }), 14 | onClose: () => set({ isOpen: false, id: "" }), 15 | })); 16 | -------------------------------------------------------------------------------- /store/use-confirm-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type CommentModalStore = { 4 | id: string; 5 | isOpen: boolean; 6 | onOpen: (id: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useCommentModal = create((set) => ({ 11 | id: "", 12 | isOpen: false, 13 | onOpen: (id: string) => set({ isOpen: true, id }), 14 | onClose: () => set({ isOpen: false, id: "" }), 15 | })); 16 | -------------------------------------------------------------------------------- /store/use-share-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type ShareModalStore = { 4 | href: string; 5 | isOpen: boolean; 6 | onOpen: (href: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useShareModal = create((set) => ({ 11 | href: "", 12 | isOpen: false, 13 | onOpen: (href: string) => set({ isOpen: true, href }), 14 | onClose: () => set({ isOpen: false, href: "" }), 15 | })); 16 | -------------------------------------------------------------------------------- /utils/trpc/comment/get-comment-count.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { db } from "~/lib/db"; 3 | import { publicProcedure } from "~/server/trpc"; 4 | 5 | export const getCommentCount = publicProcedure 6 | .input(z.string().min(1, { message: "Token ID is required." })) 7 | .query(async ({ input: nftId }) => { 8 | const commentCount = await db.comment.count({ 9 | where: { nftId }, 10 | }); 11 | return commentCount; 12 | }); 13 | -------------------------------------------------------------------------------- /components/page-heading.tsx: -------------------------------------------------------------------------------- 1 | interface IProps { 2 | label: string; 3 | tagline?: string; 4 | } 5 | 6 | export function PageHeading({ label, tagline }: IProps) { 7 | return ( 8 |
9 |

10 | {label} 11 |

12 | {tagline &&

{tagline}

} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /utils/trpc/favorites/get-favorite-count.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { db } from "~/lib/db"; 4 | import { publicProcedure } from "~/server/trpc"; 5 | 6 | export const getFavoriteCount = publicProcedure 7 | .input(z.string().min(1, { message: "Token ID is required." })) 8 | .query(async ({ input: nftId }) => { 9 | const favoriteCount = await db.favorite.count({ 10 | where: { nftId }, 11 | }); 12 | return favoriteCount; 13 | }); 14 | -------------------------------------------------------------------------------- /utils/trpc/favorites/get-favorites.ts: -------------------------------------------------------------------------------- 1 | import { db } from "~/lib/db"; 2 | import { privateProcedure } from "~/server/trpc"; 3 | 4 | export const getFavorites = privateProcedure.query(async ({ ctx }) => { 5 | const { user } = ctx; 6 | 7 | const favTokens = await db.favorite.findMany({ 8 | where: { userId: user.id }, 9 | include: { nft: true }, 10 | orderBy: { 11 | nft: { createdAt: "desc" }, 12 | }, 13 | }); 14 | return favTokens; 15 | }); 16 | -------------------------------------------------------------------------------- /utils/trpc/comment/get-comments.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { db } from "~/lib/db"; 3 | import { publicProcedure } from "~/server/trpc"; 4 | 5 | export const getComments = publicProcedure 6 | .input(z.string().min(1, { message: "Token ID is required." })) 7 | .query(async ({ input: nftId }) => { 8 | const comments = await db.comment.findMany({ 9 | where: { nftId }, 10 | include: { user: true }, 11 | }); 12 | return comments; 13 | }); 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import withPWAInit from "@ducanh2912/next-pwa"; 2 | 3 | const withPWA = withPWAInit({ 4 | dest: "public", 5 | // disable: process.env.NODE_ENV === "development", 6 | }); 7 | 8 | /** @type {import('next').NextConfig} */ 9 | const nextConfig = { 10 | images: { 11 | remotePatterns: [ 12 | { 13 | protocol: "https", 14 | hostname: "arweave.net", 15 | }, 16 | ], 17 | }, 18 | }; 19 | 20 | export default withPWA(nextConfig); 21 | -------------------------------------------------------------------------------- /components/providers/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | import { CommentModal } from "../modals/comment-modal"; 2 | import { AuthModal } from "~/components/modals/auth-modal"; 3 | import { ShareModal } from "~/components/modals/share-modal"; 4 | import { UploadModal } from "~/components/modals/upload-modal"; 5 | 6 | export function ModalProvider() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /server/routers/comment-router.ts: -------------------------------------------------------------------------------- 1 | import { router } from "~/server/trpc"; 2 | 3 | import { addComment } from "~/utils/trpc/comment/add-comment"; 4 | import { getComments } from "~/utils/trpc/comment/get-comments"; 5 | import { deleteComment } from "~/utils/trpc/comment/delete-comment"; 6 | import { getCommentCount } from "~/utils/trpc/comment/get-comment-count"; 7 | 8 | export const commentRouter = router({ 9 | addComment, 10 | getComments, 11 | deleteComment, 12 | getCommentCount, 13 | }); 14 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from "./trpc"; 2 | 3 | import { nftRouter } from "./routers/nft-router"; 4 | import { userRouter } from "./routers/user-router"; 5 | import { commentRouter } from "./routers/comment-router"; 6 | import { favoritesRouter } from "./routers/favorites-router"; 7 | 8 | export const appRouter = router({ 9 | nft: nftRouter, 10 | user: userRouter, 11 | comment: commentRouter, 12 | favorites: favoritesRouter, 13 | }); 14 | 15 | export type AppRouter = typeof appRouter; 16 | -------------------------------------------------------------------------------- /server/routers/favorites-router.ts: -------------------------------------------------------------------------------- 1 | import { router } from "~/server/trpc"; 2 | 3 | import { isFavorite } from "~/utils/trpc/favorites/is-favorite"; 4 | import { getFavorites } from "~/utils/trpc/favorites/get-favorites"; 5 | import { toggleFavorite } from "~/utils/trpc/favorites/toggle-favorite"; 6 | import { getFavoriteCount } from "~/utils/trpc/favorites/get-favorite-count"; 7 | 8 | export const favoritesRouter = router({ 9 | isFavorite, 10 | getFavorites, 11 | toggleFavorite, 12 | getFavoriteCount, 13 | }); 14 | -------------------------------------------------------------------------------- /components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | export function Logo() { 7 | return ( 8 | 9 |
10 | Logo 11 |

12 | MintSaga 13 |

14 |
15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /utils/trpc/user/get-auth-profile.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth"; 2 | 3 | import { db } from "~/lib/db"; 4 | import { publicProcedure } from "~/server/trpc"; 5 | 6 | export const getAuthProfile = publicProcedure.query(async () => { 7 | const session = await getServerSession(); 8 | if (!session || !session.user || !session.user.email) { 9 | return null; 10 | } 11 | 12 | const user = await db.user.findUnique({ 13 | where: { email: session.user.email }, 14 | }); 15 | 16 | if (!user) return null; 17 | return user; 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /server/routers/nft-router.ts: -------------------------------------------------------------------------------- 1 | import { router } from "~/server/trpc"; 2 | 3 | import { linkNft } from "~/utils/trpc/nft/link-nft"; 4 | import { getOwnedNft } from "~/utils/trpc/nft/get-owned-nft"; 5 | import { getTokenById } from "~/utils/trpc/nft/get-token-by-id"; 6 | import { toggleVisibility } from "~/utils/trpc/nft/toggle-visibility"; 7 | import { getPublishedTokens } from "~/utils/trpc/nft/get-published-tokens"; 8 | 9 | export const nftRouter = router({ 10 | linkNft, 11 | getOwnedNft, 12 | getTokenById, 13 | toggleVisibility, 14 | getPublishedTokens, 15 | }); 16 | -------------------------------------------------------------------------------- /utils/trpc/nft/get-owned-nft.ts: -------------------------------------------------------------------------------- 1 | import { db } from "~/lib/db"; 2 | import { privateProcedure } from "~/server/trpc"; 3 | 4 | export const getOwnedNft = privateProcedure.query(async ({ ctx }) => { 5 | const { user } = ctx; 6 | if (!user) { 7 | return null; 8 | } 9 | 10 | const userTokens = await db.wallet.findFirst({ 11 | where: { 12 | userId: user.id, 13 | }, 14 | include: { 15 | nft: { 16 | orderBy: { createdAt: "desc" }, 17 | }, 18 | }, 19 | }); 20 | if (!userTokens) return []; 21 | return userTokens.nft; 22 | }); 23 | -------------------------------------------------------------------------------- /components/page-heading-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "~/components/ui/skeleton"; 2 | import { PageWrapper } from "~/components/page-wrapper"; 3 | 4 | export function PageHeadingSkeleton() { 5 | return ( 6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; 2 | import { cn } from "~/lib/utils"; 3 | 4 | interface IProps { 5 | userName?: string | null; 6 | imageUrl?: string | null; 7 | className?: string; 8 | } 9 | 10 | export function UserAvatar({ imageUrl, userName, className }: IProps) { 11 | return ( 12 | 13 | 14 | 15 | {userName?.[0] || "A"} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/nothing-to-show.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface IProps { 4 | message: string; 5 | } 6 | 7 | export function NothingToShow({ message }: IProps) { 8 | return ( 9 |
10 |
11 | No NFTs linked to this account. 16 |
17 |

18 | {message} 19 |

20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /utils/trpc/favorites/is-favorite.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { db } from "~/lib/db"; 3 | import { privateProcedure } from "~/server/trpc"; 4 | 5 | export const isFavorite = privateProcedure 6 | .input(z.string().min(1, { message: "Token ID is required." })) 7 | .query(async ({ ctx, input: nftId }) => { 8 | const { user } = ctx; 9 | if (!user) { 10 | return false; 11 | } 12 | 13 | const existingFavorite = await db.favorite.findUnique({ 14 | where: { 15 | userId_nftId: { 16 | userId: user.id, 17 | nftId, 18 | }, 19 | }, 20 | }); 21 | return !!existingFavorite; 22 | }); 23 | -------------------------------------------------------------------------------- /components/providers/wallet-context-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MintbaseWalletContextProvider } from "@mintbase-js/react"; 4 | import "@near-wallet-selector/modal-ui/styles.css"; 5 | 6 | interface IProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | const MintbaseWalletSetup = { 11 | contractAddress: "hellovirtualworld.mintspace2.testnet", 12 | network: "testnet" as any, 13 | callbackUrl: "http://localhost:3000", 14 | }; 15 | 16 | export function WalletContextProvider({ children }: IProps) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(root)/nft/[tokenId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { PageWrapper } from "~/components/page-wrapper"; 4 | import { TokenArticle } from "./_components/token-article"; 5 | 6 | import { serverClient } from "~/app/_trpc/server-client"; 7 | 8 | interface IProps { 9 | params: { 10 | tokenId: string; 11 | }; 12 | } 13 | 14 | export default async function TokenIdPage({ params }: IProps) { 15 | const token = await serverClient.nft.getTokenById(params.tokenId); 16 | if (!token.isPublished) return redirect("/explore"); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MintSaga", 3 | "short_name": "MintSaga", 4 | "icons": [ 5 | { 6 | "src": "/icons/logo-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "/icons/logo-384x384.png", 13 | "sizes": "384x384", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icons/logo-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ], 22 | "theme_color": "#FFFFFF", 23 | "background_color": "#FFFFFF", 24 | "start_url": "/explore", 25 | "display": "standalone", 26 | "orientation": "portrait" 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "~/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /utils/trpc/nft/get-token-by-id.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { z } from "zod"; 3 | import { db } from "~/lib/db"; 4 | import { publicProcedure } from "~/server/trpc"; 5 | 6 | export const getTokenById = publicProcedure 7 | .input(z.string().min(1, { message: "Token ID is required." })) 8 | .query(async ({ input: tokenId }) => { 9 | const existingToken = await db.nft.findUnique({ 10 | where: { id: tokenId }, 11 | include: { 12 | wallet: { 13 | include: { user: true }, 14 | }, 15 | }, 16 | }); 17 | if (!existingToken) { 18 | throw new TRPCError({ 19 | code: "NOT_FOUND", 20 | message: "Token not found.", 21 | }); 22 | } 23 | return existingToken; 24 | }); 25 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "~/components/navbar"; 2 | import { Footer } from "~/components/footer"; 3 | 4 | interface IProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default function LandingLayout({ children }: IProps) { 9 | return ( 10 |
11 |
12 |
13 | 14 |
{children}
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "~/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |