├── .eslintignore ├── src ├── pages │ └── Home │ │ ├── index.ts │ │ ├── Home.module.scss │ │ └── Home.tsx ├── components │ ├── Card │ │ ├── index.ts │ │ ├── Card.module.scss │ │ └── Card.tsx │ ├── Input │ │ ├── index.ts │ │ ├── Input.module.scss │ │ └── Input.tsx │ ├── Tab │ │ ├── index.ts │ │ ├── Tab.module.scss │ │ └── Tab.tsx │ ├── Buttton │ │ ├── index.ts │ │ ├── Button.module.scss │ │ └── Button.tsx │ ├── HowToUse │ │ ├── index.ts │ │ ├── HowToUse.module.scss │ │ └── HowToUse.tsx │ ├── TextArea │ │ ├── index.ts │ │ ├── TextArea.module.scss │ │ └── TextArea.tsx │ ├── GithubCorner │ │ ├── index.ts │ │ └── GithubCorner.tsx │ ├── ResultProcessor │ │ ├── index.ts │ │ ├── ResultProcessor.module.scss │ │ └── ResultProcessor.tsx │ └── Galllery │ │ ├── Gallery.module.scss │ │ └── Gallery.tsx ├── hooks │ ├── index.ts │ └── useCopyToClipboard.ts ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── google-captcha-wrapper.tsx │ ├── api │ │ ├── proxy-image │ │ │ └── route.ts │ │ ├── get-profile-picture │ │ │ └── route.ts │ │ └── get-user-id │ │ │ └── route.ts │ └── layout.tsx ├── assets │ ├── images │ │ ├── how-to-use-1.jpg │ │ ├── how-to-use-2.jpg │ │ ├── how-to-use-3.jpg │ │ └── how-to-use-4.jpg │ ├── icons │ │ ├── icon-x.svg │ │ ├── icon-copy.svg │ │ ├── icon-video.svg │ │ ├── icon-image.svg │ │ └── icon-video-preview.svg │ └── styles │ │ └── globals.scss ├── utils │ └── sleep.ts ├── services │ ├── fetch-proxy-image.ts │ ├── verify-recaptcha.ts │ ├── get-user-info.ts │ ├── get-profile-picture.ts │ └── get-user-id.ts └── constants │ ├── regexes.ts │ └── urls.ts ├── .dockerignore ├── .prettierignore ├── commitlint.config.js ├── .husky ├── pre-commit └── commit-msg ├── .prettierrc ├── .env.example ├── docker-compose.yaml ├── Dockerfile ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── LICENSE ├── next.config.js ├── .eslintrc.json ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/*.json -------------------------------------------------------------------------------- /src/pages/Home/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Home'; 2 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Card'; 2 | -------------------------------------------------------------------------------- /src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Input'; 2 | -------------------------------------------------------------------------------- /src/components/Tab/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Tab'; 2 | -------------------------------------------------------------------------------- /src/components/Buttton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/HowToUse/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './HowToUse'; 2 | -------------------------------------------------------------------------------- /src/components/TextArea/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TextArea'; 2 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GithubCorner'; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | .git 3 | .results 4 | *Dockerfile* 5 | node_modules 6 | .env 7 | .next -------------------------------------------------------------------------------- /src/components/ResultProcessor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ResultProcessor'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | **/.next/** 3 | **/dist/** 4 | **/tmp/** 5 | *.json 6 | *.md 7 | *.yaml -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useCopyToClipboard } from './useCopyToClipboard'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /src/assets/images/how-to-use-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-1.jpg -------------------------------------------------------------------------------- /src/assets/images/how-to-use-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-2.jpg -------------------------------------------------------------------------------- /src/assets/images/how-to-use-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-3.jpg -------------------------------------------------------------------------------- /src/assets/images/how-to-use-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-4.jpg -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | const MS = 2000; 2 | export function sleep() { 3 | return new Promise((resolve) => setTimeout(resolve, MS)); 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 120, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.module.scss: -------------------------------------------------------------------------------- 1 | /* Tab.module.scss */ 2 | .buttons { 3 | display: flex; 4 | gap: 12px; 5 | } 6 | 7 | .active { 8 | background-color: #007bff; 9 | color: #fff; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/HowToUse/HowToUse.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | img { 3 | width: 100%; 4 | max-width: 100%; 5 | display: block; 6 | border: 1px solid #222; 7 | border-radius: 4px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ResultProcessor/ResultProcessor.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 30px; 3 | padding-top: 20px; 4 | border-top: 1px solid #eee; 5 | } 6 | 7 | .description { 8 | margin-bottom: 12px; 9 | line-height: 1.4; 10 | } 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # You need Google Recaptcha token for below field and you should add *localhost* domain in Google Recaptcha console "Domains" section 2 | # https://www.google.com/recaptcha/admin/create 3 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY= 4 | RECAPTCHA_SECRET_KEY= 5 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | context: ./ 6 | tty: true 7 | stdin_open: true 8 | command: npm run dev 9 | volumes: 10 | - ./:/app 11 | - /app/node_modules 12 | ports: 13 | - '3000:3000' 14 | -------------------------------------------------------------------------------- /src/components/TextArea/TextArea.module.scss: -------------------------------------------------------------------------------- 1 | .textarea { 2 | width: 100%; 3 | height: 150px; 4 | padding: 8px; 5 | font-size: 16px; 6 | box-sizing: border-box; 7 | border: 1px solid #ccc; 8 | border-radius: 4px; 9 | outline: none; 10 | margin-bottom: 16px; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/icons/icon-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM node:18.18.0 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # copy application files to the container 8 | COPY . . 9 | 10 | # install dependencies 11 | RUN npm install 12 | 13 | # expose the port 3000 14 | EXPOSE 3000 15 | 16 | # start the application 17 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import GoogleCaptchaWrapper from './google-captcha-wrapper'; 3 | 4 | import HomePage from '@/pages/Home'; 5 | 6 | const Home = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Home; 15 | -------------------------------------------------------------------------------- /src/components/Buttton/Button.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | border: 0; 3 | padding: 10px; 4 | margin-bottom: 5px; 5 | cursor: pointer; 6 | border-radius: 5px; 7 | } 8 | 9 | .secondary { 10 | color: #fff; 11 | background-color: #3498db; 12 | &:disabled { 13 | background-color: #96d5ff; 14 | cursor: not-allowed; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/icons/icon-copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/fetch-proxy-image.ts: -------------------------------------------------------------------------------- 1 | const fetchProxyImage = async (imageUrl: string) => { 2 | const response = await fetch(`/api/proxy-image?imageUrl=${encodeURIComponent(imageUrl)}`); 3 | const data = await response.json(); 4 | 5 | if (data.imageUrlBase64) { 6 | return data.imageUrlBase64; 7 | } 8 | }; 9 | 10 | export default fetchProxyImage; 11 | -------------------------------------------------------------------------------- /src/assets/styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | .container { 7 | max-width: 750px; 8 | margin: auto; 9 | padding: 8px; 10 | 11 | @media (min-width: 768px) { 12 | padding: 20px; 13 | } 14 | } 15 | 16 | a { 17 | color: inherit; 18 | } 19 | 20 | .grecaptcha-badge { 21 | visibility: hidden; 22 | z-index: 1010; 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/icons/icon-video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/constants/regexes.ts: -------------------------------------------------------------------------------- 1 | export const INSTAGRAM_USERNAME_REGEX_FOR_STORIES = /instagram\.com\/stories\/([^/]+)\/?(?:\d+\/?)?$/; 2 | export const INSTAGRAM_USERNAME_REGEX_FOR_PROFILE = /\/([^\/]+)\/?$/; 3 | export const INSTAGRAM_HIGHLIGHT_ID_REGEX = /\/stories\/highlights\/(\d+)\/?$/; 4 | export const INSTAGRAM_POSTPAGE_REGEX = /p/; 5 | export const INSTAGRAM_REELSPAGE_REGEX = /reel/; 6 | export const INSTAGRAM_HIGHLIGHTSPAGE_REGEX = /stories\/highlights/; 7 | -------------------------------------------------------------------------------- /src/assets/icons/icon-image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/google-captcha-wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; 4 | 5 | export default function GoogleCaptchaWrapper({ children }: { children: React.ReactNode }) { 6 | const recaptchaKey: string | undefined = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY; 7 | return ( 8 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/icons/icon-video-preview.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Home/Home.module.scss: -------------------------------------------------------------------------------- 1 | .description { 2 | margin-bottom: 12px; 3 | color: #555; 4 | } 5 | 6 | .recentItemsContainer { 7 | margin: 20px 0; 8 | user-select: none; 9 | } 10 | 11 | .recentItemsSummary { 12 | font-size: 1.2rem; 13 | font-weight: bold; 14 | cursor: pointer; 15 | } 16 | 17 | .categoryContainer { 18 | margin-bottom: 20px; 19 | } 20 | 21 | .categoryTitle { 22 | font-size: 18px; 23 | margin-bottom: 10px; 24 | } 25 | 26 | .localStorageItems { 27 | display: flex; 28 | flex-wrap: wrap; 29 | gap: 10px; 30 | } 31 | 32 | .itemContainer { 33 | display: flex; 34 | align-items: center; 35 | margin-right: 8px; 36 | } 37 | 38 | .deleteIcon { 39 | width: 20px; 40 | height: 20px; 41 | cursor: pointer; 42 | margin-left: 4px; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/api/proxy-image/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | export async function GET(request: NextRequest) { 6 | try { 7 | const { searchParams } = new URL(request.url); 8 | const imageUrl = searchParams.get('imageUrl'); 9 | 10 | const response = await fetch(imageUrl as string); 11 | 12 | const arrayBuffer = await response.arrayBuffer(); 13 | 14 | const base64Image = Buffer.from(arrayBuffer).toString('base64'); 15 | const imageUrlBase64 = `data:image/png;base64,${base64Image}`; 16 | 17 | const data = { 18 | imageUrlBase64, 19 | }; 20 | 21 | return Response.json(data, { status: 200 }); 22 | } catch (error) { 23 | return Response.json(error, { status: 400 }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/services/verify-recaptcha.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const RECAPTCHA_THRESHOLD = 0.5; 4 | 5 | async function verifyRecaptcha(token: string) { 6 | const secretKey = process.env.RECAPTCHA_SECRET_KEY; 7 | const verificationUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${token}`; 8 | 9 | try { 10 | const response = await axios.post(verificationUrl, {}, { headers: { 'Content-Type': 'application/json' } }); 11 | 12 | if (!response.data.success || response.data.score < RECAPTCHA_THRESHOLD) { 13 | throw new Error('Recaptcha verification failed'); 14 | } 15 | 16 | return response.data; 17 | } catch (error) { 18 | console.error('Recaptcha verification error:', (error as Error).message); 19 | throw error; 20 | } 21 | } 22 | 23 | export default verifyRecaptcha; 24 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; 3 | import { Toaster } from 'react-hot-toast'; 4 | 5 | import type { Metadata } from 'next'; 6 | import { Inter } from 'next/font/google'; 7 | 8 | import GithubCorner from '@/components/GithubCorner'; 9 | 10 | import '@/assets/styles/globals.scss'; 11 | 12 | const inter = Inter({ subsets: ['latin'] }); 13 | 14 | export const metadata: Metadata = { 15 | title: 'Instagram Media Downloader', 16 | description: 'Generated by create next app', 17 | }; 18 | 19 | export default function RootLayout({ children }: { children: React.ReactNode }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Input/Input.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 16px; 5 | } 6 | 7 | .input { 8 | padding: 8px; 9 | font-size: 16px; 10 | width: 100%; 11 | box-sizing: border-box; 12 | border: 1px solid #ccc; 13 | border-radius: 4px; 14 | outline: none; 15 | } 16 | 17 | .input:disabled { 18 | background-color: #f5f5f5; 19 | color: #777; 20 | } 21 | 22 | .copyButton { 23 | cursor: pointer; 24 | background-color: #3498db; 25 | color: #ffffff; 26 | border: none; 27 | padding: 8px; 28 | margin-left: 5px; 29 | border-radius: 4px; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | transition: background-color 0.3s ease; 34 | 35 | &:hover { 36 | background-color: #2980b9; 37 | } 38 | 39 | &:disabled { 40 | background-color: #96d5ff; 41 | cursor: not-allowed; 42 | } 43 | 44 | svg { 45 | font-size: 20px; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IconCopy from '@/assets/icons/icon-copy.svg'; 4 | 5 | import styles from './Input.module.scss'; 6 | 7 | type Props = { 8 | placeholder: string; 9 | value: string; 10 | readOnly?: boolean; 11 | disabled?: boolean; 12 | onCopy?: () => void; 13 | onBlur?: () => void; 14 | onChange?: (event: React.ChangeEvent) => void; 15 | }; 16 | 17 | const Input: React.FC = ({ placeholder, value, onChange, onCopy, disabled = false, ...props }) => { 18 | return ( 19 |
20 | 29 | {onCopy && ( 30 | 33 | )} 34 |
35 | ); 36 | }; 37 | 38 | export default Input; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yasin ATEŞ 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 | -------------------------------------------------------------------------------- /src/components/Galllery/Gallery.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 750px; 3 | margin: auto; 4 | padding: 20px; 5 | } 6 | 7 | .icon { 8 | display: flex; 9 | align-items: center; 10 | font-size: 18px; 11 | margin-right: 12px; 12 | } 13 | 14 | .description { 15 | display: flex; 16 | align-items: center; 17 | line-height: 1.4; 18 | border-bottom: 1px solid #ddd; 19 | padding-top: 4px; 20 | padding-bottom: 4px; 21 | 22 | &:last-of-type { 23 | margin-bottom: 12px; 24 | } 25 | 26 | svg { 27 | margin-left: 12px; 28 | } 29 | 30 | strong { 31 | margin-left: 4px; 32 | margin-right: 4px; 33 | } 34 | } 35 | 36 | .additionalResults { 37 | margin-top: 40px; 38 | padding-top: 20px; 39 | border-top: 1px solid #eee; 40 | } 41 | 42 | .additionalResults h3 { 43 | margin-bottom: 20px; 44 | padding-bottom: 10px; 45 | border-bottom: 1px solid #ddd; 46 | } 47 | 48 | .additionalResult { 49 | margin-bottom: 30px; 50 | padding-bottom: 20px; 51 | border-bottom: 1px dashed #eee; 52 | } 53 | 54 | .additionalResult:last-child { 55 | border-bottom: none; 56 | margin-bottom: 0; 57 | padding-bottom: 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Buttton/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import cx from 'classnames'; 4 | 5 | import css from './Button.module.scss'; 6 | 7 | type Classnames = { 8 | container?: string; 9 | }; 10 | 11 | type Props = { 12 | classnames?: Classnames; 13 | type?: 'button' | 'submit' | 'reset' | undefined; 14 | size?: 'medium' | 'large'; 15 | shape?: 'circle'; 16 | variant?: 'primary' | 'secondary'; 17 | width?: number | string; 18 | }; 19 | 20 | const Button: React.FC, 'size'>> = ({ 21 | children, 22 | classnames, 23 | size = 'medium', 24 | style, 25 | shape, 26 | variant = 'primary', 27 | type = 'button', 28 | width, 29 | ...props 30 | }) => { 31 | const cn: Classnames = { 32 | container: cx(css.container, css[variant], css[size], shape && css[shape], classnames?.container), 33 | }; 34 | 35 | return ( 36 | 46 | ); 47 | }; 48 | 49 | export default Button; 50 | -------------------------------------------------------------------------------- /src/hooks/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useCopyToClipboard = () => { 4 | const [isCopied, setIsCopied] = useState(false); 5 | 6 | const copyToClipboard = (text: string) => { 7 | if (navigator.clipboard) { 8 | navigator.clipboard.writeText(text).then( 9 | () => { 10 | setIsCopied(true); 11 | }, 12 | (err) => { 13 | console.error('Error copying to clipboard:', err); 14 | setIsCopied(false); 15 | } 16 | ); 17 | } else { 18 | // Fallback for browsers that do not support the Clipboard API 19 | const textArea = document.createElement('textarea'); 20 | textArea.value = text; 21 | document.body.appendChild(textArea); 22 | textArea.select(); 23 | 24 | try { 25 | document.execCommand('copy'); 26 | setIsCopied(true); 27 | } catch (err) { 28 | console.error('Error copying to clipboard:', err); 29 | setIsCopied(false); 30 | } 31 | 32 | document.body.removeChild(textArea); 33 | } 34 | }; 35 | 36 | return { isCopied, copyToClipboard }; 37 | }; 38 | 39 | export default useCopyToClipboard; 40 | -------------------------------------------------------------------------------- /src/components/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | } 4 | 5 | .gallery { 6 | border-bottom: 1px solid #ddd; 7 | margin-bottom: 4px; 8 | 9 | &:last-of-type { 10 | border-bottom: 0; 11 | } 12 | } 13 | 14 | .index { 15 | font-size: 18px; 16 | margin-right: 12px; 17 | display: none; 18 | min-width: 30px; 19 | 20 | @media (min-width: 768px) { 21 | display: inline; 22 | } 23 | } 24 | 25 | .image, 26 | .video { 27 | position: relative; 28 | display: inline-block; 29 | } 30 | 31 | .hasVideo { 32 | @media (min-width: 768px) { 33 | margin-left: 6px; 34 | } 35 | } 36 | 37 | .icon { 38 | position: absolute; 39 | top: 5px; 40 | right: 5px; 41 | color: #b7dde9; 42 | font-size: 22px; 43 | } 44 | 45 | .loader { 46 | margin-top: 12px; 47 | margin-bottom: 12px; 48 | width: 48px; 49 | height: 48px; 50 | border: 5px solid #3498db; 51 | border-bottom-color: transparent; 52 | border-radius: 50%; 53 | display: inline-block; 54 | box-sizing: border-box; 55 | animation: rotation 1s linear infinite; 56 | } 57 | 58 | @keyframes rotation { 59 | 0% { 60 | transform: rotate(0deg); 61 | } 62 | 100% { 63 | transform: rotate(360deg); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async rewrites() { 4 | return [ 5 | { 6 | source: '/api/get-user-info', 7 | destination: 'https://www.instagram.com/:username', 8 | }, 9 | ]; 10 | }, 11 | webpack(config) { 12 | // Grab the existing rule that handles SVG imports 13 | const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); 14 | 15 | config.module.rules.push( 16 | // Reapply the existing rule, but only for svg imports ending in ?url 17 | { 18 | ...fileLoaderRule, 19 | test: /\.svg$/i, 20 | resourceQuery: /url/, // *.svg?url 21 | }, 22 | // Convert all other *.svg imports to React components 23 | { 24 | test: /\.svg$/i, 25 | issuer: fileLoaderRule.issuer, 26 | resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url 27 | use: ['@svgr/webpack'], 28 | } 29 | ); 30 | 31 | // Modify the file loader rule to ignore *.svg, since we have it handled now. 32 | fileLoaderRule.exclude = /\.svg$/i; 33 | 34 | return config; 35 | }, 36 | }; 37 | 38 | module.exports = nextConfig; 39 | -------------------------------------------------------------------------------- /src/components/HowToUse/HowToUse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import howToUseImage1 from '@/assets/images/how-to-use-1.jpg'; 4 | import howToUseImage2 from '@/assets/images/how-to-use-2.jpg'; 5 | import howToUseImage3 from '@/assets/images/how-to-use-3.jpg'; 6 | import howToUseImage4 from '@/assets/images/how-to-use-4.jpg'; 7 | 8 | import styles from './HowToUse.module.scss'; 9 | 10 | type Props = {}; 11 | 12 | const HowToUse = (props: Props) => { 13 | return ( 14 |
15 |

How To Use

16 |
    17 |
  1. 18 |

    Copy Instagram url

    19 | 20 |
  2. 21 |
  3. 22 |

    Paste your url first input in page

    23 | 24 |
  4. 25 |
  5. 26 |

    Select and copy all JSON code in the tab

    27 | 28 |
  6. 29 |
  7. 30 |

    Paste the code in the input. You will then be redirected to result page

    31 | 32 |
  8. 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default HowToUse; 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:prettier/recommended"], 3 | "plugins": ["simple-import-sort"], 4 | "rules": { 5 | "import/no-anonymous-default-export": "off", 6 | "@next/next/no-img-element": "off", 7 | "react-hooks/exhaustive-deps": "off", 8 | "simple-import-sort/imports": "error", 9 | "simple-import-sort/exports": "error" 10 | }, 11 | "overrides": [ 12 | { 13 | "files": ["*.ts", "*.tsx"], 14 | "rules": { 15 | "simple-import-sort/imports": [ 16 | "error", 17 | { 18 | "groups": [ 19 | ["^react"], 20 | ["^next"], 21 | ["^@?\\w"], 22 | ["^(@/constants)(/.*|$)"], 23 | ["^(@/data)(/.*|$)"], 24 | ["^(@/hooks)(/.*|$)"], 25 | ["^(@/assets/icons)(/.*|$)"], 26 | ["^(@/assets/images)(/.*|$)"], 27 | ["^(@/services)(/.*|$)"], 28 | ["^(@/store)(/.*|$)"], 29 | ["^(@/components)(/.*|$)"], 30 | ["^\\u0000"], 31 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"], 32 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], 33 | ["^.+\\.?(scss)$"] 34 | ] 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/get-user-info.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import getUserId from './get-user-id'; 4 | 5 | // https://i.instagram.com/api/v1/users/${id}/info/ 6 | export async function getUserInfo(username: string) { 7 | const userId = await getUserId(username); 8 | 9 | try { 10 | const url = `https://i.instagram.com/api/v1/users/${userId}/info/`; 11 | const headers = { 12 | Accept: 'application/json, text/plain, */*', 13 | 'User-Agent': 14 | 'Mozilla/5.0 (Linux; Android 9; GM1903 Build/PKQ1.190110.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3770.143 Mobile Safari/537.36 Instagram 103.1.0.15.119 Android (28/9; 420dpi; 1080x2260; OnePlus; GM1903; OnePlus7; qcom; sv_SE; 164094539)', 15 | 'sec-fetch-dest': 'empty', 16 | 'sec-fetch-mode': 'cors', 17 | 'sec-fetch-site': 'same-origin', 18 | }; 19 | 20 | const response = await axios.get(url, { headers, maxBodyLength: Infinity, maxRedirects: 0 }); 21 | 22 | if (response.status !== 200) { 23 | throw new Error(`Request failed with status: ${response.status}`); 24 | } 25 | 26 | return response.data.user; 27 | } catch (error) { 28 | console.error('Instagram API request failed. Falling back to alternative method'); 29 | 30 | const errorMessage = error instanceof Error ? error.message : 'Error retrieving Instagram user ID'; 31 | throw new Error(errorMessage); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/api/get-profile-picture/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | import getProfilePicture from '@/services/get-profile-picture'; 4 | import verifyRecaptcha, { RECAPTCHA_THRESHOLD } from '@/services/verify-recaptcha'; 5 | 6 | import { sleep } from '@/utils/sleep'; 7 | 8 | export async function POST(request: NextRequest) { 9 | const { username, token } = await request.json(); 10 | 11 | if (typeof username !== 'string') { 12 | throw new Error('Invalid username format'); 13 | } 14 | 15 | try { 16 | const recaptchaResponse = await verifyRecaptcha(token); 17 | 18 | if (recaptchaResponse.success && recaptchaResponse.score >= RECAPTCHA_THRESHOLD) { 19 | await sleep(); 20 | 21 | try { 22 | const url = await getProfilePicture(username); 23 | 24 | if (url) { 25 | return new Response( 26 | JSON.stringify({ 27 | url, 28 | }), 29 | { status: 200 } 30 | ); 31 | } 32 | } catch (error) { 33 | const errorMessage = error instanceof Error ? error.message : 'Something went wrong'; 34 | const errorResponse = { status: 'Failed', message: errorMessage }; 35 | 36 | return new Response(JSON.stringify({ error: errorResponse }), { status: 400 }); 37 | } 38 | } 39 | } catch (error) { 40 | return new Response(JSON.stringify({ error: (error as Error).message }), { status: 400 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | // Tab.tsx 2 | import React, { ReactNode, useState } from 'react'; 3 | 4 | import cx from 'classnames'; 5 | 6 | import Button from '@/components/Buttton'; 7 | 8 | import styles from './Tab.module.scss'; 9 | 10 | interface TabProps { 11 | children: ReactNode; 12 | } 13 | 14 | interface ContentProps { 15 | tab: ReactNode | string; 16 | children: ReactNode; 17 | } 18 | 19 | const Tab: React.FC & { Content: React.FC } = ({ children }) => { 20 | const [activeIndex, setActiveIndex] = useState(0); 21 | 22 | const contents = React.Children.toArray(children).filter((c: any) => c.type === Tab.Content); 23 | 24 | const activeContent = contents[activeIndex]; 25 | 26 | return ( 27 | <> 28 |
29 | {contents.map((content, key) => ( 30 |
31 | 42 |
43 | ))} 44 |
45 | 46 | {activeContent} 47 | 48 | ); 49 | }; 50 | 51 | const Content: React.FC = ({ children }) => { 52 | return
{children}
; 53 | }; 54 | 55 | Tab.Content = Content; 56 | 57 | export default Tab; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagram-media-downloader", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/yasinatesim/instagram-media-downloader" 8 | }, 9 | "engines": { 10 | "node": "18" 11 | }, 12 | "scripts": { 13 | "dev": "next dev", 14 | "build": "next build", 15 | "start": "next start", 16 | "lint": "next lint --fix && tsc&& prettier --check --ignore-path .prettierignore .", 17 | "format": "prettier --write --ignore-unknown .", 18 | "prepare": "husky install" 19 | }, 20 | "dependencies": { 21 | "axios": "^1.6.7", 22 | "cheerio": "1.0.0-rc.12", 23 | "classnames": "^2.5.1", 24 | "firebase-admin": "^12.0.0", 25 | "instagram-private-api": "^1.45.3", 26 | "next": "14.0.4", 27 | "node-fetch": "^3.3.2", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-google-recaptcha-v3": "^1.10.1", 31 | "react-hot-toast": "^2.4.1", 32 | "sass": "^1.69.7" 33 | }, 34 | "devDependencies": { 35 | "@commitlint/cli": "^18.4.4", 36 | "@commitlint/config-conventional": "^18.4.4", 37 | "@svgr/webpack": "^8.1.0", 38 | "@types/classnames": "^2.3.1", 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.0.4", 44 | "eslint-config-prettier": "^9.1.0", 45 | "eslint-plugin-prettier": "^5.1.3", 46 | "eslint-plugin-simple-import-sort": "^10.0.0", 47 | "husky": "^8.0.3", 48 | "lint-staged": "^15.2.0", 49 | "prettier": "^3.2.1", 50 | "typescript": "^5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/get-user-id/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | import getUserId from '@/services/get-user-id'; 4 | import verifyRecaptcha, { RECAPTCHA_THRESHOLD } from '@/services/verify-recaptcha'; 5 | 6 | import { sleep } from '@/utils/sleep'; 7 | 8 | // Todo: Implement this -- new service -- for user posts 9 | // https://www.instagram.com/graphql/query/?query_id=17888483320059182&id={user_id}&first=24 10 | // or 11 | //https://www.instagram.com/graphql/query/?query_hash=e769aa130647d2354c40ea6a439bfc08&variables={"id":{user_id},"first": 24} 12 | 13 | export async function POST(request: NextRequest) { 14 | const { username, token } = await request.json(); 15 | 16 | if (typeof username !== 'string') { 17 | throw new Error('Invalid username format'); 18 | } 19 | 20 | try { 21 | const recaptchaResponse = await verifyRecaptcha(token); 22 | 23 | if (recaptchaResponse.success && recaptchaResponse.score >= RECAPTCHA_THRESHOLD) { 24 | await sleep(); 25 | 26 | try { 27 | const userId = await getUserId(username); 28 | 29 | return new Response( 30 | JSON.stringify({ 31 | userId, 32 | }), 33 | { status: 200 } 34 | ); 35 | } catch (error) { 36 | const errorMessage = error instanceof Error ? error.message : 'Something went wrong'; 37 | const errorResponse = { status: 'Failed', message: errorMessage }; 38 | 39 | return new Response(JSON.stringify({ error: errorResponse }), { status: 400 }); 40 | } 41 | } 42 | } catch (error) { 43 | return new Response(JSON.stringify({ error: (error as Error).message }), { status: 400 }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/constants/urls.ts: -------------------------------------------------------------------------------- 1 | export const INSTAGRAM_PROFILE_URL = `https://www.instagram.com/`; 2 | export const INSTAGRAM_GRAPHQL_URL_FOR_STORIES = `https://www.instagram.com/graphql/query/?query_hash=de8017ee0a7c9c45ec4260733d81ea31&variables={"reel_ids":[], "highlight_reel_ids":[],"precomposed_overlay":false}`; 3 | export const INSTAGRAM_GRAPHQL_URL_FOR_HIGHLIGHTS = `https://www.instagram.com/graphql/query/?query_hash=de8017ee0a7c9c45ec4260733d81ea31&variables={"reel_ids":[],"tag_names":[],"location_ids":[],"highlight_reel_ids":[""],"precomposed_overlay":false,"show_story_viewer_list":true,"story_viewer_fetch_count":50,"story_viewer_cursor":""}`; 4 | export const INSTAGRAM_GRAPHQL_URL_FOR_POST = `https://www.instagram.com/graphql/query/?doc_id=8845758582119845&variables={\"shortcode\":\"\",\"fetch_tagged_user_count\":null,\"hoisted_comment_id\":null,\"hoisted_reply_id\":null}`; 5 | export const INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS = `https://www.instagram.com/graphql/query/?doc_id=8759034877476257&variables={"data":{"count":12,"include_relationship_info":true,"latest_besties_reel_media":true,"latest_reel_media":true},"username":"","__relay_internal__pv__PolarisIsLoggedInrelayprovider":true,"__relay_internal__pv__PolarisFeedShareMenurelayprovider":true}`; 6 | export const INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS_WITH_AFTER = `https://www.instagram.com/graphql/query/?doc_id=8759034877476257&variables={"after":"","before":null,"data":{"count":12,"include_relationship_info":true,"latest_besties_reel_media":true,"latest_reel_media":true},"username":"","__relay_internal__pv__PolarisIsLoggedInrelayprovider":true,"__relay_internal__pv__PolarisFeedShareMenurelayprovider":true}`; 7 | -------------------------------------------------------------------------------- /src/components/TextArea/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import styles from './TextArea.module.scss'; 4 | 5 | type Props = { 6 | placeholder: string; 7 | value: string; 8 | onChange?: (event: React.ChangeEvent) => void; 9 | onFileUpload?: (content: string) => void; 10 | }; 11 | 12 | const Textarea: React.FC = ({ placeholder, value, onChange, onFileUpload, ...props }) => { 13 | const fileInputRef = useRef(null); 14 | 15 | const handleFileChange = (event: React.ChangeEvent) => { 16 | const file = event.target.files?.[0]; 17 | if (!file) return; 18 | 19 | const reader = new FileReader(); 20 | reader.onload = (e) => { 21 | const content = e.target?.result as string; 22 | if (onFileUpload) { 23 | onFileUpload(content); 24 | } 25 | }; 26 | reader.readAsText(file); 27 | }; 28 | 29 | const triggerFileInput = () => { 30 | fileInputRef.current?.click(); 31 | }; 32 | 33 | return ( 34 |
35 |