├── .editorconfig
├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── app
├── api
│ └── upload
│ │ └── ai
│ │ └── route.ts
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── content.tsx
├── empty-state.tsx
├── get-ai.tsx
├── get-button.tsx
├── get-gifs.tsx
├── get-illustrations.tsx
├── get-pexels-videos.tsx
├── get-photos.tsx
├── get-vectors.tsx
├── header.tsx
├── icons.tsx
├── main-nav.tsx
├── media
│ ├── gif.tsx
│ ├── illustration.tsx
│ ├── photo.tsx
│ └── video.tsx
├── messages
│ ├── fetch-error-message.tsx
│ └── save-error-message.tsx
├── overlay.tsx
├── site-header.tsx
├── theme-provider.tsx
├── theme-toggle.tsx
└── ui
│ ├── alert.tsx
│ ├── button.tsx
│ ├── dialog.tsx
│ ├── input.tsx
│ ├── select.tsx
│ └── tabs.tsx
├── config
└── site.ts
├── hooks
└── useDebouncedValue.tsx
├── lib
├── data.ts
├── fonts.ts
├── types.ts
└── utils.ts
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── next.svg
├── thirteen.svg
└── vercel.svg
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.tsbuildinfo
├── utils
└── media-fetch.utils.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_UNSPLASH_KEY=
2 | NEXT_PUBLIC_PEXELS_KEY=
3 | NEXT_PUBLIC_PIXABAY_KEY=
4 | NEXT_PUBLIC_OPENAI_API_KEY=
5 | NEXT_PUBLIC_GIPHY_KEY=
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/*
2 | .cache
3 | public
4 | node_modules
5 | *.esm.js
6 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-html-link-for-pages": "off",
12 | "react/jsx-key": "off",
13 | "tailwindcss/no-custom-classname": "off"
14 | },
15 | "settings": {
16 | "tailwindcss": {
17 | "callees": ["cn"],
18 | "config": "tailwind.config.js"
19 | },
20 | "next": {
21 | "rootDir": ["./"]
22 | }
23 | },
24 | "overrides": [
25 | {
26 | "files": ["*.ts", "*.tsx"],
27 | "parser": "@typescript-eslint/parser"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/.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 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | .contentlayer
36 | .env
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | cache
2 | .cache
3 | package.json
4 | package-lock.json
5 | public
6 | CHANGELOG.md
7 | .yarn
8 | dist
9 | node_modules
10 | .next
11 | build
12 | .contentlayer
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "../../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Cosmic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cosmic Media
2 |
3 | Search millions of high-quality, royalty-free stock photos, videos, images, and vectors from one convenient interface. Includes popular online media services: Unsplash, Pexels, Giphy, and Pixabay as well as OpenAI image generation from prompt. [Try it here](https://cosmicmedia.vercel.app/).
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## How to use it
13 | You can use Cosmic Media to search and download media manually, or you can install it in your [Cosmic](https://www.cosmicjs.com/) project as an extension to save media directly in your project.
14 |
15 |
16 | ## How to install in Cosmic
17 |
18 | 1. [Log in to Cosmic](https://app.cosmicjs.com/login).
19 | 2. Go to _Project > Extensions_.
20 | 3. Find this extension and click "Install".
21 |
22 | ## Service keys
23 |
24 | The deployed app uses default API keys for Unsplash, Giphy, Pexels, Pixaby, and OpenAI. If you run into API rate-limit issues, you can update these to your own keys:
25 |
26 | 1. `unsplash_key` Register for a key [here](https://unsplash.com/developers).
27 | 2. `pexels_key` Register for a key [here](https://www.pexels.com/api).
28 | 3. `pixabay_key` Register for a key [here](https://pixabay.com/service/about/api)
29 | 4. `openai_key` Register for a key [here](https://platform.openai.com)
30 | 5. `giphy_key` Register for a key [here](https://developers.giphy.com)
31 |
32 | ### Using service keys
33 | Keys can be provided to the app in one of the following ways:
34 |
35 | 1. As query params in the URL. For example: `?unsplash_key=YOUR_UNSPLASH_KEY&pexels_key=YOUR_PEXELS_KEY`
36 | 2. Using the `.env` file. See the `.env.example` file for env var format.
37 | 3. If installed in Cosmic as an extension, go to Cosmic Media extension settings page by going to _Extensions > Cosmic Media > Settings_, find the Query Parameters section and update the following query params to your own keys:
38 |
39 |
40 |
41 |
42 |
43 | ## Run locally
44 |
45 | ```bash
46 | git clone https://github.com/cosmicjs/cosmic-media-extension
47 | cd cosmic-media-extension
48 | yarn
49 | yarn dev
50 | ```
51 |
52 | ## Built with
53 | - [shadcn/ui](https://github.com/shadcn-ui/ui)
54 | - Next.js 13 App Directory
55 | - Radix UI Primitives
56 | - Tailwind CSS
57 | - Icons from [Lucide](https://lucide.dev)
58 | - Dark mode with `next-themes`
59 | - Tailwind CSS class sorting, merging and linting.
60 |
61 | ## License
62 |
63 | Licensed under the [MIT license](https://github.com/cosmicjs/cosmic-media-extension/blob/main/LICENSE.md).
64 |
--------------------------------------------------------------------------------
/app/api/upload/ai/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server"
2 | import { createBucketClient } from "@cosmicjs/sdk"
3 | import { z } from "zod"
4 |
5 | import { COSMIC_ENV } from "@/lib/data"
6 |
7 | const RequestBodySchema = z.object({
8 | bucket: z.object({
9 | bucket_slug: z.string(),
10 | read_key: z.string(),
11 | write_key: z.string(),
12 | }),
13 | url: z.string(),
14 | slug: z.string(),
15 | })
16 |
17 | export async function POST(request: Request) {
18 | const requestBody = await request.json()
19 |
20 | const parsedRequestBody = RequestBodySchema.safeParse(requestBody)
21 | if (parsedRequestBody.success === false) {
22 | return NextResponse.json(
23 | {
24 | message: "Request validation failed",
25 | error: parsedRequestBody.error,
26 | },
27 | { status: 400 }
28 | )
29 | }
30 |
31 | const body = parsedRequestBody.data
32 |
33 | const cosmic = createBucketClient({
34 | bucketSlug: body.bucket.bucket_slug,
35 | readKey: body.bucket.read_key,
36 | writeKey: body.bucket.write_key,
37 | apiEnvironment: COSMIC_ENV,
38 | })
39 |
40 | try {
41 | const response = await fetch(body.url)
42 | if (response.status !== 200) {
43 | throw new Error()
44 | }
45 |
46 | const data = await response.arrayBuffer()
47 |
48 | const buffer = Buffer.from(data)
49 |
50 | const media = {
51 | buffer,
52 | originalname: body.slug + ".jpg",
53 | }
54 | await cosmic.media.insertOne({ media })
55 |
56 | return NextResponse.json({ message: "AI media inserted successfully" })
57 | } catch (err) {
58 | console.error(err)
59 | return NextResponse.json(
60 | { message: "There was an error performing the request" },
61 | { status: 500 }
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css"
2 | import { Metadata } from "next"
3 |
4 | import { siteConfig } from "@/config/site"
5 | import { fontSans } from "@/lib/fonts"
6 | import { cn } from "@/lib/utils"
7 | import { ThemeProvider } from "@/components/theme-provider"
8 |
9 | export const metadata: Metadata = {
10 | title: {
11 | default: siteConfig.name,
12 | template: `%s - ${siteConfig.name}`,
13 | },
14 | description: siteConfig.description,
15 | themeColor: [
16 | { media: "(prefers-color-scheme: light)", color: "white" },
17 | { media: "(prefers-color-scheme: dark)", color: "black" },
18 | ],
19 | icons: {
20 | icon: "/favicon.ico",
21 | shortcut: "/favicon-16x16.png",
22 | apple: "/apple-touch-icon.png",
23 | },
24 | openGraph: {
25 | images: [
26 | "https://imgix.cosmicjs.com/eee1bf40-3799-11ee-be3f-55e1752361d4-2.png?w=1300&auto=compression",
27 | ],
28 | },
29 | }
30 |
31 | interface RootLayoutProps {
32 | children: React.ReactNode
33 | }
34 |
35 | export default function RootLayout({ children }: RootLayoutProps) {
36 | return (
37 | <>
38 |
39 |
46 |
47 |
50 |
51 |
52 |
53 | >
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Content from "@/components/content"
2 | import { SiteHeader } from "@/components/site-header"
3 |
4 | export default function IndexPage({
5 | searchParams,
6 | }: {
7 | searchParams: {
8 | bucket_slug: string
9 | read_key: string
10 | write_key: string
11 | location: string
12 | }
13 | }) {
14 | return (
15 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "tailwind": {
5 | "config": "tailwind.config.js",
6 | "css": "styles/globals.css",
7 | "baseColor": "zinc",
8 | "cssVariables": false
9 | },
10 | "rsc": true,
11 | "aliases": {
12 | "utils": "@/lib/utils",
13 | "components": "@/components"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/components/content.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useMemo, useState } from "react"
4 | import isMobile from "is-mobile"
5 | import { Brush, Camera, Laugh, PenTool, Video, Wand2 } from "lucide-react"
6 |
7 | import { Bucket } from "@/lib/types"
8 | import useDebouncedValue from "@/hooks/useDebouncedValue"
9 | import {
10 | Select,
11 | SelectContent,
12 | SelectGroup,
13 | SelectItem,
14 | SelectTrigger,
15 | SelectValue,
16 | } from "@/components/ui/select"
17 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
18 |
19 | import GetAI from "./get-ai"
20 | import GetGifs from "./get-gifs"
21 | import GetIllustrations from "./get-illustrations"
22 | import GetPexelsVideos from "./get-pexels-videos"
23 | import GetPhotos from "./get-photos"
24 | import GetVectors from "./get-vectors"
25 |
26 | type Query = string
27 | type SetQuery = React.Dispatch>
28 | type DebouncedQuery = string
29 |
30 | export const GlobalContext = createContext<{
31 | query: Query
32 | setQuery: SetQuery
33 | debouncedQuery: DebouncedQuery
34 | }>({
35 | query: "",
36 | setQuery: () => {},
37 | debouncedQuery: "",
38 | })
39 |
40 | export default function Content(bucket: Bucket) {
41 | const [selectedView, setSelectedView] = useState("photos")
42 | const [query, setQuery, debouncedQuery] = useDebouncedValue("")
43 |
44 | const globalContextValue = useMemo(
45 | () => ({
46 | query,
47 | setQuery,
48 | debouncedQuery,
49 | }),
50 | [query, setQuery, debouncedQuery]
51 | )
52 |
53 | const showMobile = useMemo(() => isMobile(), [])
54 |
55 | function handleTabClick() {
56 | document.getElementById("search-input")?.focus()
57 | }
58 |
59 | return (
60 |
61 |
62 | {showMobile && (
63 |
64 |
79 | {selectedView === "photos" && (
80 |
85 | )}
86 | {selectedView === "videos" && (
87 |
92 | )}
93 | {selectedView === "gifs" && (
94 |
99 | )}
100 | {selectedView === "ai" && (
101 |
106 | )}
107 | {selectedView === "illustrations" && (
108 |
113 | )}
114 | {selectedView === "vectors" && (
115 |
120 | )}
121 |
122 | )}
123 |
127 |
128 |
134 | Photos
135 |
136 |
142 | Videos
143 |
144 |
150 | Gifs
151 |
152 |
158 | AI images
159 |
160 |
166 | Illustrations
167 |
168 |
174 |
175 | Vectors
176 |
177 |
178 |
179 |
184 |
185 |
186 |
191 |
192 |
193 |
198 |
199 |
200 |
205 |
206 |
207 |
212 |
213 |
214 |
219 |
220 |
221 |
222 |
223 | )
224 | }
225 |
--------------------------------------------------------------------------------
/components/empty-state.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext } from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import { ArrowUpRight } from "lucide-react"
6 |
7 | import { Icons } from "@/components/icons"
8 |
9 | import { GlobalContext } from "./content"
10 |
11 | export default function EmptyState() {
12 | const { query, setQuery, debouncedQuery } = useContext(GlobalContext)
13 | const searchParams = useSearchParams()
14 | const location = searchParams.get("location")
15 | return (
16 |
17 |
18 | Use the search bar above to find royalty-free media from popular online
19 | media services.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {location !== "media-modal" && (
29 |
54 | )}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/components/get-ai.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext, useMemo, useState } from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import isMobile from "is-mobile"
6 | import { ExternalLink, Loader2, XCircle } from "lucide-react"
7 | import slugify from "slugify"
8 |
9 | import { OPEN_AI_KEY } from "@/lib/data"
10 | import { Bucket, MediaModalData, Photo, PhotoData } from "@/lib/types"
11 | import { cn, emptyModalData } from "@/lib/utils"
12 | import { Button, buttonVariants } from "@/components/ui/button"
13 | import {
14 | Dialog,
15 | DialogContent,
16 | DialogDescription,
17 | DialogHeader,
18 | } from "@/components/ui/dialog"
19 | import GetButton from "@/components/get-button"
20 | import { Icons } from "@/components/icons"
21 | import { FetchErrorMessage } from "@/components/messages/fetch-error-message"
22 | import { SaveErrorMessage } from "@/components/messages/save-error-message"
23 | import Overlay from "@/components/overlay"
24 |
25 | import { GlobalContext } from "./content"
26 | import EmptyState from "./empty-state"
27 | import Header from "./header"
28 | import PhotoOutput from "./media/photo"
29 | import Input from "./ui/input"
30 |
31 | const { Configuration, OpenAIApi } = require("openai")
32 |
33 | export default function GetAI(bucket: Bucket) {
34 | const { query, setQuery } = useContext(GlobalContext)
35 | const searchParams = useSearchParams()
36 | const openai_key = searchParams.get("openai_key") || OPEN_AI_KEY
37 | const configuration = new Configuration({
38 | apiKey: openai_key,
39 | })
40 | const openai = new OpenAIApi(configuration)
41 |
42 | const [photos, setPhotos] = useState([])
43 | const [generating, setGenerating] = useState(false)
44 | const [photoData, setPhotosData] = useState({
45 | adding_media: [],
46 | added_media: [],
47 | })
48 | const [saveError, setSaveError] = useState(false)
49 | const [serviceFetchError, setServiceFetchError] = useState()
50 | const [mediaModalData, setMediaModalData] =
51 | useState(emptyModalData)
52 | const showMobile = useMemo(() => isMobile(), [])
53 |
54 | async function handleAddAIPhotoToMedia(photo: Photo) {
55 | if (!bucket.bucket_slug) return setSaveError(true)
56 | const adding_media = [...(photoData.adding_media || []), photo.id]
57 | setPhotosData({ ...photoData, adding_media })
58 | const slug = slugify(query)
59 | const url = photo.url
60 | try {
61 | const res = await fetch("/api/upload/ai", {
62 | method: "POST",
63 | headers: {
64 | "Content-Type": "application/json",
65 | },
66 | body: JSON.stringify({ url, slug, bucket }),
67 | })
68 | const adding_media = photoData.adding_media?.filter(
69 | (id: string) => id !== photo.id
70 | )
71 | if (!res?.ok) {
72 | setPhotosData({
73 | adding_media: [],
74 | added_media: [],
75 | })
76 | setSaveError(true)
77 | return
78 | }
79 | const added_media = [...photoData.added_media, photo.id]
80 | setPhotosData({ ...photoData, adding_media, added_media })
81 | } catch (e) {
82 | console.log(e)
83 | }
84 | }
85 |
86 | async function searchAIPhotos(q: string) {
87 | setServiceFetchError("")
88 | const query = q
89 | setQuery(query)
90 | if (query === "") {
91 | setPhotos([])
92 | return
93 | }
94 | try {
95 | setGenerating(true)
96 | const response = await openai.createImage({
97 | prompt: q,
98 | n: 8,
99 | size: "1024x1024",
100 | })
101 | const photos = response.data.data
102 | for (const photo of photos) {
103 | photo.id = photo.url
104 | }
105 | setPhotos(photos)
106 | setGenerating(false)
107 | } catch (e: any) {
108 | setGenerating(false)
109 | setServiceFetchError("OpenAI")
110 | console.log(e)
111 | }
112 | }
113 | return (
114 |
115 | {saveError && (
116 |
124 | )}
125 | {mediaModalData.url && (
126 |
184 | )}
185 |
220 | {serviceFetchError && (
221 |
222 |
223 |
224 | )}
225 | {generating && (
226 |
227 |
🤖 Generating images
228 |
229 |
230 |
231 |
232 | )}
233 | {!generating && photos?.length !== 0 && (
234 |
235 | {photos?.map((photo: Photo) => (
236 |
{
240 | setMediaModalData({
241 | url: photo.url,
242 | description: query,
243 | photo: photo,
244 | download_url: photo.url,
245 | name: `${photo.id}-cosmic-media.jpg`,
246 | service: "OpenAI",
247 | external_url: photo.url,
248 | })
249 | }}
250 | >
251 |
252 |
257 |
258 |
259 | {showMobile &&
}
260 |
261 | ))}
262 |
263 | )}
264 | {!generating && photos?.length === 0 &&
}
265 |
266 | )
267 | }
268 |
--------------------------------------------------------------------------------
/components/get-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Check, Download, Loader2 } from "lucide-react"
4 |
5 | import {
6 | GiphyImage,
7 | Photo,
8 | PhotoData,
9 | PixabayPhoto,
10 | UnsplashPhoto,
11 | Video,
12 | } from "@/lib/types"
13 | import { Button } from "@/components/ui/button"
14 |
15 | export default function GetButton({
16 | data,
17 | media,
18 | handleAddPhotoToMedia,
19 | handleAddVideoToMedia,
20 | isZoom,
21 | }: {
22 | data: PhotoData
23 | media: Photo | UnsplashPhoto | PixabayPhoto | Video | GiphyImage
24 | handleAddPhotoToMedia?: (
25 | photo: Photo | UnsplashPhoto | PixabayPhoto | UnsplashPhoto
26 | ) => void
27 | handleAddVideoToMedia?: (video: Video) => void
28 | isZoom?: boolean
29 | }) {
30 | if (data.adding_media && data.adding_media.indexOf(media.id) !== -1)
31 | return (
32 |
33 |
40 |
41 | )
42 | if (data.added_media && data.added_media.indexOf(media.id) !== -1)
43 | return (
44 | e.stopPropagation()}>
45 |
52 |
53 | )
54 | return (
55 | e.stopPropagation()}
58 | >
59 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/components/get-gifs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext, useEffect, useMemo, useState } from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import isMobile from "is-mobile"
6 | import { ExternalLink, Loader2, XCircle } from "lucide-react"
7 |
8 | import { GIPHY_KEY, GIPHY_SEARCH_URL, cosmic } from "@/lib/data"
9 | import { Bucket, GiphyImage, MediaModalData, PhotoData } from "@/lib/types"
10 | import { cn, emptyModalData } from "@/lib/utils"
11 | import { buttonVariants } from "@/components/ui/button"
12 | import {
13 | Dialog,
14 | DialogContent,
15 | DialogDescription,
16 | DialogHeader,
17 | } from "@/components/ui/dialog"
18 | import GetButton from "@/components/get-button"
19 | import { Icons } from "@/components/icons"
20 | import GifOutput from "@/components/media/gif"
21 | import { FetchErrorMessage } from "@/components/messages/fetch-error-message"
22 | import { SaveErrorMessage } from "@/components/messages/save-error-message"
23 | import Overlay from "@/components/overlay"
24 |
25 | import { GlobalContext } from "./content"
26 | import EmptyState from "./empty-state"
27 | import Header from "./header"
28 | import Input from "./ui/input"
29 |
30 | export default function GetGifs(bucket: Bucket) {
31 | const { query, setQuery, debouncedQuery } = useContext(GlobalContext)
32 | const searchParams = useSearchParams()
33 | const giphy_key = searchParams.get("giphy_key") || GIPHY_KEY
34 |
35 | const [giphyImages, setGiphyImages] = useState([])
36 | const [photoData, setPhotosData] = useState({
37 | adding_media: [],
38 | added_media: [],
39 | })
40 | const [saveError, setSaveError] = useState(false)
41 | const [serviceFetchError, setServiceFetchError] = useState()
42 | const [mediaModalData, setMediaModalData] =
43 | useState(emptyModalData)
44 | const showMobile = useMemo(() => isMobile(), [])
45 |
46 | const cosmicBucket = cosmic(
47 | bucket.bucket_slug,
48 | bucket.read_key,
49 | bucket.write_key
50 | )
51 |
52 | async function searchGifs(q: string) {
53 | setServiceFetchError("")
54 | const query = q
55 | if (query === "") {
56 | setGiphyImages([])
57 | return
58 | }
59 | try {
60 | await fetch(
61 | GIPHY_SEARCH_URL + "?api_key=" + giphy_key + "&q=" + q + "&limit=80"
62 | )
63 | .then((res) => res.json())
64 | .then((res) => {
65 | if (res.meta.status !== 200) setServiceFetchError("Giphy")
66 | const gifs = res.data
67 | if (!gifs) {
68 | setGiphyImages([])
69 | } else {
70 | setGiphyImages(gifs)
71 | }
72 | })
73 | } catch (e: any) {
74 | setGiphyImages([])
75 | setServiceFetchError("Giphy")
76 | console.log(e)
77 | }
78 | }
79 |
80 | async function handleAddGifToMedia(image: GiphyImage) {
81 | if (!bucket.bucket_slug) return setSaveError(true)
82 | const adding_media = [...(photoData.adding_media || []), image.id]
83 | setPhotosData({ ...photoData, adding_media })
84 |
85 | try {
86 | const response = await fetch(image?.images?.downsized_medium?.url ?? "")
87 | const blob = await response.blob()
88 | const media: any = new Blob([blob], {
89 | type: "image/gif",
90 | })
91 | media.name = image.id + ".gif"
92 | await cosmicBucket.media.insertOne({ media })
93 | const adding_media = photoData.adding_media?.filter(
94 | (id: string) => id !== image.id
95 | )
96 | const added_media = [...photoData.added_media, image.id]
97 | setPhotosData({ ...photoData, adding_media, added_media })
98 | } catch (err) {
99 | console.log(err)
100 | setSaveError(true)
101 | setPhotosData({
102 | adding_media: [],
103 | added_media: [],
104 | })
105 | }
106 | }
107 | useEffect(() => {
108 | searchGifs(debouncedQuery)
109 | //eslint-disable-next-line
110 | }, [debouncedQuery])
111 | return (
112 |
113 | {saveError && (
114 |
122 | )}
123 | {mediaModalData.url && (
124 |
193 | )}
194 |
214 | {serviceFetchError && (
215 |
216 |
217 |
218 | )}
219 | {giphyImages?.length !== 0 && (
220 |
221 | {giphyImages?.map((image: GiphyImage) => (
222 |
{
226 | setMediaModalData({
227 | url: image?.images?.downsized_medium?.url,
228 | description: image?.title,
229 | photo: image,
230 | download_url: image?.images?.downsized_medium?.url,
231 | name: `${image.id}-cosmic-media.gif`,
232 | service: "giphy",
233 | external_url: image.url,
234 | creator: {
235 | name: image?.user?.display_name,
236 | url: image?.user?.profile_url,
237 | },
238 | })
239 | }}
240 | >
241 |
246 | handleAddGifToMedia(image)}
249 | data={photoData}
250 | />
251 |
252 |
253 | {showMobile && }
254 |
255 | ))}
256 |
257 | )}
258 | {!query && giphyImages?.length === 0 &&
}
259 | {!serviceFetchError && query && giphyImages?.length === 0 && (
260 |
261 |
262 |
263 | )}
264 |
265 | )
266 | }
267 |
--------------------------------------------------------------------------------
/components/get-illustrations.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext, useEffect, useMemo, useState } from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import isMobile from "is-mobile"
6 | import { ExternalLink, Loader2, XCircle } from "lucide-react"
7 |
8 | import { PIXABAY_KEY, PIXABAY_SEARCH_URL, cosmic } from "@/lib/data"
9 | import { Bucket, MediaModalData, PhotoData, PixabayPhoto } from "@/lib/types"
10 | import { cn, debounce, emptyModalData } from "@/lib/utils"
11 | import { buttonVariants } from "@/components/ui/button"
12 | import {
13 | Dialog,
14 | DialogContent,
15 | DialogDescription,
16 | DialogHeader,
17 | } from "@/components/ui/dialog"
18 | import GetButton from "@/components/get-button"
19 | import { Icons } from "@/components/icons"
20 | import VectorOutput from "@/components/media/illustration"
21 | import { FetchErrorMessage } from "@/components/messages/fetch-error-message"
22 | import { SaveErrorMessage } from "@/components/messages/save-error-message"
23 | import Overlay from "@/components/overlay"
24 |
25 | import { GlobalContext } from "./content"
26 | import EmptyState from "./empty-state"
27 | import Header from "./header"
28 | import Input from "./ui/input"
29 |
30 | export default function GetIllustrations(bucket: Bucket) {
31 | const { query, setQuery, debouncedQuery } = useContext(GlobalContext)
32 | const searchParams = useSearchParams()
33 | const pixabay_key = searchParams.get("pixabay_key") || PIXABAY_KEY
34 |
35 | const [pixabayIllustrations, setPixabayIllustrations] = useState<
36 | PixabayPhoto[]
37 | >([])
38 | const [photoData, setPhotosData] = useState({
39 | adding_media: [],
40 | added_media: [],
41 | })
42 | const [saveError, setSaveError] = useState(false)
43 | const [serviceFetchError, setServiceFetchError] = useState()
44 | const [mediaModalData, setMediaModalData] =
45 | useState(emptyModalData)
46 | const showMobile = useMemo(() => isMobile(), [])
47 |
48 | const cosmicBucket = cosmic(
49 | bucket.bucket_slug,
50 | bucket.read_key,
51 | bucket.write_key
52 | )
53 |
54 | async function searchPixabay(q: string) {
55 | debounce(() => setServiceFetchError(""))
56 | const query = q
57 | if (query === "") {
58 | setPixabayIllustrations([])
59 | return
60 | }
61 | try {
62 | await fetch(
63 | PIXABAY_SEARCH_URL +
64 | "?key=" +
65 | pixabay_key +
66 | "&q=" +
67 | q +
68 | "&image_type=illustration" +
69 | "&per_page=80"
70 | )
71 | .then((res) => res.json())
72 | .then((data) => {
73 | const photos = data.hits
74 | if (!photos) {
75 | setPixabayIllustrations([])
76 | } else {
77 | setPixabayIllustrations(photos)
78 | }
79 | })
80 | } catch (e: any) {
81 | setPixabayIllustrations([])
82 | setServiceFetchError("Pixabay")
83 | console.log(e)
84 | }
85 | }
86 |
87 | async function handleAddPixabayIllustrationToMedia(photo: PixabayPhoto) {
88 | if (!bucket.bucket_slug) return setSaveError(true)
89 | const adding_media = [...(photoData.adding_media || []), photo.id]
90 | setPhotosData({ ...photoData, adding_media })
91 |
92 | try {
93 | const response = await fetch(photo.imageURL ?? "")
94 | const blob = await response.blob()
95 | const media: any = new Blob([blob], {
96 | type: "image/jpeg",
97 | })
98 | media.name = photo.id + ".jpg"
99 | await cosmicBucket.media.insertOne({ media })
100 | const adding_media = photoData.adding_media?.filter(
101 | (id: string) => id !== photo.id
102 | )
103 | const added_media = [...photoData.added_media, photo.id]
104 | setPhotosData({ ...photoData, adding_media, added_media })
105 | } catch (err) {
106 | console.log(err)
107 | setSaveError(true)
108 | setPhotosData({ adding_media: [], added_media: [] })
109 | }
110 | }
111 | useEffect(() => {
112 | searchPixabay(debouncedQuery)
113 | //eslint-disable-next-line
114 | }, [debouncedQuery])
115 | return (
116 |
117 | {saveError && (
118 |
126 | )}
127 | {mediaModalData.url && (
128 |
199 | )}
200 |
220 | {serviceFetchError && (
221 |
222 |
223 |
224 | )}
225 | {pixabayIllustrations?.length !== 0 && (
226 |
227 | {pixabayIllustrations?.map((photo: PixabayPhoto) => (
228 |
{
232 | setMediaModalData({
233 | url: photo.largeImageURL,
234 | description: photo.tags,
235 | photo,
236 | download_url: photo?.fullHDURL,
237 | name: `${photo.id}-cosmic-media.jpg`,
238 | service: "pixabay",
239 | creator: {
240 | name: photo.user,
241 | url: `https://pixabay.com/users/${photo.user_id}`,
242 | },
243 | external_url: photo.pageURL,
244 | })
245 | }}
246 | >
247 |
252 |
255 | handleAddPixabayIllustrationToMedia(photo)
256 | }
257 | data={photoData}
258 | />
259 |
260 |
261 | {showMobile && }
262 |
263 | ))}
264 |
265 | )}
266 | {!query && pixabayIllustrations?.length === 0 &&
}
267 | {!serviceFetchError && query && pixabayIllustrations?.length === 0 && (
268 |
269 |
270 |
271 | )}
272 |
273 | )
274 | }
275 |
--------------------------------------------------------------------------------
/components/get-pexels-videos.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext, useEffect, useState } from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import { ExternalLink, Loader2, XCircle } from "lucide-react"
6 | import { createClient } from "pexels"
7 |
8 | import { PEXELS_KEY, cosmic } from "@/lib/data"
9 | import { Bucket, MediaModalData, Video, VideoData } from "@/lib/types"
10 | import { cn, emptyModalData } from "@/lib/utils"
11 | import { buttonVariants } from "@/components/ui/button"
12 | import {
13 | Dialog,
14 | DialogContent,
15 | DialogDescription,
16 | DialogHeader,
17 | } from "@/components/ui/dialog"
18 | import GetButton from "@/components/get-button"
19 | import { Icons } from "@/components/icons"
20 | import VideoOutput from "@/components/media/video"
21 | import { FetchErrorMessage } from "@/components/messages/fetch-error-message"
22 | import { SaveErrorMessage } from "@/components/messages/save-error-message"
23 |
24 | import { GlobalContext } from "./content"
25 | import EmptyState from "./empty-state"
26 | import Header from "./header"
27 | import Input from "./ui/input"
28 |
29 | export default function GetPexelsVideos(bucket: Bucket) {
30 | const { query, setQuery, debouncedQuery } = useContext(GlobalContext)
31 |
32 | const searchParams = useSearchParams()
33 | const pexels_key = searchParams.get("pexels_key") || PEXELS_KEY
34 | const [saveError, setSaveError] = useState(false)
35 | const [serviceFetchError, setServiceFetchError] = useState()
36 | const [videos, setVideos] = useState