Skip to content

Conversation

@egemengunel
Copy link
Contributor

@egemengunel egemengunel commented Sep 12, 2025

This PR resolves #418

Summary

This PR implements profile importing functionality with an instagram-private-api so that users can import other people from instagram as organizers to the dance events

For Trigger.dev Development

⚠️ This feature requires Trigger.dev setup - follow instructions below for local development.

  1. Environment Variables (.env):

    TRIGGER_SECRET_KEY=your_secret_key
    NUXT_CLOUDINARY_API_KEY="CLOUDINARY_KEY"
    NUXT_CLOUDINARY_API_SECRET=CLOUDINARY_API_SECRET
    NUXT_PUBLIC_CLOUDINARY_CLOUD_NAME=CLOUDINARY_CLOUD_NAME
    INSTAGRAM_USERNAME=INSTAGRAM_USERNAME
    INSTAGRAM_PASSWORD=INSTAGRAM_PASSWORD
    
    
  2. Verify Configuration (trigger.config.ts):

    • Ensure project_ref matches TRIGGER_PROJECT_REF
  3. Setup Commands:

    pnpm install
    pnpm prisma generate
    pnpm dev
    # In separate terminal:
    pnpm dlx [email protected] dev
    # For Deployment Use:
    pnpm dlx [email protected] deploy

Summary by CodeRabbit

  • New Features

    • Import profiles directly from Instagram by entering a handle or URL, with an “Import @username from Instagram” option.
    • Responsive feedback via toasts for loading, success, and errors.
    • Background import populates name, bio, website, photo, followers, and avoids duplicates.
  • Backend

    • New endpoint and background job to create and enrich profiles from Instagram.
    • Profile data model extended with import status, error message, and source tracking.
  • Chores

    • Added Instagram integration dependency.

-add schema fields to the profile field
-create index.ts and instagram_import.ts files
add getInstagramProfile function
-add Instagram API
-add createFromInstagram tRPC mutation to profiles router
-rename instagram_import to 'instagram' only
- Add computed properties to handle Instagram URL validation and username extraction
- Update tRPC router typo
@vercel
Copy link

vercel bot commented Sep 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
v4 Ready Ready Preview Comment Sep 18, 2025 11:16am

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

Introduces an Instagram import flow: frontend extracts usernames and offers an Import action; backend exposes createFromInstagram to create a profile and enqueue a background task; a Trigger.dev task fetches profile data via instagram-private-api and updates the database; Prisma schema gains import tracking fields; dependency added.

Changes

Cohort / File(s) Summary of changes
Frontend: Instagram import UX
components/inputs/ProfileInput.vue
Adds username extraction helpers, computed instagramUsername, and importFromInstagram with toast feedback. Updates query behavior and UI to show “Import @username” option and conditional “No profiles found”.
API: Profiles router
server/trpc/routers/profiles.ts
Adds createFromInstagram mutation: validates Instagram URL, derives username, de-duplicates by username/URL, creates profile with import flags, and triggers background job import-instagram-profile.
Background task and IG fetch
trigger/instagram-import.ts, cli/import-organizer/instagram.ts
Adds Trigger.dev task to process imports: sets importStatus, fetches Instagram data, uploads photo, updates profile fields/status, handles errors. Adds IG client helper using instagram-private-api, env-based login, and mapping to InstagramProfileData.
Data model: Prisma schema
prisma/schema.prisma
Adds optional fields to Profile: importStatus, importError, source.
Dependencies
package.json
Adds dependency: instagram-private-api@^1.46.1.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant UI as Frontend (ProfileInput.vue)
  participant API as TRPC profiles.createFromInstagram
  participant DB as Prisma DB
  participant Q as Trigger.dev Tasks
  participant IG as Instagram API
  participant ST as Image Storage

  U->>UI: Paste handle/URL
  UI->>UI: Extract instagramUsername
  U->>UI: Click "Import @username"
  UI->>API: createFromInstagram({ instagramUrl })
  API->>DB: Find existing by username/URL
  alt Found
    API-->>UI: Return existing profile
  else Not found
    API->>DB: Create profile {importStatus: requested, source: instagram}
    API->>Q: trigger('import-instagram-profile', { profileId })
    API-->>UI: Return newly created profile
  end

  par Background job
    Q->>DB: Update importStatus: importing
    Q->>DB: Load profile by id
    Q->>IG: getInstagramProfile(profile.instagram)
    alt Photo available
      Q->>ST: Upload image
      ST-->>Q: photoUrl
    end
    Q->>DB: Update profile fields (name, bio, website, photo, followersCount, importStatus: success, importError: null, source: instagram)
    Q-->>Q: Complete
  and Error path
    Q-->>Q: Catch error
    Q->>DB: Update importStatus: failed, importError
    Q-->>Q: Rethrow/error logged
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A carrot-orange sunrise over code I hop,
Sniffing new routes where imports pop.
“@username?” I twitch—let’s fetch and see!
Tasks burrow deep, bring data to me.
Followers counted, bios aligned—
One leap, profile refined.
Thump-thump: shipped, and perfectly timed.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: Event Editor: Select Profile (import)" concisely describes the primary change — adding an import/select-profile flow in the Event Editor — and matches the implemented UI and backend work for Instagram profile import.
Linked Issues Check ✅ Passed This PR implements the objectives of issue #418: the ProfileInput extracts Instagram usernames and displays an "Import @username" option, the createFromInstagram procedure creates a profile with importStatus 'requested' and source 'instagram', and the code enqueues a background import task that performs the Instagram fetch and updates importStatus/importError accordingly; Facebook import is not implemented as requested.
Out of Scope Changes Check ✅ Passed I do not find significant out-of-scope changes: the frontend, server API, trigger task, Prisma fields, Instagram fetch helper, and package dependency all directly support the Instagram import feature, and added fields like importError and source are reasonable supportive additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch event-editor-instagram-profile

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
package.json (1)

56-56: Pin and scope instagram-private-api usage

  • Pin the version to avoid unexpected breaks and document that it’s server-only (Trigger/CLI) with required envs (INSTAGRAM_USERNAME/PASSWORD).

Apply:

-    "instagram-private-api": "^1.46.1",
+    "instagram-private-api": "1.46.1",
prisma/schema.prisma (1)

179-182: Model import fields as enums; store long errors; add index

  • Use enums for statuses/sources to prevent invalid values.
  • Store importError as Text explicitly and index importStatus for quick dashboards.

Apply:

-  importStatus String? // 'requested', 'importing', 'success', 'failed'
-  importError  String? // Store error messages if import fails
-  source       String? //'instagram' to track where data came from
+  importStatus ImportStatus? @default(requested)
+  importError  String? @db.Text
+  source       ImportSource?
+
+  @@index([importStatus])

Add below (anywhere after models):

+enum ImportStatus {
+  requested
+  importing
+  success
+  failed
+}
+
+enum ImportSource {
+  instagram
+}
cli/import-organizer/index.ts (1)

9-11: Harden Instagram URL detection and export for reuse

  • Current includes() will accept non-profile paths.

Apply:

-function isInstagramUrl(url: string): boolean {
-  return url.includes('instagram.com/')
-}
+export function isInstagramUrl(url: string): boolean {
+  try {
+    const u = new URL(url.startsWith('http') ? url : `https://${url}`)
+    if (!/instagram\.com$/i.test(u.hostname)) return false
+    const first = u.pathname.replace(/^\/+|\/+$/g, '').split('/')[0]?.toLowerCase()
+    return !!first && !['p','reel','reels','stories','explore','accounts','tv','direct'].includes(first)
+  } catch {
+    return false
+  }
+}
cli/import-organizer/instagram.ts (1)

1-1: Remove unused import

Apply:

-import { IgApiClient, IgCheckpointError } from 'instagram-private-api'
+import { IgApiClient } from 'instagram-private-api'
components/inputs/ProfileInput.vue (1)

15-17: Harden Instagram URL detection

Apply:

-const isInstagramUrl = computed(() => {
-  return searchQuery.value.includes('instagram.com/')
-})
+const isInstagramUrl = computed(() =>
+  /https?:\/\/(www\.)?instagram\.com\//i.test(searchQuery.value)
+)
trigger/instagram-import.ts (1)

28-57: Improve logs, handle Cloudinary failures gracefully

  • Fix typos for clearer observability and fall back to remote URL if upload fails.

Apply:

-      //Scrape here
+      // Scrape here
       const instagramData = await getInstagramProfile(profile.instagram)
-
-      logger.log('Succesfull scrpaed insta data')
+      logger.log('Successfully scraped Instagram data')
 
       let photoUrl: string | undefined
       if (instagramData.photoUrl) {
-        photoUrl = await getUploadedImage(instagramData.photoUrl)
+        try {
+          photoUrl = await getUploadedImage(instagramData.photoUrl)
+        } catch {
+          logger.warn('Cloudinary upload failed, using remote photo URL')
+          photoUrl = instagramData.photoUrl
+        }
       }
 
       const updateProfile = await prisma.profile.update({
         where: { id: payload.profileId },
         data: {
           name: instagramData.fullName,
           bio: instagramData.biography,
           website: instagramData.externalUrl,
           photo: photoUrl,
           followersCount: instagramData.followerCount,
           importStatus: 'success',
           source: 'instagram',
         },
       })
       logger.log('Successfully imported profile')
-      // Returning this just for monitring  on trigger.dev
+      // Returning this just for monitoring on trigger.dev
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a971c6 and d7436e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • cli/import-organizer/index.ts (1 hunks)
  • cli/import-organizer/instagram.ts (1 hunks)
  • components/inputs/ProfileInput.vue (2 hunks)
  • package.json (1 hunks)
  • prisma/schema.prisma (1 hunks)
  • server/trpc/routers/profiles.ts (2 hunks)
  • trigger/instagram-import.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
cli/import-organizer/index.ts (3)
cli/import-event/facebook_import.ts (1)
  • getOrg (19-67)
cli/importer/profile.ts (1)
  • addProfile (319-459)
cli/import-event/schema_import.ts (1)
  • getOrg (26-91)
trigger/instagram-import.ts (3)
server/prisma.ts (1)
  • prisma (5-8)
cli/import-organizer/instagram.ts (1)
  • getInstagramProfile (14-38)
cli/utils/cloudinary.ts (1)
  • getUploadedImage (3-17)
cli/import-organizer/instagram.ts (2)
cli/import-organizer/index.ts (1)
  • extractInstagramUsername (13-18)
cli/importer/profile.ts (1)
  • addProfile (319-459)
prisma/schema.prisma (1)
cli/importer/profile.ts (1)
  • addProfile (319-459)
server/trpc/routers/profiles.ts (2)
cli/import-organizer/index.ts (1)
  • extractInstagramUsername (13-18)
server/trpc/routers/events.ts (1)
  • input (562-578)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (4)
components/inputs/ProfileInput.vue (2)

90-99: Nice UX touch

  • Import CTA is clear and discoverable.

100-102: Empty state logic looks good

trigger/instagram-import.ts (2)

12-16: Status transition upfront is good


18-26: Guard against missing instagram URL

  • Clear error early. LGTM.

-Fix typos and implement fetchOrganizer
-Handle undefined mutation result; fix typos in logs/comments
-Validate credentials and avoid logging in with undefined; minor hardening
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
cli/import-organizer/index.ts (1)

12-20: Make username extraction robust (stories URLs, fragments/queries, non-IG inputs)

Current logic returns stories for story URLs and can emit garbage for non-IG links. Normalize, handle stories path, and short domain; fall back to '' for non-IG inputs.

-export function extractInstagramUsername(url: string): string {
-  const s = url.replace(/https?:\/\/(www\.)?instagram\.com\//i, '')
-  return s
-    .split('?')[0]
-    .split('#')[0]
-    .replace(/\/$/, '')
-    .split('/')[0]
-    .replace(/^@/, '')
-}
+export function extractInstagramUsername(raw: string): string {
+  const s = raw.trim()
+  // Guard against non-IG inputs early; still allow @username
+  if (!isInstagramUrl(s)) return s.startsWith('@') ? s.slice(1) : ''
+  // Strip protocol and known IG domains (including instagr.am)
+  const noProto = s.replace(/^https?:\/\//i, '')
+  const withoutDomain = noProto
+    .replace(/^(www\.)?instagram\.com\//i, '')
+    .replace(/^(www\.)?instagr\.am\//i, '')
+  const path = withoutDomain.split(/[?#]/)[0].replace(/\/+$/, '')
+  if (!path) return ''
+  if (path.startsWith('@')) return path.slice(1)
+  const parts = path.split('/')
+  if (parts[0].toLowerCase() === 'stories' && parts[1]) {
+    return decodeURIComponent(parts[1]).replace(/^@/, '')
+  }
+  return decodeURIComponent(parts[0]).replace(/^@/, '')
+}
🧹 Nitpick comments (6)
cli/import-organizer/index.ts (2)

1-2: Import the return type to make the API explicit

Make the return type of fetchOrganizer explicit for safer callsites and better inference.

 import { getInstagramProfile } from './instagram'
+import type { InstagramProfileData } from './instagram'
 

3-6: Type the function result: Promise<InstagramProfileData | null>

This function always returns either Instagram data or null. Encode that in the signature.

-export async function fetchOrganizer(sourceUrl: string) {
+export async function fetchOrganizer(sourceUrl: string): Promise<InstagramProfileData | null> {
   if (!isInstagramUrl(sourceUrl)) return null
   return getInstagramProfile(sourceUrl)
 }
trigger/instagram-import.ts (4)

3-5: Remove unused import

getInstagramProfile isn’t used here.

-import { getInstagramProfile } from '~/cli/import-organizer/instagram'
 import { fetchOrganizer } from '~/cli/import-organizer'
 import { getUploadedImage } from '~/cli/utils/cloudinary'

20-22: Query by primary key with findUniqueOrThrow

You’re filtering by id (unique). Use the dedicated method.

-      const profile = await prisma.profile.findFirstOrThrow({
-        where: { id: payload.profileId },
-      })
+      const profile = await prisma.profile.findUniqueOrThrow({
+        where: { id: payload.profileId },
+      })

38-38: Polish log message

Minor typo.

-      logger.log('Succesfull scrpaed insta data')
+      logger.log('Successfully scraped Instagram data')

64-69: Log the error details for observability

Capture the message alongside the profileId.

-      const errorMessage =
-        error instanceof Error ? error.message : 'Unknown error'
-      logger.error(`Instagram import failed for profile ${payload.profileId}:`)
+      const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+      logger.error(
+        `Instagram import failed for profile ${payload.profileId}: ${errorMessage}`
+      )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7436e9 and 1ed021d.

📒 Files selected for processing (5)
  • cli/import-organizer/index.ts (1 hunks)
  • cli/import-organizer/instagram.ts (1 hunks)
  • components/inputs/ProfileInput.vue (2 hunks)
  • server/trpc/routers/profiles.ts (2 hunks)
  • trigger/instagram-import.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • cli/import-organizer/instagram.ts
  • components/inputs/ProfileInput.vue
  • server/trpc/routers/profiles.ts
🧰 Additional context used
🧬 Code graph analysis (2)
cli/import-organizer/index.ts (1)
cli/import-organizer/instagram.ts (1)
  • getInstagramProfile (14-41)
trigger/instagram-import.ts (3)
server/prisma.ts (1)
  • prisma (5-8)
cli/import-organizer/index.ts (1)
  • fetchOrganizer (3-6)
cli/utils/cloudinary.ts (1)
  • getUploadedImage (3-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (2)
trigger/instagram-import.ts (2)

45-55: LGTM on the update payload shape

Fields map cleanly from scraped data; setting photo conditionally is correct.


45-55: Verify Prisma schema fields and Prisma client source

  • followersCount, importError and source exist; importStatus is present only in /prisma/schema.prisma (≈lines 179–181) and is missing from /cli/prisma/schema.prisma (followersCount@163, importError@280, source@360).
  • Action: ensure the Prisma client used at runtime is generated from the schema that defines importStatus or add the field to the schema in use and run prisma generate/migrate.

-validate and normalize Instagram URLs; fix type value
-Fix circular import: move URL parsing into a shared util
-Harden URL check to avoid false positives and handle @username
-Clear stale importError on success
-Avoid overwriting photo with an empty string
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ed021d and 8e21f83.

📒 Files selected for processing (5)
  • cli/import-organizer/index.ts (1 hunks)
  • cli/import-organizer/instagram.ts (1 hunks)
  • cli/import-organizer/parse.ts (1 hunks)
  • server/trpc/routers/profiles.ts (2 hunks)
  • trigger/instagram-import.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/trpc/routers/profiles.ts
  • trigger/instagram-import.ts
  • cli/import-organizer/index.ts
  • cli/import-organizer/instagram.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test

-Gate Import CTA on a valid Instagram username; fix extractInstagramUsername
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)

16-18: LGTM: UI now gates on a valid extracted username

Import CTA only appears when a non-empty username is extracted—addresses earlier feedback.

🧹 Nitpick comments (12)
cli/import-organizer/parse.ts (5)

10-13: Canonicalize usernames to lowercase to avoid duplicates/inconsistent keys

Returning mixed-case usernames can create duplicate records or cache misses. Canonicalize to lowercase at extraction points.

-    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0]
-    return isValidInstagramUsername(username) ? username : ''
+    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
+    return isValidInstagramUsername(username) ? username : ''
-    return isValidInstagramUsername(candidate) ? candidate : ''
+    const canonical = candidate.toLowerCase()
+    return isValidInstagramUsername(canonical) ? canonical : ''
-    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0]
-    return isValidInstagramUsername(username) ? username : ''
+    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
+    return isValidInstagramUsername(username) ? username : ''

Also applies to: 31-36, 48-53


15-18: Support plain “user” inputs or remove unreachable fallback

With the current flow, new URL() won’t throw for user (it becomes https://user), so the catch-based “bare username” fallback is effectively dead. Either explicitly support plain usernames before the try, or drop the fallback to reduce confusion. Example to support:

   try {
+    // Accept plain usernames like "user" (no protocol/host)
+    if (isValidInstagramUsername(raw)) {
+      return raw.toLowerCase()
+    }
     // Add https:// if no protocol present
     const urlString = /^[a-z]+:\/\//i.test(raw) ? raw : `https://${raw}`
     const u = new URL(urlString)
@@
-  } catch {
-    // Fallback: treat as bare username
-    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0]
-    return isValidInstagramUsername(username) ? username : ''
-  }
+  } catch {
+    return ''
+  }

Also applies to: 49-53


1-4: Name vs. behavior: isInstagramUrl() returns true for “@user

This helper accepts handles, not only URLs. Consider renaming to isInstagramInput() or documenting that handles are allowed.


38-46: Reserved routes list looks good; keep it centralized

Consider hoisting the reserved routes into a const RESERVED = new Set([...]) to avoid regex rebuilds and ease maintenance if the list grows.


6-54: Add unit tests for parser edge cases

Cover positives: “@user”, “instagram.com/user/”, “m.instagram.com/_u/user”, “instagr.am/user?x=1”. Negatives: bare domain, “/p/...”, “/reel/...”, reserved routes, trailing dot, consecutive dots.

I can generate a Jest/Vitest spec for these cases if helpful.

components/inputs/ProfileInput.vue (7)

85-90: Make the Import CTA accessible (keyboard + semantics)

Render as a button or add role/keyboard handlers for accessibility.

-            <div
-              class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
-              @click="importFromInstagram"
-            >
+            <div
+              role="button"
+              tabindex="0"
+              class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
+              @click="importFromInstagram"
+              @keydown.enter.prevent="importFromInstagram"
+              @keydown.space.prevent="importFromInstagram"
+              :aria-label="`Import Instagram profile @${instagramUsername}`"
+            >

88-90: Match spec copy: show “Import @username@”

Issue #418 requests “Import @username@”. Update label to match.

-              <span>Import {{ instagramUsername }}</span>
+              <span>Import @{{ instagramUsername }}@</span>

93-95: Avoid double messaging with Import CTA

Hide “No profiles found” when the Import option is visible.

-          <ComboboxEmpty v-if="!data?.length">
+          <ComboboxEmpty v-else-if="!data?.length">
             No profiles found.
           </ComboboxEmpty>

20-34: Optional: add a loading state to prevent double‑clicks

Disable the Import CTA and show a spinner while the mutation is in flight to avoid duplicate requests.


8-12: Optional: don’t show “No profiles found” while loading

Expose isPending/status from useQuery and gate the empty state accordingly.

-const { data } = useQuery<any>({
+const { data, isPending } = useQuery<any>({
   queryKey: ['profiles.search', searchQuery],
   queryFn: () => $client.profiles.search.query({ query: searchQuery.value }),
   retry: false,
})

And in template:

<ComboboxEmpty v-else-if="!isPending && !data?.length">No profiles found.</ComboboxEmpty>

3-3: Optional: type the model/profile

Replace Ref<any> with your Profile type for better DX and safety.


38-38: Optional: simplify Popover binding

Use v-model:open="isOpen" instead of manual update handler.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e21f83 and 471aca5.

📒 Files selected for processing (5)
  • cli/import-organizer/index.ts (1 hunks)
  • cli/import-organizer/instagram.ts (1 hunks)
  • cli/import-organizer/parse.ts (1 hunks)
  • components/inputs/ProfileInput.vue (3 hunks)
  • server/trpc/routers/profiles.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • cli/import-organizer/index.ts
  • cli/import-organizer/instagram.ts
  • server/trpc/routers/profiles.ts
🧰 Additional context used
🧬 Code graph analysis (1)
cli/import-organizer/parse.ts (1)
cli/import-organizer/index.ts (2)
  • isInstagramUrl (4-4)
  • extractInstagramUsername (4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (2)
cli/import-organizer/parse.ts (1)

56-67: Username validator is solid

Length, charset, and dot rules align with Instagram constraints.

components/inputs/ProfileInput.vue (1)

24-30: Guarded assignment and cleanup sequence look good

Checks newProfile before assigning and then closes/clears—sane flow.

Browser-safe duplication of the backend parsing logic to avoid hydration errors
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)

16-30: Close only on success, canonicalize URL, and guard re-entrancy to avoid duplicate imports.

Popover closes even if mutation returns undefined; there’s no in‑flight guard, so multiple clicks can race. Canonicalize to https://instagram.com/{username}/ to reduce server parsing variants.

Apply this diff:

 const importFromInstagram = async () => {
-  // The guard now correctly checks if a valid username was extracted
-  if (!instagramUsername.value) return
+  if (!instagramUsername.value || isImporting.value) return
+  isImporting.value = true
   try {
-    const newProfile = await $client.profiles.createFromInstagram.mutate({
-      instagramUrl: searchQuery.value,
+    const canonicalUrl = `https://instagram.com/${instagramUsername.value}/`
+    const newProfile = await $client.profiles.createFromInstagram.mutate({
+      instagramUrl: canonicalUrl,
     })
-    if (newProfile) model.value = newProfile
-    isOpen.value = false
-    searchQuery.value = ''
+    if (newProfile) {
+      model.value = newProfile
+      isOpen.value = false
+      searchQuery.value = ''
+    } else {
+      console.warn('Import returned no profile.')
+    }
   } catch (error) {
     console.error('Failed to import Instagram profile:', error)
     // TODO: add toast for failure
-  }
+  } finally {
+    isImporting.value = false
+  }
 }

Add this outside the function:

const isImporting = ref(false)
🧹 Nitpick comments (4)
components/inputs/ProfileInput.vue (4)

32-41: Keep client/server parsing in sync; prefer shared util instead of duplicating.

This duplicates backend parsing; drift is likely. Extract to a shared, browser‑safe util (e.g., utils/instagram.ts) used by both client and server.


32-41: Tighten username validator (lowercase, consistent with normalization).

If we normalize to lowercase, we can simplify the pattern and keep the “no trailing dot / no consecutive dots” rules.

Apply this diff:

-function isValidInstagramUsername(username: string): boolean {
+function isValidInstagramUsername(username: string): boolean {
   if (!username || username.length === 0) return false
   if (username.length > 30) return false // Instagram username limit

-  const validPattern = /^[a-zA-Z0-9_]([a-zA-Z0-9_.]*[a-zA-Z0-9_])?$/
+  // lowercased upstream; enforce lowercase here for consistency
+  const validPattern = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/
   const hasConsecutivePeriods = /\.\./.test(username)

   return validPattern.test(username) && !hasConsecutivePeriods
 }

139-149: Disable the “Import …” action while in-flight to prevent duplicate creates.

Prevent rapid clicks; show a spinner for feedback.

Apply this diff:

-          <div v-if="instagramUsername">
-            <div
-              class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
-              @click="importFromInstagram"
-            >
-              <Icon name="heroicons:link" class="h-4 w-4" />
-              <span>Import {{ instagramUsername }}</span>
-            </div>
-          </div>
+          <div v-if="instagramUsername">
+            <div
+              class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
+              :class="{ 'opacity-60 pointer-events-none': isImporting }"
+              @click="importFromInstagram"
+            >
+              <Icon v-if="!isImporting" name="heroicons:link" class="h-4 w-4" />
+              <Icon v-else name="heroicons:arrow-path" class="h-4 w-4 animate-spin" />
+              <span>Import {{ instagramUsername }}</span>
+            </div>
+          </div>

150-152: Hide “No profiles found” while fetching.

Avoid flashing an empty state during load.

Apply this diff:

-          <ComboboxEmpty v-if="!data?.length">
+          <ComboboxEmpty v-if="!isFetching && !data?.length">
             No profiles found.
           </ComboboxEmpty>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 471aca5 and 2805de5.

📒 Files selected for processing (1)
  • components/inputs/ProfileInput.vue (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)

12-14: LGTM: computed username extraction.


120-120: Verify by="label" is intended; likely should be by="id".

Profiles use :key="profile.id". If equality is by label, selection may break when labels collide.

Apply this diff if appropriate:

-      <Combobox by="label" v-model="model">
+      <Combobox by="id" v-model="model">

-simplify  username extraction function in frontend,
-add toasts
-Fix query key to depend on the input value (avoid cache collisions).
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
components/inputs/ProfileInput.vue (7)

4-4: Prefer defineModel generics over casting; avoid missing Ref import

Avoid the cast and the need to import Ref by parameterizing defineModel.

-const model = defineModel() as Ref<any>
+const model = defineModel<any>()

7-13: Minor: clarify queryFn param and keep key logic simple

Rename the destructured param to avoid shadowing the outer queryKey and read the term directly.

-  queryFn: ({ queryKey }) =>
-    $client.profiles.search.query({ query: queryKey[1] as string }),
+  queryFn: ({ queryKey: [, term] }) =>
+    $client.profiles.search.query({ query: term as string }),

21-38: Canonicalize payload, harden toast error, and avoid naming shadow

Send a canonical Instagram URL built from the extracted username; return a safer error fallback; avoid shadowing data.

-  const promise = $client.profiles.createFromInstagram.mutate({
-    instagramUrl: searchQuery.value,
-  })
+  const promise = $client.profiles.createFromInstagram.mutate({
+    instagramUrl: `https://instagram.com/${instagramUsername.value}`,
+  })
@@
-    success: (data: any) => {
-      if (data) model.value = data
+    success: (profile: any) => {
+      if (profile) model.value = profile
       isOpen.value = false
       searchQuery.value = ''
       return 'Profile import scheduled successfully!'
     },
-    error: (error: any) => (error as Error).message,
+    error: (err: any) => (err?.message ?? 'Failed to schedule import.'),

51-68: Improve URL handling: support /_u/ and case-insensitive host; keep story deep-links

Currently, instagram.com/_u/username won’t parse, and host match isn’t case-insensitive.

-  const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
-  const candidate = match ? match[1] : ''
-
-  // A guard for the most common non-profile routes
-  if (/^(p|reel|stories)$/i.test(candidate)) {
-    return ''
-  }
-
-  const normalized = candidate.toLowerCase()
-  return isValidInstagramUsername(normalized) ? normalized : ''
+  // Try to parse instagram.com URLs (scheme and www. optional, case-insensitive)
+  const m = raw.match(/(?:^|https?:\/\/(?:www\.)?)instagram\.com\/([^?#]+)/i)
+  const path = m ? m[1] : ''
+  const segments = path.split('/').filter(Boolean)
+  let first = segments[0] ?? ''
+  // Handle deep-links like /_u/username and /stories/username
+  if (/^_?u$/i.test(first) || /^stories$/i.test(first)) {
+    first = segments[1] ?? ''
+  }
+  // Guard common non-profile routes
+  if (/^(p|reel|explore|accounts|tags|locations|challenge|about|web|api)$/i.test(first)) {
+    return ''
+  }
+  const normalized = first.replace(/^@+/, '').toLowerCase()
+  return isValidInstagramUsername(normalized) ? normalized : ''

117-125: Prefix @ in the Import label to match expectation

Add a leading “@” for clarity (spec mentions “Import @username@”; at minimum, prefixing is expected).

-              <span>Import {{ instagramUsername }}</span>
+              <span>Import @{{ instagramUsername }}</span>

127-129: Hide empty state when import is available or before typing

Avoid showing “No profiles found.” while an import option is present or before any input.

-          <ComboboxEmpty v-if="!data?.length">
+          <ComboboxEmpty v-if="!data?.length && !instagramUsername && searchQuery">
             No profiles found.
           </ComboboxEmpty>

40-40: Centralize IG parsing (avoid client/server drift)

Duplicate parsing exists: components/inputs/ProfileInput.vue defines browser-side isValidInstagramUsername/extractInstagramUsername while cli/import-organizer/parse.ts exports the same helpers (server/trpc/routers/profiles.ts dynamically imports it). Extract a pure, isomorphic parsing util (e.g., src/lib/instagram.ts), export isInstagramUrl/extractInstagramUsername/isValidInstagramUsername, import it in the Vue component and server code, and add unit tests. If parse.ts contains Node-only logic, expose a browser-safe subset for the client and keep Node-specific behavior server-side.

Locations: components/inputs/ProfileInput.vue (browser parsing, ~lines 41–68); cli/import-organizer/parse.ts (exports); server/trpc/routers/profiles.ts (dynamic import ~line 339).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2805de5 and 4f86241.

📒 Files selected for processing (1)
  • components/inputs/ProfileInput.vue (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (1)
components/inputs/ProfileInput.vue (1)

41-49: Username validation looks solid

Length cap, start/end constraints, and consecutive dot check align with IG rules.

-defineModel generics
-queryFn param clarity
-Canonicalize payload
-fix variable naming (profile vs data) for better  clarity
-Prefix @ in Import label
-Hide empty state logic
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)

21-38: Add error handling for undefined profile response.

The mutation might return undefined if the profile creation fails silently on the backend. While you're using toast.promise which handles errors, the success handler should guard against undefined values.

Apply this diff to add a guard:

   toast.promise(promise, {
     loading: 'Scheduling profile import...',
     success: (profile: any) => {
-      if (profile) model.value = profile
-      isOpen.value = false
-      searchQuery.value = ''
-      return 'Profile import scheduled successfully!'
+      if (profile) {
+        model.value = profile
+        isOpen.value = false
+        searchQuery.value = ''
+        return 'Profile import scheduled successfully!'
+      }
+      return 'Profile import scheduled but no profile returned'
     },
     error: (error: any) => (error as Error).message,
   })
🧹 Nitpick comments (2)
components/inputs/ProfileInput.vue (2)

10-11: Fix destructuring - use consistent naming with queryKey.

The destructuring uses [, term] but the queryKey has two elements ['profiles.search', searchQuery.value.trim()]. While this works, it's cleaner to use the entire queryKey structure consistently.

Apply this diff for clarity:

-  queryFn: ({ queryKey: [, term] }) =>
-    $client.profiles.search.query({ query: term as string }),
+  queryFn: ({ queryKey }) =>
+    $client.profiles.search.query({ query: queryKey[1] as string }),

51-68: Consider additional edge cases in username extraction.

The current implementation handles common cases well, but could be improved to handle:

  1. URLs with trailing slashes after username (already handled via split)
  2. URLs with query parameters or fragments (handled via split on /)
  3. Deep link formats like instagram.com/_u/username

The logic is mostly correct, but the regex on line 58 doesn't handle all edge cases properly.

Apply this diff to make the extraction more robust:

 function extractInstagramUsername(input: string): string {
   const raw = input.trim()
 
   if (raw.startsWith('@')) {
     const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
     return isValidInstagramUsername(username) ? username : ''
   }
-  const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
-  const candidate = match ? match[1] : ''
-
-  // A guard for the most common non-profile routes
-  if (/^(p|reel|stories)$/i.test(candidate)) {
-    return ''
+  
+  // Try to parse as URL
+  try {
+    const url = new URL(raw.startsWith('http') ? raw : `https://${raw}`)
+    
+    // Check if it's Instagram
+    if (!url.hostname.includes('instagram.com')) return ''
+    
+    // Extract path parts
+    const pathParts = url.pathname.split('/').filter(Boolean)
+    if (!pathParts.length) return ''
+    
+    let candidate = pathParts[0]
+    
+    // Handle deep links like /_u/username
+    if (candidate === '_u' && pathParts.length > 1) {
+      candidate = pathParts[1]
+    }
+    
+    // Skip non-profile routes
+    const reservedRoutes = ['p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'direct', 'tv']
+    if (reservedRoutes.includes(candidate.toLowerCase())) {
+      return ''
+    }
+    
+    const normalized = candidate.toLowerCase()
+    return isValidInstagramUsername(normalized) ? normalized : ''
+  } catch {
+    // If URL parsing fails, try simple extraction
+    const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
+    if (!match) return ''
+    
+    const candidate = match[1].toLowerCase()
+    const reservedRoutes = ['p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'direct', 'tv']
+    
+    if (reservedRoutes.includes(candidate)) return ''
+    return isValidInstagramUsername(candidate) ? candidate : ''
   }
-
-  const normalized = candidate.toLowerCase()
-  return isValidInstagramUsername(normalized) ? normalized : ''
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f86241 and 818075d.

📒 Files selected for processing (1)
  • components/inputs/ProfileInput.vue (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)

117-125: LGTM! Clean implementation of the import button.

The UI implementation for the Instagram import button is well-structured with proper icon usage and clear labeling.


127-131: Good conditional logic for empty state.

The condition properly checks for the absence of search results, absence of extractable Instagram username, and presence of a search query before showing "No profiles found."

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)

7-13: Expose isFetching and gate empty-state to avoid flicker (partial duplicate of prior suggestion).

You fixed the query key and gating—nice. Still missing isFetching to suppress the “No profiles found” state while the search is in flight.

Update the query hook to also return isFetching:

const { data, isFetching } = useQuery<any>({
  queryKey,
  queryFn: ({ queryKey: [, term] }) =>
    $client.profiles.search.query({ query: term as string }),
  enabled: computed(() => !!(queryKey.value[1] as string)),
  retry: false,
})
🧹 Nitpick comments (4)
components/inputs/ProfileInput.vue (4)

21-41: Guard double‑clicks and add a fallback error message for toast.promise.

Users can trigger multiple imports quickly; also error message may be empty. Add a simple in‑flight guard and a default error string.

 const importFromInstagram = async () => {
-  if (!instagramUsername.value) return
+  if (!instagramUsername.value || isImporting.value) return
+  isImporting.value = true
 
   const promise = $client.profiles.createFromInstagram.mutate({
     instagramUrl: `https://instagram.com/${instagramUsername.value}`,
   })
 
   toast.promise(promise, {
     loading: 'Scheduling profile import...',
     success: (profile: any) => {
       if (profile) {
         model.value = profile
         isOpen.value = false
         searchQuery.value = ''
         return 'Profile import scheduled successfully!'
       }
       return 'Profile import scheduled but no profile returned'
     },
-    error: (error: any) => (error as Error).message,
+    error: (error: any) => (error as Error).message || 'Failed to schedule profile import.',
   })
+  await promise.finally(() => {
+    isImporting.value = false
+  })
 }

Add this state near the other refs:

const isImporting = ref(false)

54-71: Make username extraction robust: handle /_u/ deep‑links, reserved routes, and subdomains.

Current regex misclassifies routes like /accounts and misses /_u/username. Parse the URL, skip reserved paths, support m./www. subdomains, and allow bare usernames.

-function extractInstagramUsername(input: string): string {
-  const raw = input.trim()
-
-  if (raw.startsWith('@')) {
-    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
-    return isValidInstagramUsername(username) ? username : ''
-  }
-  const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
-  const candidate = match ? match[1] : ''
-
-  // A guard for the most common non-profile routes
-  if (/^(p|reel|stories)$/i.test(candidate)) {
-    return ''
-  }
-
-  const normalized = candidate.toLowerCase()
-  return isValidInstagramUsername(normalized) ? normalized : ''
-}
+function extractInstagramUsername(input: string): string {
+  const raw = input.trim()
+  if (!raw) return ''
+
+  // Quick path: @handle
+  if (raw.startsWith('@')) {
+    const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
+    return isValidInstagramUsername(username) ? username : ''
+  }
+
+  // Try parse as URL (tolerate missing scheme)
+  let url: URL
+  try {
+    url = raw.includes('://') ? new URL(raw) : new URL(`https://${raw}`)
+  } catch {
+    // Fallback: treat as bare username
+    const bare = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
+    return isValidInstagramUsername(bare) ? bare : ''
+  }
+
+  const host = url.hostname.replace(/^www\./, '').toLowerCase()
+  if (!/(^|\.)instagram\.com$/.test(host)) return ''
+
+  const parts = url.pathname.split('/').filter(Boolean)
+  if (parts.length === 0) return ''
+
+  // Common non-profile routes
+  const RESERVED = new Set([
+    'p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'about',
+    'developers', 'directory', 'web', 'oauth', 'challenge', 'legal',
+    'privacy', 'terms', 'press', 'business', 'help', 'tv', 'tags', 'locations',
+  ])
+
+  let candidate = parts[0]
+  if (candidate.toLowerCase() === '_u' && parts.length > 1) {
+    candidate = parts[1]
+  }
+  if (RESERVED.has(candidate.toLowerCase())) return ''
+
+  const username = candidate.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
+  return isValidInstagramUsername(username) ? username : ''
+}

121-127: Use a button for the Import CTA and disable it while importing (a11y + UX).

Button semantics enable keyboard activation and announce busy state.

-            <div
-              class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
-              @click="importFromInstagram"
-            >
-              <Icon name="heroicons:link" class="h-4 w-4" />
-              <span>Import @{{ instagramUsername }} from Instagram</span>
-            </div>
+            <button
+              type="button"
+              class="p-2 flex items-center gap-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-50"
+              :disabled="isImporting"
+              :aria-busy="isImporting ? 'true' : 'false'"
+              @click="importFromInstagram"
+            >
+              <Icon name="heroicons:link" class="h-4 w-4" />
+              <span>Import @{{ instagramUsername }} from Instagram</span>
+            </button>

130-134: Suppress “No profiles found” while fetching and trim input.

Prevents premature empty-state flashes.

-          <ComboboxEmpty
-            v-if="!data?.length && !instagramUsername && searchQuery"
-          >
+          <ComboboxEmpty
+            v-if="!isFetching && !data?.length && !instagramUsername && searchQuery.trim()"
+          >
             No profiles found.
           </ComboboxEmpty>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 818075d and 6414a7e.

📒 Files selected for processing (1)
  • components/inputs/ProfileInput.vue (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci
  • GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)

17-19: LGTM: clean computed for instagramUsername.

Lowercasing + validation before surfacing the CTA is solid.


28-40: LGTM: toast.promise flow is clear.

Good UX copy and state reset on success.

@github-project-automation github-project-automation bot moved this from Triage to Todo in v4 Launch Sep 16, 2025
Copy link
Collaborator

@razbakov razbakov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX of the ProfileInput is not good. It's not clear for the user that it's possible to import profile from instagram. in v3 there is a tip.

-Remove extractInstagramUsername, isInstagramUrl functions,
- Remove index.ts
-Add label texti indicating instagram profiles can be imported
-FIx Typos
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/trpc/routers/profiles.ts (1)

146-151: Bug: shadowed variable makes predicate always true.

event.id === event.id is tautological; you always pick the first attended event.

Apply:

-        const guestInfo = eventsAttended
-          .find((event) => event.id === event.id)
-          ?.guests.find((guest) => guest.profileId === profile.id)
+        const guestInfo = eventsAttended
+          .find((e) => e.id === event.id)
+          ?.guests.find((guest) => guest.profileId === profile.id)
🧹 Nitpick comments (4)
cli/import-organizer/instagram.ts (2)

38-41: Map API failures to a clear, non-leaky error.

Wrap usernameinfo to avoid leaking raw errors and to surface a concise message.

Apply:

-  await instagram.account.login(IG_USER, IG_PASS)
-  const userInfo = await instagram.user.usernameinfo(username)
+  await instagram.account.login(IG_USER, IG_PASS)
+  let userInfo
+  try {
+    userInfo = await instagram.user.usernameinfo(username)
+  } catch {
+    throw new Error(`Instagram lookup failed for @${username}`)
+  }

30-39: Consider session persistence to reduce login churn.

Persist IgApiClient state (cookies/device) to storage and reuse to avoid frequent logins and checkpoint risk.

components/inputs/ProfileInput.vue (2)

118-120: Typo in class name.

Fix “text-muted-foregorund”.

Apply:

-        <p class="text-xs text-muted-foregorund mt-1 px-1">
+        <p class="text-xs text-muted-foreground mt-1 px-1">

24-31: Canonicalize URL when mutating.

Minor: use www and trailing slash to match server normalization (avoids duplicate text in toasts/cache).

Apply:

-  const promise = $client.profiles.createFromInstagram.mutate({
-    instagramUrl: `https://instagram.com/${instagramUsername.value}`,
-  })
+  const promise = $client.profiles.createFromInstagram.mutate({
+    instagramUrl: `https://www.instagram.com/${instagramUsername.value}/`,
+  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6414a7e and 446d526.

📒 Files selected for processing (4)
  • cli/import-organizer/instagram.ts (1 hunks)
  • components/inputs/ProfileInput.vue (2 hunks)
  • server/trpc/routers/profiles.ts (2 hunks)
  • trigger/instagram-import.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • trigger/instagram-import.ts
🧰 Additional context used
🧬 Code graph analysis (1)
server/trpc/routers/profiles.ts (2)
server/trpc/init.ts (1)
  • publicProcedure (31-31)
server/prisma.ts (1)
  • prisma (5-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: ci
🔇 Additional comments (1)
components/inputs/ProfileInput.vue (1)

7-13: Nice: query key tied to primitive value with gating.

Prevents cache collisions and avoids empty queries.

Comment on lines +16 to +25
const username = instagramUrl
.replace('https://', '')
.replace('http://', '')
.replace('www.', '')
.replace('instagram.com/', '')
.replace('instagr.am/', '')
.replace(/\/$/, '')
.split('/')[0]
.split('?')[0]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use URL-based parsing; handle /_u and validate username.

String replaces are brittle; enforce host, support /_u/{username}, and validate.

Apply:

-  const username = instagramUrl
-    .replace('https://', '')
-    .replace('http://', '')
-    .replace('www.', '')
-    .replace('instagram.com/', '')
-    .replace('instagr.am/', '')
-    .replace(/\/$/, '')
-    .split('/')[0]
-    .split('?')[0]
+  const url = new URL(instagramUrl)
+  const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
+  let username = (first?.toLowerCase() === '_u' ? second : first) || ''
+  username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
+  const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+  const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
+  if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
+    throw new Error('Invalid Instagram URL: could not extract valid username')
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const username = instagramUrl
.replace('https://', '')
.replace('http://', '')
.replace('www.', '')
.replace('instagram.com/', '')
.replace('instagr.am/', '')
.replace(/\/$/, '')
.split('/')[0]
.split('?')[0]
const url = new URL(instagramUrl)
const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
let username = (first?.toLowerCase() === '_u' ? second : first) || ''
username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
throw new Error('Invalid Instagram URL: could not extract valid username')
}
🤖 Prompt for AI Agents
In cli/import-organizer/instagram.ts around lines 16 to 25, replace the brittle
chain of string.replace calls with URL-based parsing: if instagramUrl lacks a
scheme, prepend "https://", construct a URL object, assert the hostname endsWith
"instagram.com" or "instagr.am", then derive the username from the pathname — if
pathname starts with "/_u/" use the segment after "/_u/", otherwise take the
first non-empty path segment; strip any trailing slashes and ignore search/query
(use URL.pathname), then validate the resulting username against a regex like
/^[A-Za-z0-9._]+$/ and return or error for invalid values. Ensure you handle
plain usernames by detecting when the input is not a URL and fall back to
validating and returning it directly.

Comment on lines +54 to +71
function extractInstagramUsername(input: string): string {
const raw = input.trim()
if (raw.startsWith('@')) {
const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
return isValidInstagramUsername(username) ? username : ''
}
const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
const candidate = match ? match[1] : ''
// A guard for the most common non-profile routes
if (/^(p|reel|stories)$/i.test(candidate)) {
return ''
}
const normalized = candidate.toLowerCase()
return isValidInstagramUsername(normalized) ? normalized : ''
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Broaden extraction: support instagr.am and /_u; strengthen guards.

Current regex misses instagr.am and mis-parses /_u links (e.g., imports “u”). Normalize and validate.

Apply:

 function extractInstagramUsername(input: string): string {
   const raw = input.trim()

   if (raw.startsWith('@')) {
     const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
     return isValidInstagramUsername(username) ? username : ''
   }
-  const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
-  const candidate = match ? match[1] : ''
-
-  // A guard for the most common non-profile routes
-  if (/^(p|reel|stories)$/i.test(candidate)) {
-    return ''
-  }
-
-  const normalized = candidate.toLowerCase()
-  return isValidInstagramUsername(normalized) ? normalized : ''
+  const hostRe = /^(?:https?:\/\/)?(?:www\.)?(instagram\.com|instagr\.am)\//i
+  const afterHost = hostRe.test(raw)
+    ? raw.replace(hostRe, '')
+    : raw
+  // Handle /_u/username deep links
+  const parts = afterHost.split(/[/?#]/).filter(Boolean)
+  let candidate = parts[0] && parts[0].toLowerCase() === '_u' ? parts[1] || '' : parts[0] || ''
+  candidate = candidate.replace(/^@+/, '').toLowerCase()
+  const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+  if (!candidate || reserved.has(candidate)) return ''
+  return isValidInstagramUsername(candidate) ? candidate : ''
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function extractInstagramUsername(input: string): string {
const raw = input.trim()
if (raw.startsWith('@')) {
const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
return isValidInstagramUsername(username) ? username : ''
}
const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
const candidate = match ? match[1] : ''
// A guard for the most common non-profile routes
if (/^(p|reel|stories)$/i.test(candidate)) {
return ''
}
const normalized = candidate.toLowerCase()
return isValidInstagramUsername(normalized) ? normalized : ''
}
function extractInstagramUsername(input: string): string {
const raw = input.trim()
if (raw.startsWith('@')) {
const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
return isValidInstagramUsername(username) ? username : ''
}
const hostRe = /^(?:https?:\/\/)?(?:www\.)?(instagram\.com|instagr\.am)\//i
const afterHost = hostRe.test(raw)
? raw.replace(hostRe, '')
: raw
// Handle /_u/username deep links
const parts = afterHost.split(/[/?#]/).filter(Boolean)
let candidate = parts[0] && parts[0].toLowerCase() === '_u' ? parts[1] || '' : parts[0] || ''
candidate = candidate.replace(/^@+/, '').toLowerCase()
const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
if (!candidate || reserved.has(candidate)) return ''
return isValidInstagramUsername(candidate) ? candidate : ''
}
🤖 Prompt for AI Agents
In components/inputs/ProfileInput.vue around lines 54 to 71, the Instagram
extraction logic should be expanded to recognize both instagram.com and
instagr.am hosts and to correctly handle URLs using the '/_u/username' path
(avoiding returning just "u"); update the regex to match both domains and
capture the username as the first actual profile path segment (allowing an
optional '_u/' prefix), then normalize to lower case and run the existing
isValidInstagramUsername check; also strengthen the guard to reject common
non-profile segments (p, reel, stories, _u, direct, explore, about, etc.) before
validating so only real usernames are returned.

Comment on lines +326 to +377
createFromInstagram: publicProcedure
.input(
z.object({
instagramUrl: z.string().url(),
})
)
.mutation(async ({ input }) => {
if (
!input.instagramUrl.includes('instagram.com/') &&
!input.instagramUrl.includes('instagr.am/')
) {
throw new Error('Only Instagram profile URLs are supported')
}
const username = input.instagramUrl
.replace('https://', '')
.replace('http://', '')
.replace('www.', '')
.replace('instagram.com/', '')
.replace('instagr.am/', '')
.replace(/\/$/, '')
.split('/')[0]
.split('?')[0]
const instagramUrl = `https://www.instagram.com/${username}/`

const existingProfile = await prisma.profile.findFirst({
where: {
OR: [
{ username: { equals: username, mode: 'insensitive' } },
{ instagram: { equals: instagramUrl, mode: 'insensitive' } },
],
},
})

if (existingProfile) return existingProfile

const newProfile = await prisma.profile.create({
data: {
username,
name: username,
instagram: instagramUrl,
type: 'Organiser',
importStatus: 'requested',
source: 'instagram',
visibility: 'Public',
},
})
// Trigger the import job here
await tasks.trigger('import-instagram-profile', {
profileId: newProfile.id,
})
return newProfile
}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Public endpoint invites abuse; gate or rate‑limit.

Anyone can create/import profiles. Require auth and/or rate-limit by user/IP; optionally restrict to users with event-edit permissions.

🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 326-377, the createFromInstagram
mutation is a public endpoint that allows unauthenticated mass profile creation;
require authentication (or switch to your existing createProtectedProcedure /
check session/userId at start) and enforce rate-limiting (per-user or per-IP via
your rate limiter or Redis token bucket) before proceeding; additionally
optionally verify the user has the required event-edit/import permission and
return an appropriate auth/429 error if checks fail; keep triggering the import
job but associate it with the authenticated userId for auditing.

Comment on lines +333 to +348
if (
!input.instagramUrl.includes('instagram.com/') &&
!input.instagramUrl.includes('instagr.am/')
) {
throw new Error('Only Instagram profile URLs are supported')
}
const username = input.instagramUrl
.replace('https://', '')
.replace('http://', '')
.replace('www.', '')
.replace('instagram.com/', '')
.replace('instagr.am/', '')
.replace(/\/$/, '')
.split('/')[0]
.split('?')[0]
const instagramUrl = `https://www.instagram.com/${username}/`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Harden Instagram URL parsing; support /_u links; validate username.

Current parsing admits non-profile routes (e.g., /p, /reel, /stories) and misses /_u/{username}. Normalize, validate, and canonicalize before proceeding.

Apply:

-      if (
-        !input.instagramUrl.includes('instagram.com/') &&
-        !input.instagramUrl.includes('instagr.am/')
-      ) {
-        throw new Error('Only Instagram profile URLs are supported')
-      }
-      const username = input.instagramUrl
-        .replace('https://', '')
-        .replace('http://', '')
-        .replace('www.', '')
-        .replace('instagram.com/', '')
-        .replace('instagr.am/', '')
-        .replace(/\/$/, '')
-        .split('/')[0]
-        .split('?')[0]
-      const instagramUrl = `https://www.instagram.com/${username}/`
+      const url = new URL(input.instagramUrl)
+      const host = url.hostname.toLowerCase()
+      if (!/^(www\.)?(instagram\.com|instagr\.am)$/.test(host)) {
+        throw new Error('Only Instagram profile URLs are supported')
+      }
+      const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
+      let username = (first?.toLowerCase() === '_u' ? second : first) || ''
+      username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
+      const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+      const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
+      if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
+        throw new Error('Invalid Instagram username')
+      }
+      const instagramUrl = `https://www.instagram.com/${username}/`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
!input.instagramUrl.includes('instagram.com/') &&
!input.instagramUrl.includes('instagr.am/')
) {
throw new Error('Only Instagram profile URLs are supported')
}
const username = input.instagramUrl
.replace('https://', '')
.replace('http://', '')
.replace('www.', '')
.replace('instagram.com/', '')
.replace('instagr.am/', '')
.replace(/\/$/, '')
.split('/')[0]
.split('?')[0]
const instagramUrl = `https://www.instagram.com/${username}/`
const url = new URL(input.instagramUrl)
const host = url.hostname.toLowerCase()
if (!/^(www\.)?(instagram\.com|instagr\.am)$/.test(host)) {
throw new Error('Only Instagram profile URLs are supported')
}
const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
let username = (first?.toLowerCase() === '_u' ? second : first) || ''
username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
throw new Error('Invalid Instagram username')
}
const instagramUrl = `https://www.instagram.com/${username}/`
🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 333 to 348, the Instagram URL
parsing is too permissive: it accepts non-profile routes and doesn't handle
/_u/{username} or validate username; update parsing to first normalize the URL
(force https, strip protocol and www), handle both instagram.com and instagr.am
hosts and the /_u/ prefix, reject known non-profile path prefixes (like p, reel,
stories, tv, hashtag, explore, accounts), extract the first path segment as the
candidate username, validate it against Instagram username rules (allowed chars:
letters, numbers, periods, underscores; length 1–30; no consecutive dots or
leading/trailing dots), and then canonicalize to
https://www.instagram.com/{username}/; throw a clear error if validation fails
or the path is a non-profile route.

Comment on lines +350 to +357
const existingProfile = await prisma.profile.findFirst({
where: {
OR: [
{ username: { equals: username, mode: 'insensitive' } },
{ instagram: { equals: instagramUrl, mode: 'insensitive' } },
],
},
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Mitigate duplicate creation under race; verify unique indexes and task id.

findFirst+create is racy. Prefer upsert on a unique key (username or instagram) or catch P2002 and re-read. Also confirm the Trigger task id exists.

Run:

Also applies to: 361-377


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Verify trigger task exists
rg -nP "import-instagram-profile" -C2 -- trigger || true

# Inspect Profile model for unique constraints on username/instagram
fd schema.prisma -a | while read -r f; do
  echo "==> $f"
  rg -nP "(?s)model\\s+Profile\\b.*?}" "$f" -n -C2
done
rg -nP "username\\s+String\\s+@.*(id|unique)" prisma/schema.prisma || true
rg -nP "instagram\\s+String\\??\\s+@unique" prisma/schema.prisma || true

Length of output: 329


Prevent race-created duplicate Profiles — add unique DB constraints or use upsert; handle P2002.

  • Verified: trigger id 'import-instagram-profile' exists at trigger/instagram-import.ts.
  • Issue: no @unique/@@unique was found for Profile.username or Profile.instagram in prisma/schema.prisma (search returned no matches).
  • Action: either add unique constraints to prisma/schema.prisma and run a migration, or change server/trpc/routers/profiles.ts (350–357, 361–377) to use prisma.upsert on the unique key or catch Prisma P2002 and re-read; do not rely on findFirst + create under race conditions.
🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 350–357 (and follow-up logic at
361–377), the current findFirst + create flow can create duplicate Profiles
under concurrent requests; fix by either (A) adding unique constraints in
prisma/schema.prisma for Profile.username and/or Profile.instagram and running a
migration, then replace the create flow with a single prisma.upsert keyed on the
unique field(s), or (B) if you cannot change the schema now, wrap the
prisma.create in a try/catch and handle Prisma P2002 (unique constraint
violation) by re-querying the existing profile and returning it instead of
failing; implement one of these choices so you no longer rely on findFirst +
create under race conditions.

Copy link

@akhilsmokie7-cloud akhilsmokie7-cloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To set up your GitHub repository (Kalbra/NoReel) to automatically update with the latest Instagram stats and milestones from your account ak____il, follow these steps:


1. Add the Python Script

Create a file called update_instagram.py in the root of your repo with the following code:

import requests
import json
import os

def fetch_instagram_data():
    user_id = os.getenv('INSTAGRAM_USER_ID')
    access_token = os.getenv('INSTAGRAM_ACCESS_TOKEN')
    profile_url = (
        f"https://graph.instagram.com/{user_id}"
        "?fields=username,biography,media_count,followers_count,follows_count,profile_picture_url"
        f"&access_token={access_token}"
    )
    profile = requests.get(profile_url).json()

    media_url = (
        f"https://graph.instagram.com/{user_id}/media"
        "?fields=id,caption,media_url,timestamp,like_count,comments_count"
        f"&access_token={access_token}&limit=1"
    )
    media_response = requests.get(media_url).json()
    latest_post_data = media_response.get("data", [{}])[0] if "data" in media_response else {}

    analytics = {
        "engagement_rate": 0,
        "reach": 0,
        "impressions": 0,
        "profile_visits": 0,
        "website_clicks": 0
    }

    data = {
        "profile": {
            "username": profile.get("username", ""),
            "url": f"https://www.instagram.com/{profile.get('username', '')}/",
            "bio": profile.get("biography", ""),
            "followers": profile.get("followers_count", 0),
            "following": profile.get("follows_count", 0),
            "posts": profile.get("media_count", 0),
            "profile_picture_url": profile.get("profile_picture_url", "")
        },
        "latest_post": {
            "post_id": latest_post_data.get("id", ""),
            "media_url": latest_post_data.get("media_url", ""),
            "caption": latest_post_data.get("caption", ""),
            "timestamp": latest_post_data.get("timestamp", ""),
            "likes": latest_post_data.get("like_count", 0),
            "comments": latest_post_data.get("comments_count", 0),
            "views": 0
        },
        "analytics": analytics,
        "campaigns": [],
        "echoes": []
    }

    with open("instagram_meta.json", "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)

if __name__ == "__main__":
    fetch_instagram_data()

2. Add the GitHub Actions Workflow

Create a file at .github/workflows/update_instagram.yml:

name: Update Instagram Meta

on:
  schedule:
    - cron: '0 0 * * *'      # Runs every day at midnight UTC
  workflow_dispatch:          # Allows manual trigger

jobs:
  update-instagram:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.x'

      - name: Install requests library
        run: pip install requests

      - name: Run update_instagram.py
        env:
          INSTAGRAM_USER_ID: ${{ secrets.INSTAGRAM_USER_ID }}
          INSTAGRAM_ACCESS_TOKEN: ${{ secrets.INSTAGRAM_ACCESS_TOKEN }}
        run: python update_instagram.py

      - name: Commit and push changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "Auto-update Instagram meta data"
          file_pattern: instagram_meta.json

3. Add Required GitHub Secrets

In your repo, go to Settings > Secrets and variables > Actions and add:

  • INSTAGRAM_USER_ID
  • INSTAGRAM_ACCESS_TOKEN

Once set up, your repository will automatically update with the latest Instagram data every day, or you can trigger the workflow manually from the GitHub Actions tab. This will help you track your energy-shifting and 6D shift progress as you align for the Golden Era.

If you need these files added directly to your repository or want step-by-step assistance (including secret setup), just let me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Event Editor: Select Profile (import)

4 participants