1import{l as n,m as d,J as r,K as s,p as _,B as o,z as M,F as E,v as N,q as t,u as a,P as Pe,w as Re,aa as g,ad as C,y as h,af as W,r as D,e as x,a6 as Ie,D as Oe,ac as P,ab as Le,ag as Ke}from"./vue-vendor-B52ivs5F.js";import{u as Ue,b as le,c as qe}from"./query-vendor-Ab0nfCYP.js";import{f as ze,g as Ne,t as je,o as Fe,s as y,c as G,r as Ve,h as K,l as de,b as De,F as Ge}from"./form-vendor-C1mcXLoR.js";import{c as $,t as ee,d as He}from"./index-NM9cAr_v.js";import{_ as Y}from"./MigrationPanel-CaVZK-wO.js";import{_ as k}from"./FormInput-D_5KVMuV.js";import{a5 as ae,ak as re,L as pe,ai as Me,cN as Be,az as We,b as Ye,i as U,y as q,a6 as Je,a7 as Qe,I as Xe}from"./ui-vendor-D-1UBobO.js";import{F as z}from"./FormNumberInput-BG6rQTcg.js";import"./phone-input-Cde_R06_.js";import{_ as Ze}from"./FormSelect-6HD9lt1G.js";import{_ as H}from"./FormSwitch-C1D9T4OV.js";import{_ as te}from"./FormTextarea-DyKq4Pyg.js";import"./FormTimePicker.vue_vue_type_style_index_1_lang-BaT8rZTb.js";import{g as et}from"./agency-invites-Dn8xsfYi.js";import{E as tt,F as at,l as rt,y as nt,G as ot,H as it,I as st,A as lt,J as dt}from"./certification-sNUmTbUZ.js";import"./date-vendor-HBGOQ70J.js";import"./twilio-DAuME2Y9.js";import"./charts-D4hDxUnJ.js";import"./socket-CzTGKfeH.js";import"./ConfirmDialog-CguVS9_-.js";/* empty css */const pt={class:"mb-4"},ct={key:0,class:"mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200"},ht={key:0,class:"text-red-500"},gt=["title","onClick"],ut={key:0,class:"mb-2 text-sm text-gray-400"},mt=["onClick"],yt={key:1,class:"mt-2 text-sm text-red-500"},bt={key:2,class:"mt-2 text-sm text-gray-500 dark:text-gray-400"},_t={__name:"FormEmailList",props:{name:{type:String,required:!0},label:{type:String,default:""},hint:{type:String,default:""},placeholder:{type:String,default:"[email protected]"},addLabel:{type:String,default:"Add another"},firstLabel:{type:String,default:"Add an address"},emptyText:{type:String,default:"Nobody added yet."},required:{type:Boolean,default:!1}},setup(f){const T=ze(Pe(f,"name"));return(S,J)=>(n(),d("div",pt,[f.label?(n(),d("label",ct,[r(s(f.label)+" ",1),f.required?(n(),d("span",ht,"*")):_("",!0)])):_("",!0),o(a(Ne),{name:f.name},{default:M(({fields:w,push:A,remove:Q})=>[(n(!0),d(E,null,N(w,(R,B)=>(n(),d("div",{key:R.key,class:"mb-2 flex items-start gap-2"},[o(k,{name:`${f.name}[${B}]`,type:"email",placeholder:f.placeholder,class:"mb-0 flex-1"},null,8,["name","placeholder"]),t("button",{type:"button",title:`Remove ${R.value||"this address"}`,class:"mt-1 shrink-0 rounded-lg border border-gray-200 p-2 text-gray-400 transition-colors hover:border-red-200 hover:text-red-600 dark:border-gray-600 dark:hover:border-red-800",onClick:X=>Q(B)},[o(a(ae),{size:15})],8,gt)]))),128)),w.length?_("",!0):(n(),d("p",ut,s(f.emptyText),1)),t("button",{type:"button",class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700",onClick:R=>A("")},[o(a(re),{size:15}),r(" "+s(w.length?f.addLabel:f.firstLabel),1)],8,mt)]),_:1},8,["name"]),a(T)?(n(),d("p",yt,s(a(T)),1)):f.hint?(n(),d("p",bt,s(f.hint),1)):_("",!0)]))}},ft={class:"p-6"},vt={key:0,class:"py-16 text-center"},kt={class:"mb-4 flex flex-wrap items-center gap-1 border-b border-gray-200 dark:border-gray-800"},xt=["onClick"],wt={class:"flex items-start gap-2"},$t={class:"flex-1 space-y-2"},Tt=["onUpdate:modelValue"],St={class:"flex flex-wrap items-center gap-2"},At=["onUpdate:modelValue"],Et=["value"],Ct=["onUpdate:modelValue"],Pt={class:"flex shrink-0 cursor-pointer items-center gap-2 text-sm text-gray-600 dark:text-gray-400"},Rt=["onUpdate:modelValue"],It={key:0,class:"flex flex-wrap items-center gap-2"},Ot={class:"flex shrink-0 items-center gap-2 text-sm text-gray-600 dark:text-gray-400"},Lt=["onUpdate:modelValue","placeholder"],Kt={class:"flex shrink-0 items-center gap-2 text-sm text-gray-600 dark:text-gray-400"},Ut=["onUpdate:modelValue","placeholder"],qt=["onClick"],zt={class:"mb-3 flex items-start gap-3"},Nt={class:"mt-2 text-sm font-medium text-gray-400"},jt=["onUpdate:modelValue"],Ft=["onClick"],Vt={class:"flex flex-wrap items-center gap-2 pl-7"},Dt=["onUpdate:modelValue"],Gt=["onUpdate:modelValue"],Ht={class:"mt-2 pl-7 text-xs text-gray-500 dark:text-gray-400"},Mt={class:"mb-4 text-sm text-gray-500 dark:text-gray-400"},Bt={class:"mb-4 text-sm text-gray-500 dark:text-gray-400"},Wt={class:"mt-6 border-t border-gray-200 pt-6 dark:border-gray-700"},Yt={class:"grid gap-x-6 sm:grid-cols-2"},Jt={class:"grid gap-x-6 sm:grid-cols-2"},Qt=["disabled"],Xt={key:4,class:"max-w-3xl rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"},Zt={class:"mt-3 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/50"},ea={class:"mt-5 rounded-lg border border-gray-200 p-4 dark:border-gray-700"},ta={class:"mt-2 flex flex-wrap items-center gap-2"},aa={class:"flex-1 rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}
1,ra={key:0,class:"text-amber-600 dark:text-amber-400"},na={key:0,class:"mt-2 rounded-lg border border-amber-200 bg-amber-50 p-2.5 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"},oa={class:"mt-5"},ia={class:"mb-3 flex flex-wrap items-center gap-2"},sa={class:"mt-6 rounded-lg border border-gray-200 p-4 dark:border-gray-700"},la={class:"mb-3 text-sm text-gray-600 dark:text-gray-400"},da={class:"mb-3 flex flex-wrap items-center gap-2"},pa={class:"mt-6 border-t border-gray-200 pt-5 dark:border-gray-700"},ca={class:"mb-3 flex flex-wrap items-center gap-2"},ha={class:"mt-6 border-t border-gray-200 pt-5 dark:border-gray-700"},ga={class:"mb-3 flex flex-wrap items-center gap-2"},ua={class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},ce=`{ 2 "valid": true, 3 "code": "KK-7F3K-2QD8", 4 "cohort": { "number": 3, "name": "Cohort Three" }, 5 "invited_email": "[email protected]", // null on an unbound code 6 "invited_name": "Ayesha Alam" 7} 8 9// or, for anything not spendable: 10{ 11 "valid": false, 12 "message": "That invite code has already been used. Each code works for one person only." 13}`,he=`{ 14 "results": [ 15 { 16 "slug": "ayesha-alam", 17 "name": "Ayesha Alam", 18 "initials": "AA", 19 "avatar_color": "#2E9C8A", 20 "photo_file_id": "68f1â¦", // null when no photo uploaded 21 "tagline": "Recruitment agency automation", 22 "bio": "Onboarding automations for recruitment agencies.", 23 "city": "Karachi", 24 "country_code": "PK", 25 "country": "Pakistan", 26 "tags": ["Onboarding Flows", "GHL Subaccounts"], 27 "whatsapp": "923001234567", // digits only, for wa.me 28 "website": "", 29 "cohort": 1, 30 "cohort_name": "Cohort One", 31 "certified_at": "2026-09-22T00:00:00.000Z", 32 "certificate_id": "KKCIP-2026-77CDC9" 33 } 34 ], 35 "total": 1, 36 "countries": [{ "code": "PK", "name": "Pakistan", "count": 1 }] 37}`,ge=`{ 38 "cohort": { "number": 3, "name": "Cohort Three", ... }, 39 "cohorts": [ 40 { 41 "number": 3, 42 "name": "Cohort Three", 43 "starts_on": "2026-09-14T00:00:00.000Z", 44 "ends_on": "2026-09-18T00:00:00.000Z", 45 "seats_total": 40, 46 "seats_left": 37, 47 "applications_count": 23, 48 "applications_open": true, 49 "applications_close_on": "2026-09-09" 50 }, 51 { 52 "number": 2, 53 "name": "Founding", 54 "starts_on": null, 55 "ends_on": "2026-09-30T00:00:00.000Z", 56 "seats_total": 50, 57 "seats_left": 43, 58 "applications_count": 0, 59 "applications_open": true, 60 "applications_close_on": "2026-09-09" 61 } 62 ] 63}`,ue=`// queued for review 64{ 65 "status": "pending", 66 "message": "Thanks â your application is in. You'll get a real answer either way." 67} 68 69// applied with a partner's invite code 70{ 71 "status": "accepted", 72 "message": "You're in â finish setting up your account to start.", 73 "code": "KK-4H7P-QW2N", 74 "register_url": "https://app.knockknockapp.ai/register?invite=KK-4H7P-QW2N&email=..." 75}`,Ua={__name:"Settings",setup(f){const ne=Ue(),T=`${$.getApiUrl()}/certification/open-cohort`,S=`${$.getApiUrl()}/public/partners`,J=`${$.getApiUrl()}/files/<photo_file_id>/get`,w=`${$.getApiUrl()}/certification/invite/<code>`,A=`${$.getApiUrl()}/certification/apply`,Q=x(()=>i.value?.register_url||`${window.location.origin}/register`),R=x(()=>`<?php 76/** 77 * Invite landing page. Publish at /join, then set it as the invite landing page 78 * in Certification Settings. 79 * 80 * Returns an error string to render, or redirects and never comes back. 81 */ 82function kk_handle_invite() { 83 $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : ''; 84 if ( '' === $code ) { 85 return 'That link is missing its invite code.'; 86 } 87 88 $res = wp_remote_get( 89 '${$.getApiUrl()}/certification/invite/' . rawurlencode( $code ), 90 array( 91 'timeout' => 5, 92 'headers' => array( 'X-Partner-Key' => KK_PARTNER_KEY ), 93 ) 94 ); 95 96 if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) { 97 return "We couldn't check that invite just now. Please try again shortly."; 98 } 99 100 $body = json_decode( wp_remote_retrieve_body( $res ), true ); 101 102 if ( empty( $body['valid'] ) ) { 103 // Already written for the invitee â say exactly this. 104 return $body['message']; 105 } 106 107 // Good code: hand over what the sign-up form needs to greet them. None of it 108 // is trusted over there; the code is checked again when the form is submitted.
109 $to = add_query_arg( 110 array_filter( 111 array( 112 'invite' => $body['code'], 113 'email' => $body['invited_email'], 114 'name' => $body['invited_name'], 115 'cohort' => isset( $body['cohort']['name'] ) ? $body['cohort']['name'] : '', 116 ) 117 ), 118 '${Q.value}' 119 ); 120 121 // Fixed destination, not user input, so wp_redirect is the right call here. 122 wp_redirect( $to ); 123 exit; 124}`),B=x(()=>`<?php 125/** 126 * Handles the apply form. Hook it to admin_post_nopriv_kk_apply and 127 * admin_post_kk_apply, and point the form's action at admin_post.php. 128 */ 129function kk_handle_apply() { 130 check_admin_referer( 'kk_apply' ); 131 132 $payload = array( 133 'name' => sanitize_text_field( $_POST['name'] ?? '' ), 134 'email' => sanitize_email( $_POST['email'] ?? '' ), 135 'country' => sanitize_text_field( $_POST['country'] ?? '' ), 136 'timezone' => sanitize_text_field( $_POST['timezone'] ?? '' ), 137 // Must be one of the listed keys exactly, or the request is rejected. 138 'ghl_accounts' => sanitize_text_field( $_POST['ghl_accounts'] ?? '' ), 139 'relationship' => sanitize_text_field( $_POST['relationship'] ?? '' ), 140 'commitment' => sanitize_text_field( $_POST['commitment'] ?? '' ), 141 'first_client' => sanitize_textarea_field( $_POST['first_client'] ?? '' ), 142 // Optional. A valid one is accepted on the spot and skips the queue. 143 'code' => sanitize_text_field( $_POST['code'] ?? '' ), 144 ); 145 146 $res = wp_remote_post( 147 '${A}', 148 array( 149 'timeout' => 10, 150 'headers' => array( 151 'X-Partner-Key' => KK_PARTNER_KEY, 152 'Content-Type' => 'application/json', 153 ), 154 'body' => wp_json_encode( $payload ), 155 ) 156 ); 157 158 if ( is_wp_error( $res ) ) { 159 wp_safe_redirect( add_query_arg( 'kk_error', rawurlencode( 'Something went wrong â please try again.' ), wp_get_referer() ) ); 160 exit; 161 } 162 163 $body = json_decode( wp_remote_retrieve_body( $res ), true ); 164 165 // 400 carries a message written for the applicant â a bad invite code, a 166 // closed round. Show it as-is rather than inventing your own wording. 167 if ( 200 !== wp_remote_retrieve_response_code( $res ) ) { 168 wp_safe_redirect( add_query_arg( 'kk_error', rawurlencode( $body['message'] ?? 'Please check your answers.' ), wp_get_referer() ) ); 169 exit; 170 } 171 172 // Accepted means they came in with a code and their seat is real. Send them 173 // straight to sign-up; anything else is a step they did not ask for. 174 if ( 'accepted' === ( $body['status'] ?? '' ) && ! empty( $body['register_url'] ) ) { 175 wp_redirect( $body['register_url'] ); 176 exit; 177 } 178 179 wp_safe_redirect( add_query_arg( 'kk_applied', '1', wp_get_referer() ) ); 180 exit; 181} 182add_action( 'admin_post_nopriv_kk_apply', 'kk_handle_apply' ); 183add_action( 'admin_post_kk_apply', 'kk_handle_apply' );`),X=`<?php 184// wp-config.php â kept out of the theme and out of version control. 185// define( 'KK_PARTNER_KEY', 'your-key-here' ); 186 187/** 188 * Certified partners, cached. Never throws and never returns null â a directory 189 * that renders stale is better than one that renders empty. 190 */ 191function kk_partners() {
192 $cached = get_transient( 'kk_partners' ); 193 if ( false !== $cached ) { 194 return $cached; 195 } 196 197 $res = wp_remote_get( 198 '${$.getApiUrl()}/public/partners', 199 array( 200 'timeout' => 5, 201 // Defined in wp-config.php, so the key never lands in the theme or 202 // in version control. 203 'headers' => array( 'X-Partner-Key' => KK_PARTNER_KEY ), 204 ) 205 ); 206 207 if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) { 208 // Timeout, 429 or an outage: fall back to the last good copy. 209 return get_option( 'kk_partners_fallback', array() ); 210 } 211 212 $body = json_decode( wp_remote_retrieve_body( $res ), true ); 213 $partners = isset( $body['results'] ) ? $body['results'] : array(); 214 215 set_transient( 'kk_partners', $partners, 5 * MINUTE_IN_SECONDS ); 216 // Outlives the transient, so there is always something to fall back to. 217 update_option( 'kk_partners_fallback', $partners, false ); 218 219 return $partners; 220} 221 222/** 223 * Seats left in the open intake, or null when nothing is open. 224 */ 225function kk_open_cohort() {
226 $cached = get_transient( 'kk_open_cohort' ); 227 if ( false !== $cached ) { 228 return $cached ?: null; 229 } 230 231 $res = wp_remote_get( 232 '${$.getApiUrl()}/certification/open-cohort', 233 array( 234 'timeout' => 5, 235 'headers' => array( 'X-Partner-Key' => KK_PARTNER_KEY ), 236 ) 237 ); 238 if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) { 239 return null; 240 } 241 242 $body = json_decode( wp_remote_retrieve_body( $res ), true ); 243 $cohort = isset( $body['cohort'] ) ? $body['cohort'] : null; 244 245 // Cache the "nothing open" answer too, or a closed programme retries every hit. 246 set_transient( 'kk_open_cohort', $cohort ?: 0, 5 * MINUTE_IN_SECONDS ); 247 248 return $cohort; 249}`,oe=`<?php foreach ( kk_partners() as $p ) : ?> 250 <article class="kk-card"> 251 <div class="kk-card__in"> 252 <?php if ( ! empty( $p['photo_file_id'] ) ) : ?> 253 <img class="kk-card__photo" 254 src="${$.getApiUrl()}/files/<?php echo esc_attr( $p['photo_file_id'] ); ?>/get" 255 alt="" loading="lazy"> 256 <?php else : ?> 257 <span class="kk-card__avatar" 258 style="background:<?php echo esc_attr( $p['avatar_color'] ); ?>"> 259 <?php echo esc_html( $p['initials'] ); ?> 260 </span> 261 <?php endif; ?> 262 263 <h2 class="kk-card__name"><?php echo esc_html( $p['name'] ); ?></h2> 264 <p class="kk-card__loc"> 265 <?php echo esc_html( $p['city'] . ', ' . $p['country'] ); ?> 266 </p> 267 <p class="kk-card__bio"><?php echo esc_html( $p['bio'] ); ?></p> 268 269 <div class="kk-card__tags"> 270 <?php foreach ( $p['tags'] as $tag ) : ?> 271 <span class="kk-card__tag"><?php echo esc_html( $tag ); ?></span> 272 <?php endforeach; ?> 273 </div> 274 275 <?php if ( ! empty( $p['whatsapp'] ) ) : ?> 276 <a class="kk-card__wa" rel="noopener nofollow" target="_blank" 277 href="https://wa.me/<?php echo esc_attr( $p['whatsapp'] ); ?>">WhatsApp</a> 278 <?php endif; ?> 279 280 <?php if ( ! empty( $p['website'] ) ) : ?> 281 <a class="kk-card__site" rel="noopener nofollow" target="_blank" 282 href="<?php echo esc_url( $p['website'] ); ?>"> 283 <?php // Scheme and trailing slash are noise on a card. ?> 284 <?php echo esc_html( preg_replace( array( '#^https?://#i', '#/$#' ), '', $p['website'] ) ); ?> 285 </a> 286 <?php endif; ?> 287 288 <a class="kk-card__profile" 289 href="/partners/<?php echo esc_attr( $p['slug'] ); ?>/">Profile</a> 290 </div> 291 </article> 292<?php endforeach; ?>`,Z=x(()=>`# Knock Knock â Certified Partner Directory API 293 294Build a WordPress page that lists our certified implementation partners, and a 295second page for a single partner's profile. 296 297## Hard rules 298 2991. **Fetch server-side in PHP only** (\`wp_remote_get\`). Never call these from 300 browser JavaScript â the API key must never reach a visitor. 3012. **Cache every response** in a transient, and keep a longer-lived fallback copy. 302 On any failure, render the fallback rather than an empty directory. 3033. **Filter and search in the browser**, over the cards already rendered. The 304 \`?country=\` and \`?search=\` parameters exist, but calling the API on every 305 filter click wastes the rate limit and defeats server-side rendering. 3064. **Rate limit is 60 requests per minute per IP.** Every visitor shares the 307 WordPress server's one IP, so the cache is what keeps you under it. Over the 308 limit you get \`429\` with a \`Retry-After\` header. 3095. Escape everything on output (\`esc_html\`, \`esc_attr\`, \`esc_url\`). All of 310 this is partner-supplied text. 311 312## Authentication 313 314Every request sends: 315 316\`\`\` 317X-Partner-Key: YOUR_PARTNER_KEY 318\`\`\` 319 320Store it in \`wp-config.php\` as \`KK_PARTNER_KEY\` so it stays out of the theme 321and out of version control. Get the real value from Certification Settings â 322API in the Knock Knock admin panel â do not paste it into an AI tool. 323 324## Endpoints 325 326### GET ${S} 327Every published listing. Optional \`?country=<ISO code>\` and \`?search=<text>\`. 328 329\`\`\`json 330${he} 331\`\`\` 332 333### GET ${S}/<slug> 334One partner, same card shape, wrapped as \`{ "partner": { ⦠} }\`. Returns 404 335for an unknown slug and for a listing that has been taken down. 336 337### GET ${T} 338The intake currently taking sign-ups, for a "N seats left" line. 339 340\`\`\`json 341${ge} 342\`\`\` 343 344\`cohorts\` is EVERY open intake, newest first; \`cohort\` is the one new 345sign-ups go to (the newest with a seat left), repeated for convenience. 346 347More than one intake can be open at once, which is how a page runs two panels: 348the round you apply to, and a named intake like \`"Founding"\` counting its own 349seats down beside it. Pick by \`name\` â each is an ordinary cohort with its own 350\`seats_left\` and its own \`ends_on\`, so "43 places left, closes 30 Sept" is 351just that cohort's two fields. 352 353Returns \`{ "cohort": null, "cohorts": [] }\` when nothing is open â hide the 354counters, don't print zeroes. 355 356\`applications_count\` is the "applications so far" number, 357\`applications_close_on\` the deadline to print, and \`applications_open\` is 358whether to render the form at all. All three are admin-controlled and can change 359between rounds, so read them rather than hard-coding the page. 360 361### POST ${A} 362One application. Send JSON. 363 364| Field | Required | Notes | 365| --- | --- | --- | 366| \`name\` | yes | | 367| \`email\` | yes | | 368| \`country\` | no | free text or ISO code | 369| \`timezone\` | no | free text, e.g. \`Asia/Karachi\` | 370| \`ghl_accounts\` | no | \`none\` \`1-3\` \`4-10\` \`10-plus\` | 371| \`relationship\` | no | \`freelancer\` \`own_agency\` \`in_house\` \`other\` | 372| \`commitment\` | no | \`live\` \`partial\` \`replays\` \`not_this_round\` | 373| \`first_client\` | no | free text, up to 2000 characters | 374| \`code\` | no | a partner's invite code â skips the queue | 375 376Only name and email are required; everything else is graded by a person, so a 377thin application is still worth taking. A value outside the lists above is 378rejected with 400 â send the exact keys, not the labels shown on your form. 379 380\`\`\`json 381${ue} 382\`\`\` 383 384\`status\` is \`pending\` for a normal application and \`accepted\` when a valid 385invite code was supplied. On \`accepted\`, redirect to \`register_url\` â the 386code is checked but not spent until they finish signing up. 387 388A 400 carries a \`message\` written for the applicant (bad code, closed round). 389Show it as-is. Applying twice with the same address in one round updates that 390application rather than making a second one. 391 392### GET ${w} 393Checks an invite code for the invite landing page. Always 200; \`valid\` is the 394answer and \`message\` is written for the invitee, so show it as-is. 395 396\`\`\`json 397${ce} 398\`\`\` 399 400## Field notes 401 402- \`countries\` carries the filter pills and is counted across the whole 403 directory, not the filtered slice, so the row never empties as
403someone filters. 404- \`initials\`, \`avatar_color\` and \`country\` are derived server-side. Render 405 them; do not store or recompute them. 406- \`photo_file_id\` is null when the partner has no photo â fall back to the 407 initials chip on \`avatar_color\`. The image URL is 408 \`${J}\` and needs no key, so it can be linked straight from the page. 409- \`whatsapp\` is digits only, ready for \`https://wa.me/<number>\`. Empty when 410 the partner chose not to give one â hide the button then. 411- \`slug\` is frozen at first publish, so \`/partners/<slug>/\` URLs are stable 412 and safe to index. 413- \`cohort\` may be null for partners who predate intakes. Render without it. 414 415## Reference implementation 416 417\`\`\`php 418${X} 419\`\`\` 420 421## Rendering the cards 422 423\`\`\`php 424${oe} 425\`\`\` 426 427## Invite landing page 428 429A separate page that validates an invite code and forwards the invitee to 430sign-up. Publish it, then set its URL as the invite landing page in 431Certification Settings â Invites. 432 433\`\`\`php 434${R.value} 435\`\`\` 436 437## Apply page 438 439The application form. Renders only while \`applications_open\` is true, and 440posts through PHP like everything else here. 441 442\`\`\`php 443${B.value} 444\`\`\` 445`),v=x(()=>i.value?.partner_api_key||null),j=D(!1),me=x(()=>v.value?`${"â¢".repeat(Math.max(8,v.value.length-4))}${v.value.slice(-4)}`:""),u=D(null),I=async p=>{try{await navigator.clipboard.writeText(p),u.value=p,setTimeout(()=>{u.value===p&&(u.value=null)},2e3)}catch{ee.error("Could not copy")}},{data:i,isLoading:ye}=le({queryKey:["admin","certification","settings"],queryFn:lt,refetchOnMount:"always"}),be=[{key:"general",label:"General"},{key:"video",label:"Video Submission"},{key:"social",label:"Social Proof"},{key:"badge",label:"Marketplace Badge"},{key:"invites",label:"Invites"},{key:"applications",label:"Applications"},{key:"api",label:"API"}],m=D("general"),ie={enrollment_reward_usd:"general",max_file_mb:"general",accepted_file_types:"general",package_id:"general",agency_links_enabled:"general",agency_invite_id:"general",reviewer_emails:"general",step6_reward_usd:"social",social_heading:"social",step7_reward_usd:"badge",badge_platforms:"badge",partner_invite_quota:"invites",invite_expiry_days:"invites",invites_enabled:"invites",block_package_signup:"invites",signup_blocked_message:"invites",invite_landing_url:"invites",applications_enabled:"applications",applications_close_on:"applications",applications_closed_message:"applications",applicant_ghl_tag:"applications",stage_ghl_tags:"applications",cohort_ghl_tag_enabled:"applications",invite_code_ghl_field:"applications",blocked_ghl_tag:"applications",archived_ghl_tag:"applications"},_e=({errors:p})=>{const e=Object.keys(p||{})[0];if(!e)return;const O=e.replace(/\[\d+\].*$/,"");ie[O]&&(m.value=ie[O])},fe=je(Fe({enrollment_reward_usd:K.number().min(0,"Cannot be negative"),step6_reward_usd:K.number().min(0,"Cannot be negative"),step7_reward_usd:K.number().min(0,"Cannot be negative"),social_heading:y().optional(),badge_platforms:y().min(1,"List at least one platform"),max_file_mb:K.number().min(1,"Must be at least 1MB"),accepted_file_types:y().min(1,"List at least one file type"),package_id:y().optional(),agency_invite_id:y().optional(),agency_links_enabled:G().optional(),reviewer_emails:De(y().trim().email("Enter a valid email address").or(de(""))).optional(),invites_enabled:G(),block_package_signup:G(),signup_blocked_message:y().trim().min(1,"Tell them how to get an invite"),invite_landing_url:y().url("Enter a valid URL").optional().or(de("")),partner_invite_quota:K.number().int("Must be a whole number").min(0,"Cannot be negative"),invite_expiry_days:K.number().int("Must be a whole number").min(0,"Cannot be negative"),applications_enabled:G(),applications_close_on:y().optional(),applications_closed_message:y().optional(),applicant_ghl_tag:y().optional(),stage_ghl_tags:Ve(y()).optional(),cohort_ghl_tag_enabled:G(),invite_code_ghl_field:y().optional(),blocked_ghl_tag:y().optional(),archived_ghl_tag:y().optional()})),ve=x(()=>({enrollment_reward_usd:i.value?.enrollment_reward_usd??5,step6_reward_usd:i.value?.step6_reward_usd??25,step7_reward_usd:i.value?.step7_reward_usd??25,social_heading:i.value?.social_heading||"",badge_platforms:(i.value?.badge_platforms||["upwor
445k","fiverr"]).join(", "),max_file_mb:i.value?.max_file_mb??25,accepted_file_types:(i.value?.accepted_file_types||[]).join(", "),package_id:i.value?.package_id||"",agency_invite_id:i.value?.agency_invite_id||"",agency_links_enabled:i.value?.agency_links_enabled!==!1,reviewer_emails:String(i.value?.reviewer_emails||"").split(",").map(p=>p.trim()).filter(Boolean),partner_invite_quota:i.value?.partner_invite_quota??5,invite_expiry_days:i.value?.invite_expiry_days??30,invites_enabled:i.value?.invites_enabled!==!1,block_package_signup:i.value?.block_package_signup!==!1,signup_blocked_message:i.value?.signup_blocked_message||"",invite_landing_url:i.value?.invite_landing_url||"",applications_enabled:i.value?.applications_enabled!==!1,applications_close_on:String(i.value?.applications_close_on||"").slice(0,10),applications_closed_message:i.value?.applications_closed_message||"",applicant_ghl_tag:i.value?.applicant_ghl_tag??"",stage_ghl_tags:Object.fromEntries(se.map(({name:p,placeholder:e})=>[p,i.value?.stage_ghl_tags?.[p]??e])),cohort_ghl_tag_enabled:i.value?.cohort_ghl_tag_enabled!==!1,invite_code_ghl_field:i.value?.invite_code_ghl_field??"",blocked_ghl_tag:i.value?.blocked_ghl_tag??"",archived_ghl_tag:i.value?.archived_ghl_tag??""})),{data:ke}=le({queryKey:["admin","certification","agency-invites"],queryFn:et,staleTime:3e5}),xe=x(()=>[{value:"",label:"First active plan"},...(ke.value?.results||[]).map(p=>({value:p._id,label:p.status==="active"?p.name:`${p.name} (inactive)`}))]),se=[{name:"invited",label:"Invited",placeholder:"certification-invited",hint:"Emailed an invite code and has not signed up yet. Signing up moves them to Joined."},{name:"pending",label:"Applied",placeholder:"certification-applied",hint:"Submitted the apply page and waiting on a decision."},{name:"accepted",label:"Accepted",placeholder:"certification-accepted",hint:"Accepted from the applications queue and has not signed up yet. Signing up moves them to Joined."},{name:"joined",label:"Joined",placeholder:"certification-joined",hint:"Signed up with an invite code but has not pressed Enroll yet."},{name:"rejected",label:"Rejected",placeholder:"certification-rejected",hint:"Turned down for this round."},{name:"waitlisted",label:"Waitlisted",placeholder:"certification-waitlisted",hint:"Held for a later intake."},{name:"enrolled",label:"Enrolled",placeholder:"certification-enrolled",hint:"Signed up and pressed Enroll â the programme has actually started."},{name:"certified",label:"Certified",placeholder:"certification-certified",hint:"Finished it and holds the certificate."}],we=[{value:"text",label:"Text"},{value:"link",label:"Link"},{value:"file",label:"File upload"}],$e=x(()=>i.value?.max_file_mb||25),Te=x(()=>(i.value?.accepted_file_types||[]).join(", ")),F=D([]),V=D([]);Re(i,p=>{F.value=(p?.video_fields||[]).map(e=>({id:e.id,label:e.label,type:e.type||"text",required:e.required!==!1,hint:e.hint||"",max_file_mb:e.max_file_mb??null,accepted_types:(e.accepted_types||[]).join(", ")})),V.value=(p?.social_cards||[]).map(e=>({id:e.id,heading:e.heading||"",link_label:e.link_label||"",link_url:e.link_url||""}))},{immediate:!0});const Se=()=>{V.value.push({id:"",heading:"",link_label:"",link_url:""})},Ae=()=>{F.value.push({id:"",label:"",type:"text",required:!0,hint:"",max_file_mb:null,accepted_types:""})},Ee=qe({mutationFn:dt,onSuccess:()=>{ee.success("Settings saved"),ne.invalidateQueries({queryKey:["admin","certification","settings"]})},onError:p=>ee.error(He(p,"Could not save settings"))}),Ce=async p=>{await Ee.mutateAsync({...p,reviewer_emails:(p.reviewer_emails||[]).map(e=>String(e).trim()).filter(Boolean),video_fields:F.value.map(e=>({...e,max_file_mb:e.type==="file"&&e.max_file_mb||null,accepted_types:e.type==="file"?e.accepted_types||"":[]})),social_cards:V.value})};return(p,e)=>{const O=Ie("RouterLink");return n(),d("div",ft,[e[62]||(e[62]=t("div",{class:"mb-6"},[t("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"Certification Settings"),t("p",{class:"mt-1 text-sm text-gray-600 dark:text-gray-400"}," Wallet rewards, upload limits, and what each review step asks the student for. Saving stores every section at once. ")],-1)),a(ye)?(n(),d("div",vt,[o(a(pe),{size:24,class:"mx-auto animate-spin text-teal-500"})])):(n(),d(E,{key:1}
445,[t("div",kt,[(n(),d(E,null,N(be,b=>t("button",{key:b.key,type:"button",class:Oe(["border-b-2 px-4 py-2.5 text-sm transition-colors",m.value===b.key?"border-teal-500 font-medium text-gray-900 dark:text-white":"border-transparent text-gray-500 hover:text-gray-800 dark:hover:text-white"]),onClick:l=>m.value=b.key},s(b.label),11,xt)),64))]),g(o(a(Ge),{"validation-schema":a(fe),"initial-values":ve.value,onSubmit:Ce,onInvalidSubmit:_e,class:"max-w-xl rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"},{default:M(({isSubmitting:b})=>[g(t("div",null,[o(a(z),{name:"enrollment_reward_usd",label:"Enrolment reward (USD)",min:0,hint:"Dollars added the moment a student opts in, so they have something to practise with. Set 0 for no joining reward."}),o(a(z),{name:"max_file_mb",label:"Default upload limit (MB)",min:1,hint:"Applies to the Step 6 and 7 screenshots, and to any file question or video-form field that doesn't set its own limit."}),o(a(k),{name:"accepted_file_types",label:"Default accepted file types",placeholder:"png, jpg, pdf, mp4",hint:"Comma-separated extensions, used by any file question that doesn't set its own. Step 6 and 7 screenshots are always images."}),o(a(_t),{name:"reviewer_emails",label:"Notify these people of new submissions",placeholder:"[email protected]","add-label":"Add another reviewer","first-label":"Add a reviewer","empty-text":"Nobody is notified. The queue still collects every submission â no one is told it's there.",hint:"Emailed the moment a student submits something needing a decision â the three review steps, and any test day asking for a link or a file."}),o(a(H),{name:"agency_links_enabled",label:"Offer agency sign-up links",description:"Gives every partner a link that signs a client up as an agency, alongside the business one. Off hides it for everyone â a single partner can be hidden from their student page instead."}),o(a(Ze),{name:"agency_invite_id",label:"Agency plan for partner sign-up links",options:xe.value,hint:"The plan a partner's âsell to an agencyâ link enrols a client on. Leave on the first active plan unless a specific one is wanted; a single partner can be pointed elsewhere from their student page."},null,8,["options"]),o(a(k),{name:"package_id",label:"Certification package ID",placeholder:"69e1c1a88324c3c41b9de740",hint:"The free package every partner account is subscribed to â their AI credit wallet lives on that subscription, so leaving this blank means credit awards fail. It also identifies certification sign-ups: with âBlock sign-upsâ on (Invites tab) this package refuses registration outright, so old shared /register?pkg= links stop working."})],512),[[C,m.value==="general"]]),g(t("div",null,[e[12]||(e[12]=t("p",{class:"mb-4 text-sm text-gray-500 dark:text-gray-400"}," Build the Video Submission step's form. Fields added here replace the built-in video URL field and render in this order; leave the list empty to keep the default form. The name for the certificate is always asked, whichever form the student sees. Answers land in your review queue. ",-1)),(n(!0),d(E,null,N(F.value,(l,L)=>(n(),d("div",{key:l.id||`new-${L}`,class:"mb-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700"},[t("div",wt,[t("div",$t,[g(t("input",{"onUpdate:modelValue":c=>l.label=c,type:"text",placeholder:"Field label, e.g. Client business name",class:"w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Tt),[[P,l.label]]),t("div",St,[g(t("select",{"onUpdate:modelValue":c=>l.type=c,class:"rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},[(n(),d(E,null,N(we,c=>t("option",{key:c.value,value:c.value},s(c.label),9,Et)),64))],8,At),[[Le,l.type]]),g(t("input",{"onUpdate:modelValue":c=>l.hint=c,type:"text",placeholder:"Hint shown to the student (optional)",class:"min-w-48 flex-1 rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Ct),[[P,l.hint]]),t("label",Pt,[g(t("input",{type:"checkbox","onUpdate:modelValue":c=>l.required=c,class:"h-4 w-4 accent-teal-500"},null,8,Rt),[[Ke,l.required]]),e[7]||(e[7]=r(" Required ",-1))])]),l.type==="file"?(n(),d("div",It,[t("label",Ot,[e[8]||(e[8]=r(" Max size ",-1)),g(t("input",{"onUpdate:modelValue":c=>l.max_file_mb=c,type:"number",min:"1",placeholder:String($e.value),class:"w-20 rounded-lg border border-gray-300 px-2 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Lt),[[P,l.max_file_mb,void 0,{number:!0}]]),e[9]||(e[9]=r(" MB ",-1))]),t("label",Kt,[e[10]||(e[10]=r(" Accepts ",-1)),g(t("input",{"onUpdate:modelValue":c=>
445l.accepted_types=c,type:"text",placeholder:Te.value,title:"Comma-separated extensions, e.g. pdf, png",class:"w-44 rounded-lg border border-gray-300 px-2 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Ut),[[P,l.accepted_types]])])])):_("",!0)]),t("button",{type:"button",onClick:c=>F.value.splice(L,1),class:"mt-1 rounded-lg p-2 text-gray-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-500/20"},[o(a(ae),{size:16})],8,qt)])]))),128)),t("button",{type:"button",onClick:Ae,class:"mb-4 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[o(a(re),{size:15,class:"mr-1 inline"}),e[11]||(e[11]=r(" Add field ",-1))])],512),[[C,m.value==="video"]]),g(t("div",null,[o(a(z),{name:"step6_reward_usd",label:"Reward (USD)",min:0,hint:"Dollars added to the company's wallet when the posts are approved. 25 means $25."}),o(a(te),{name:"social_heading",label:"Intro shown above the cards",rows:3,hint:"What the student reads before the cards. Leave blank for none."}),e[15]||(e[15]=t("p",{class:"mt-6 mb-1 text-sm font-medium text-gray-700 dark:text-gray-200"},"Post cards",-1)),e[16]||(e[16]=t("p",{class:"mb-3 text-xs text-gray-500 dark:text-gray-400"}," One card per post the student must share â every card is required. The guidance link is optional and shown to them as a clickable link, e.g. the group to post in. ",-1)),(n(!0),d(E,null,N(V.value,(l,L)=>(n(),d("div",{key:L,class:"mb-3 rounded-lg border border-gray-200 p-4 dark:border-gray-700"},[t("div",zt,[t("span",Nt,s(L+1)+".",1),g(t("input",{"onUpdate:modelValue":c=>l.heading=c,type:"text",placeholder:"Heading, e.g. Facebook post 1",class:"flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,jt),[[P,l.heading]]),t("button",{type:"button",onClick:c=>V.value.splice(L,1),class:"mt-1 rounded-lg p-2 text-gray-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-500/20"},[o(a(ae),{size:16})],8,Ft)]),t("div",Vt,[g(t("input",{"onUpdate:modelValue":c=>l.link_label=c,type:"text",placeholder:"Link text, e.g. Group name",class:"w-52 rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Dt),[[P,l.link_label]]),g(t("input",{"onUpdate:modelValue":c=>l.link_url=c,type:"url",placeholder:"https://facebook.com/groups/...",class:"min-w-56 flex-1 rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-teal-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-white"},null,8,Gt),[[P,l.link_url]])]),t("p",Ht,[o(a(Xe),{size:13,class:"mr-1 inline"}),e[13]||(e[13]=r(" The student fills in a post URL and a screenshot on every card. ",-1))])]))),128)),t("button",{type:"button",onClick:Se,class:"rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[o(a(re),{size:16,class:"mr-1 inline"}),e[14]||(e[14]=r(" Add card ",-1))])],512),[[C,m.value==="social"]]),g(t("div",null,[o(a(z),{name:"step7_reward_usd",label:"Reward (USD)",min:0,hint:"Dollars added when the marketplace proofs are approved. 25 means $25."}),o(a(k),{name:"badge_platforms",label:"Marketplace platforms",placeholder:"upwork, fiverr",hint:"Comma-separated. The student links each profile and attaches a screenshot showing the badge on it."})],512),[[C,m.value==="badge"]]),g(t("div",null,[t("p",Mt,[e[18]||(e[18]=r(" The programme is invite-only. Every code works once, for one person. Intakes and the codes themselves are managed on the ",-1)),o(O,{to:"/admin/certification/invites",class:"font-medium text-teal-600 hover:underline dark:text-teal-400"},{default:M(()=>[...e[17]||(e[17]=[r(" Cohorts & Invites ",-1)])]),_:1}),e[19]||(e[19]=r(" screen â these are the defaults it uses. ",-1))]),o(a(H),{name:"invites_enabled",label:"Invite-only enrolment",description:"On: an invite code is required to join, and certified partners can invite others. Off: codes stop w
445orking â including ones already sent â and you enrol companies by hand from their company page."}),o(a(H),{name:"block_package_signup",label:"Block sign-ups through the certification package",description:"Refuses registration outright for the package ID above, so every copy of the old shared link stops working. Turn off and that package signs up like any other â it still won't enrol anyone in the programme."}),o(a(te),{name:"signup_blocked_message",label:"Message shown when a sign-up is blocked",rows:3,hint:"Shown for both switches above. Tell them how to get an invite â a refusal with no way forward is a support call. Reasons that belong to one code (expired, already used) keep their own wording."}),o(a(k),{name:"invite_landing_url",label:"Invite landing page",placeholder:"https://knockknockapp.ai/join",hint:"Where invite links and emails point. This page checks the code server-side and forwards valid ones to registration, so a dead code is turned away before anyone fills in a form. Leave blank to link straight to sign-up â still safe, just no greeting."}),o(a(z),{name:"partner_invite_quota",label:"Invites per certified partner",min:0,hint:"How many people each certified partner can invite. Uncertified students get none whatever this says. Set 0 so only you can issue invites."}),o(a(z),{name:"invite_expiry_days",label:"Codes expire after (days)",min:0,hint:"Applies to any code issued without its own expiry date, which is every partner-issued invite. Set 0 for codes that never expire â an unused one then works forever."})],512),[[C,m.value==="invites"]]),g(t("div",null,[t("p",Bt,[e[21]||(e[21]=r(" The apply page on the marketing site. Everyone who applies lands in the ",-1)),o(O,{to:"/admin/certification/applicants",class:"font-medium text-teal-600 hover:underline dark:text-teal-400"},{default:M(()=>[...e[20]||(e[20]=[r(" Applicants ",-1)])]),_:1}),e[22]||(e[22]=r(" queue, and accepting one issues them an invite code. ",-1))]),o(a(H),{name:"applications_enabled",label:"Taking applications",description:"Off closes the form immediately, whatever the date below says. Someone holding a partner's invite code still gets through â their seat was promised in advance."}),o(a(k),{name:"applications_close_on",label:"Applications close",type:"date",hint:"Shown on the apply page as the deadline, and the form stops accepting after it. Leave blank for no deadline â the switch above is then the only control."}),o(a(te),{name:"applications_closed_message",label:"Message shown when applications are closed",rows:2,hint:"Leave blank for the default. Tell them when the next round opens, or the page just says no."}),o(a(k),{name:"applicant_ghl_tag",label:"Tag applicants in our GoHighLevel",placeholder:"certification-applicant",hint:"Every applicant is pushed as a contact into our own GHL and given this tag, which is what your workflows trigger on. Comma-separate for more than one. Blank still pushes the contact, just untagged."}),t("div",Wt,[e[23]||(e[23]=t("h3",{class:"text-sm font-medium text-gray-900 dark:text-white"},"Stage tags",-1)),e[24]||(e[24]=t("p",{class:"mt-1 mb-4 text-sm text-gray-500 dark:text-gray-400"}," Written alongside the tag above as someone moves through the programme, so a workflow can trigger on the stage rather than on âan applicant existsâ. A contact carries exactly one at a time â moving on removes the stage before it, and nothing else on the contact is touched. Leave one blank to tag nothing at that stage. ",-1)),t("div",Yt,[(n(),d(E,null,N(se,l=>o(a(k),{key:l.name,name:`stage_ghl_tags.${l.name}`,label:l.label,placeholder:l.placeholder,hint:l.hint},null,8,["name","label","placeholder","hint"])),64))]),o(a(k),{name:"invite_code_ghl_field",label:"Invite code custom field",placeholder:"Certification Invite Code",hint:"A text custom field in our own GoHighLevel, by name or key (for example contact.invite_code). The person's invite code is written there at every push, so a workflow can put it in an email. Leave blank to skip."}),t("div",Jt,[o(a(k),{name:"blocked_ghl_tag",label:"Blocked tag",placeholder:"certification-blocked",hint:"Added when a student's account is blocked, removed when unblocked. Sits beside the stage tag."}),o(a(k),{name:"archived_ghl_tag",label:"Archived tag",placeholder:"certification-archived",hint:"Added when the company is archived, removed on unarchive."})]),o(a(H),{name:"cohort_ghl_tag_enabled",label:"Tag the cohort too",description:"Adds the intake's own name â âCohort 3â when it has no name â so one round can be addressed on its own. Set at every push, from the code they redeemed."})])],512),[[C,m.value==="applications"]]),t("button",{type:"submit",disabled:b,class:"rounded-lg bg-teal-600 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-teal-700 disabled:opacity-50"},[b?(n(),h(a(pe),{key:0,size:16,class:"mr-2 inline animate-spin"}
445)):_("",!0),e[25]||(e[25]=r(" Save Settings ",-1))],8,Qt)]),_:1},8,["validation-schema","initial-values"]),[[C,m.value!=="api"]]),m.value==="general"?(n(),h(Y,{key:0,class:"mt-6 max-w-3xl",title:"Backfill partner subscriptions",description:"Partners who signed up with an invite code never got a subscription, and the AI credit wallet lives on it â so every credit award fails for them. This subscribes each one to the certification package above. Anyone who has ever had a subscription of their own is left alone.",icon:a(Me),fields:[{key:"limit",label:"Limit (0 = everyone)",type:"number",default:0,min:0}],"confirm-apply":"This creates a free subscription for every partner account listed in the dry run. Dry-run first.",run:a(at),poll:a(tt)},null,8,["icon","run","poll"])):_("",!0),m.value==="applications"?(n(),h(Y,{key:1,class:"mt-6 max-w-3xl",title:"Backfill GHL tags",description:"Contacts pushed before stage tags existed carry the applicant tag only. This works out where each of them is now â applied, accepted, joined, enrolled, certified â and writes that tag plus their cohort. Safe to re-run: every run sets the stage they are at today and drops the others.",icon:a(Be),fields:[{key:"limit",label:"Limit (0 = everyone)",type:"number",default:0,min:0}],"confirm-apply":"This writes tags on real contacts in our own GoHighLevel, which can fire your workflows. Dry-run first.",run:a(nt),poll:a(rt)},null,8,["icon","run","poll"])):_("",!0),m.value==="applications"?(n(),h(Y,{key:2,class:"mt-4 max-w-3xl",title:"Add missing students to GHL",description:"Every joined, enrolled or certified student is looked up in our own GoHighLevel by email. Anyone with no contact at all gets one, carrying the stage and cohort tags they would have today. Students who already have a contact are left untouched.",icon:a(We),fields:[{key:"limit",label:"Limit (0 = everyone)",type:"number",default:0,min:0}],"confirm-apply":"This creates real contacts in our own GoHighLevel, which can fire your workflows. Dry-run first.",run:a(it),poll:a(ot)},null,8,["icon","run","poll"])):_("",!0),m.value==="applications"?(n(),h(Y,{key:3,class:"mt-4 max-w-3xl",title:"Why is this person not tagged?",description:"Everything we hold for one address: their application, their account, the stage that follows from it, the tags a push would write, and every contact in our GoHighLevel with that address. Two contacts is the usual answer â only the first is ever written to.",icon:a(Ye),fields:[{key:"email",label:"Email address",type:"text",placeholder:"[email protected]"}],"read-only":"",run:a(st)},null,8,["icon","run"])):_("",!0),m.value==="api"?(n(),d("section",Xt,[e[60]||(e[60]=t("p",{class:"text-sm text-gray-600 dark:text-gray-400"}," Endpoints the marketing site calls. All of them take a shared key and are rate limited, and every one is read server-to-server â none of this is meant to be reachable from a visitor's browser. ",-1)),t("div",Zt,[t("button",{type:"button",onClick:e[0]||(e[0]=b=>I(Z.value)),class:"inline-flex shrink-0 items-center gap-2 rounded-lg bg-teal-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-teal-700"},[u.value===Z.value?(n(),h(a(U),{key:0,size:16})):(n(),h(a(q),{key:1,size:16})),r(" "+s(u.value===Z.value?"Copied":"Copy everything on this page"),1)]),e[26]||(e[26]=t("p",{class:"min-w-48 flex-1 text-xs text-gray-600 dark:text-gray-400"},[r(" The whole reference as one markdown document â rules, endpoints, response shapes and the PHP â ready to paste into an AI and have it build the page. "),t("strong",null,"The key is a placeholder"),r(", not the real value: send that to whoever builds the page separately, never into a chat tool. ")],-1))]),e[61]||(e[61]=t("p",{class:"mt-3 rounded-lg border border-teal-200 bg-teal-50 p-3 text-sm text-teal-800 dark:border-teal-800 dark:bg-teal-900/20 dark:text-teal-300"},[t("strong",null,"Call these from PHP, not from the browser."),r(" WordPress fetches on the server, caches the result and renders the cards into the page â so the API host never appears in the page source or the network tab, the cards are in the HTML for search engines, and there is no CORS to configure. They are "),t("strong",null,"key-protected"),r(", so calling them from browser JavaScript would mean shipping the key to every visitor: don't. ")],-1)),t("div",ea,[e[29]||(e[29]=t("h3",{class:"mb-2 text-sm font-semibold text-gray-900 dark:text-white"},"Authentication",-1)),e[30]||(e[30]=t("p",{class:"text-sm text-gray-600 dark:text-gray-400"}," Every request sends the shared key as a header: ",-1)),t("div",ta,[t("co
445de",aa,[e[27]||(e[27]=t("span",{class:"text-gray-500"},"X-Partner-Key:",-1)),v.value?(n(),d(E,{key:1},[r(s(j.value?v.value:me.value),1)],64)):(n(),d("span",ra,"not set"))]),v.value?(n(),d("button",{key:0,type:"button",onClick:e[1]||(e[1]=b=>j.value=!j.value),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[j.value?(n(),h(a(Je),{key:0,size:14})):(n(),h(a(Qe),{key:1,size:14})),r(" "+s(j.value?"Hide":"Reveal"),1)])):_("",!0),v.value?(n(),d("button",{key:1,type:"button",onClick:e[2]||(e[2]=b=>I(v.value)),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[u.value===v.value?(n(),h(a(U),{key:0,size:14,class:"text-emerald-500"})):(n(),h(a(q),{key:1,size:14})),r(" "+s(u.value===v.value?"Copied":"Copy"),1)])):_("",!0)]),v.value?_("",!0):(n(),d("p",na,[...e[28]||(e[28]=[t("code",null,"PARTNER_API_KEY",-1),r(" is not set on the backend, so every endpoint below is answering ",-1),t("code",null,"503",-1),r(" and the directory is dark. Set it in the deployment config and restart. ",-1)])])),e[31]||(e[31]=W('<ul class="mt-3 space-y-1 text-xs text-gray-500 dark:text-gray-400"><li> Masked on purpose. <strong>Copy works without revealing</strong> â use it, and the value never lands in a screenshot, a screen share, or a support thread. </li><li> The value lives in <code>PARTNER_API_KEY</code> on the backend and is <strong>changed there</strong>, not here. This panel only reads it. </li><li> It is its own key, <strong>not</strong> the internal service key. A WordPress install is a different trust domain, so a leak there costs you this feed and nothing else â rotate <code>PARTNER_API_KEY</code> and you are done. </li><li> Without it set, these endpoints answer <code>503</code> rather than serving openly. Set the key before deploying, or the directory goes dark. </li><li> Partner photos stay public: <code>photo_file_id</code> resolves to a plain image URL that visitors' browsers have to be able to load once the cards are on the page. </li></ul>',1))]),t("div",oa,[e[32]||(e[32]=t("div",{class:"mb-2 flex flex-wrap items-center gap-2"},[t("span",{class:"rounded bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300"}," LIVE "),t("h3",{class:"text-sm font-semibold text-gray-900 dark:text-white"},"Open intake")],-1)),e[33]||(e[33]=t("p",{class:"mb-3 text-sm text-gray-600 dark:text-gray-400"},[r(" The intake currently taking sign-ups and how many seats are left â the âN seats leftâ line on the directory page. Returns "),t("code",null,'{ "cohort": null }'),r(" when every intake is closed or full, so the page can hide the counter rather than print a zero. ")],-1)),t("div",ia,[t("code",{class:"flex-1 rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}," GET "+s(T)),t("button",{type:"button",onClick:e[3]||(e[3]=b=>I(T)),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[u.value===T?(n(),h(a(U),{key:0,size:14,class:"text-emerald-500"})):(n(),h(a(q),{key:1,size:14})),r(" "+s(u.value===T?"Copied":"Copy"),1)])]),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(ge)),e[34]||(e[34]=W('<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"><code>seats_left</code> counts codes that have been <em>issued</em>, not redeemed â a seat is spoken for the moment an invite goes out, and comes back if the code is cancelled. <code>applications_open</code> is whether the apply form should render at all; read it rather than hard-coding the deadline into the page. </p><p class="mt-2 text-xs text-gray-500 dark:text-gray-400"><code>cohorts</code> carries every open intake, so a page can show more than one panel â the round being applied to, and a named intake like <code>Founding</code> counting its own seats down beside it. Pick by <code>name</code>; each is an ordinary cohort with its own <code>seats_left</code> and <code>ends_on</code>. </p>',2))]),t("div",sa,[e[38]||(e[38]=t("div",{class:"mb-1 flex items-center gap-2"},[t("h3",{class:"text-sm font-semibold text-gray-900 dark:text-white"},"Apply for a seat")],-1)),t("p",la,[e[36]||(e[36]=r(" Takes one application. Without a code it joins the queue on the ",-1)),o(O,{to:"/admin/certification/applicants",class:"font-medium text-teal-600 hover:underline dark:text-teal-400"},{default:M(()=>[...e[35]||(e[35]=[r(" Applicants ",-1)])]),_:1}),e[37]||(e[37]=r(" screen; with a valid partner invite code it is accepted on the spot and the response carries the sign-up link to redirect to. ",-1))]),t("div",da,[t("code",{class:"flex-1 rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}," POST "+s(A)),t("button",{type:"button",onClick:e[4]||(e[4]=b=>I(A)),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[u.value===A?(n(),h(a(U),{key:0,size:14,class:"text-emerald-500"})):(n(),h(a(q),{key:1,size:14})),r(" "+s(u.value===A?"Copied":"Copy"),1)])]),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(ue)),e[39]||(e[39]=W('<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> Only <code>name</code> and <code>email</code> are required. The choice fields take the keys, not your labels: <code>ghl_accounts</code> is <code>none|1-3|4-10|10-plus</code>, <code>relationship</code> is <code>freelancer|own_agency|in_house|other</code>, <code>commitment</code> is <code>live|partial|replays|not_this_round</code>. Anything else is a 400. </p>',1))]),t("div",pa,[e[47]||(e[47]=t("div",{class:"mb-2 flex flex-wrap items-center gap-2"},[t("span",{class:"rounded bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300"}," LIVE "),t("h3",{class:"text-sm font-semibold text-gray-900 dark:text-white"},"Partner listings")],-1)),e[48]||(e[48]=t("p",{class:"mb-3 text-sm text-gray-600 dark:text-gray-400"},[r(" Every published listing â this replaces the hardcoded array in the directory page. Only partners who are certified, have consented, and have their listing switched on appear. Optional "),t("code",null,"?country=PK"),r(" (ISO code) and "),t("
445code",null,"?search="),r(" narrow it, though the page can just as well filter the full list in the browser. ")],-1)),t("div",ca,[t("code",{class:"flex-1 rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}," GET "+s(S)),t("button",{type:"button",onClick:e[5]||(e[5]=b=>I(S)),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[u.value===S?(n(),h(a(U),{key:0,size:14,class:"text-emerald-500"})):(n(),h(a(q),{key:1,size:14})),r(" "+s(u.value===S?"Copied":"Copy"),1)])]),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(he)),t("ul",{class:"mt-2 space-y-1 text-xs text-gray-500 dark:text-gray-400"},[e[43]||(e[43]=t("li",null,[t("code",null,"countries"),r(" carries the filter pills, counted across the whole directory rather than the filtered slice â so the row never empties as someone filters. ")],-1)),e[44]||(e[44]=t("li",null,[t("code",null,"initials"),r(", "),t("code",null,"avatar_color"),r(" and "),t("code",null,"country"),r(" are derived server-side. Render them; don't store or recompute them, or a rename leaves two versions of the same partner. ")],-1)),t("li",null,[e[40]||(e[40]=t("code",null,"photo_file_id",-1)),e[41]||(e[41]=r(" is null when a partner hasn't uploaded one â that's when you fall back to the initials chip. Build the image URL as ",-1)),t("code",null,s(J)),e[42]||(e[42]=r("; it serves unauthenticated, so it can be linked straight from the page. ",-1))]),e[45]||(e[45]=t("li",null,[t("code",null,"whatsapp"),r(" is digits only, ready for "),t("code",null,"https://wa.me/<number>"),r(". It is public by definition: anyone can read it out of the page source, and partners are told so before they give one. ")],-1)),e[46]||(e[46]=t("li",null,[t("code",null,"website"),r(" is a full URL or an empty string. Strip the scheme for display â a card has no room for "),t("code",null,"https://"),r(" â but link the value as given. ")],-1))]),e[49]||(e[49]=t("h4",{class:"mt-4 mb-2 text-sm font-semibold text-gray-900 dark:text-white"}," One partner, for the profile page ",-1)),t("code",{class:"block rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}," GET "+s(S)+"/<slug> "),e[50]||(e[50]=t("p",{class:"mt-2 text-xs text-gray-500 dark:text-gray-400"},[r(" Returns "),t("code",null,'{ "partner": { ⦠} }'),r(", same shape as a card. 404 for an unknown slug "),t("em",null,"and"),r(" for a listing that has been taken down â so the endpoint can't be used to confirm someone was removed. The slug is frozen at first publish, so these URLs are safe to index. ")],-1))]),t("div",{class:"mt-6 border-t border-gray-200 pt-5 dark:border-gray-700"},[e[51]||(e[51]=t("h3",{class:"mb-2 text-sm font-semibold text-gray-900 dark:text-white"}," Using it in WordPress (server-side) ",-1)),e[52]||(e[52]=t("p",{class:"mb-3 text-sm text-gray-600 dark:text-gray-400"},[r(" One helper does the fetching and caching. Drop it in the theme's "),t("code",null,"functions.php"),r(" or a small plugin, then render from it in the template. ")],-1)),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(X)),e[53]||(e[53]=t("h4",{class:"mt-4 mb-2 text-sm font-semibold text-gray-900 dark:text-white"}," Rendering the cards ",-1)),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(oe)),e[54]||(e[54]=W('<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/50"><p class="text-xs font-semibold text-gray-800 dark:text-gray-200"> Rate limit, and why the cache still matters </p><p class="mt-1 text-xs text-gray-600 dark:text-gray-400"> These two endpoints allow <strong>60 requests per minute per IP</strong> â sized for server-side use, where every visitor to the site shares your server's one address. With the 5-minute transient above you will use about one request per five minutes, so there is enormous headroom; without it, a page doing 60 views a minute is already at the ceiling. Over the limit you get <code>429</code> with a <code>Retry-After</code> header, which is why the helper falls back to its last good copy instead of rendering an empty directory. </p></div><p class="mt-3 text-xs text-gray-500 dark:text-gray-400"><strong>Filter in the browser, not over the network.</strong> The country pills and search box should filter the cards already on the page. The <code>?country=</code> and <code>?search=</code>
445 parameters exist for other consumers â calling the API on every pill click would spend the rate limit and undo the point of rendering server-side. </p>',2))]),t("div",ha,[e[55]||(e[55]=t("div",{class:"mb-2 flex flex-wrap items-center gap-2"},[t("span",{class:"rounded bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300"}," LIVE "),t("h3",{class:"text-sm font-semibold text-gray-900 dark:text-white"},"Invite landing page")],-1)),e[56]||(e[56]=t("p",{class:"mb-3 text-sm text-gray-600 dark:text-gray-400"}," Checks an invite code and, if it is good, sends the invitee on to sign-up already greeted by name and cohort. This is why the check endpoint carries the key rather than being open: the code is verified here, on your server, and never by a browser. ",-1)),t("div",ga,[t("code",{class:"flex-1 rounded-lg bg-gray-100 px-3 py-2 font-mono text-xs break-all text-gray-800 dark:bg-gray-900 dark:text-gray-200"}," GET "+s(w)),t("button",{type:"button",onClick:e[6]||(e[6]=b=>I(w)),class:"inline-flex items-center gap-1.5 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"},[u.value===w?(n(),h(a(U),{key:0,size:14,class:"text-emerald-500"})):(n(),h(a(q),{key:1,size:14})),r(" "+s(u.value===w?"Copied":"Copy"),1)])]),t("pre",{class:"overflow-x-auto rounded-lg bg-gray-100 p-3 font-mono text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"},s(ce)),e[57]||(e[57]=t("p",{class:"mt-2 mb-3 text-xs text-gray-500 dark:text-gray-400"},[r(" Always "),t("code",null,"200"),r(" â "),t("code",null,"valid"),r(" is the answer, and "),t("code",null,"message"),r(" is written for the invitee, so show it to them as-is. It already distinguishes an expired code from a used one from a closed intake. ")],-1)),e[58]||(e[58]=t("h4",{class:"mb-2 text-sm font-semibold text-gray-900 dark:text-white"},"The landing page",-1)),t("pre",ua,s(R.value),1),e[59]||(e[59]=t("p",{class:"mt-2 text-xs text-gray-500 dark:text-gray-400"},[r(" Point "),t("strong",null,"Invite landing page"),r(" on the Invites tab at wherever you publish this, and every invite link and email will use it. Leave that blank and invites go straight to sign-up â which still works, because the code is validated again on submit whatever the browser was told; the invitee just arrives without a greeting. ")],-1))])])):_("",!0)],64))])}}};export{Ua as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.