diff --git a/backend/content/views/offers.py b/backend/content/views/offers.py
index e972cbd..261bf31 100644
--- a/backend/content/views/offers.py
+++ b/backend/content/views/offers.py
@@ -39,6 +39,7 @@ def _offer_to_dict(offer: Offer) -> dict:
"name": offer.organization.name,
"type": offer.organization.type,
"country": offer.organization.country,
+ "website": offer.organization.website,
},
"source_type": offer.source_type.name,
"target_profile": offer.target_profile.name,
diff --git a/frontend/src/components/sections/RoleContent.vue b/frontend/src/components/sections/RoleContent.vue
index c2dab21..9faea74 100644
--- a/frontend/src/components/sections/RoleContent.vue
+++ b/frontend/src/components/sections/RoleContent.vue
@@ -144,6 +144,38 @@
{{ offer.organization.name }}
Open ↗
+
@@ -307,6 +339,28 @@ function itemLabel(item) { return typeof item === 'string' ? item : item.label }
function itemValue(item) { return typeof item === 'string' ? item : item.value }
function itemType(item) { return typeof item === 'object' && item.type ? item.type : 'offer_type' }
+// ── Contact fallback (email → LinkedIn → name → org website → offer link) ──────
+function clean(value) {
+ const trimmed = typeof value === 'string' ? value.trim() : ''
+ return trimmed.length ? trimmed : null
+}
+
+function bestContact(offer) {
+ const email = clean(offer.contact?.email)
+ if (email) return { kind: 'email', value: email }
+
+ const linkedin = clean(offer.contact?.linkedin)
+ if (linkedin) return { kind: 'linkedin', value: linkedin, name: clean(offer.contact?.name) }
+
+ const name = clean(offer.contact?.name)
+ if (name) return { kind: 'name', value: name }
+
+ const website = clean(offer.organization?.website)
+ if (website) return { kind: 'website', value: website, orgName: offer.organization?.name }
+
+ return { kind: 'link', value: offer.link }
+}
+
// ── Actions ───────────────────────────────────────────────────────────────────
let debounceTimer = null
diff --git a/frontend/src/pages/AdminPage.vue b/frontend/src/pages/AdminPage.vue
index 72e91a7..54ba5a3 100644
--- a/frontend/src/pages/AdminPage.vue
+++ b/frontend/src/pages/AdminPage.vue
@@ -120,6 +120,32 @@
ℹ As a teacher, you can only add opportunities for {{ teacherOrgName ?? 'your institution' }}.
+
+
{{ addError }}
Offer created successfully.
@@ -423,22 +449,51 @@ const BLANK_FORM = () => ({
title: '', summary: '', link: '', country: '',
offer_type: '', organization_id: '', target_profile: '',
status: 'draft', deadline: '', domains: [],
+ contact_name: '', contact_email: '', contact_phone: '', contact_linkedin: '',
})
const form = reactive(BLANK_FORM())
const addLoading = ref(false)
const addError = ref('')
const addSuccess = ref(false)
+const contactError = ref('')
function resetForm() {
Object.assign(form, BLANK_FORM())
addError.value = ''
addSuccess.value = false
+ contactError.value = ''
+}
+
+function buildContactPayload() {
+ const name = form.contact_name.trim()
+ const email = form.contact_email.trim()
+ const phone = form.contact_phone.trim()
+ const linkedin = form.contact_linkedin.trim()
+
+ if (!name && !email && !phone && !linkedin) return { contact: undefined, error: null }
+
+ if (!name) return { contact: undefined, error: 'Contact name is required if any contact field is filled in.' }
+ if (!email && !phone) return { contact: undefined, error: 'Provide a contact email or phone number.' }
+
+ return {
+ contact: { name, email: email || null, phone: phone || null, linkedin: linkedin || null },
+ error: null,
+ }
}
async function submitOffer() {
addLoading.value = true
addError.value = ''
addSuccess.value = false
+ contactError.value = ''
+
+ const { contact, error: contactValidationError } = buildContactPayload()
+ if (contactValidationError) {
+ contactError.value = contactValidationError
+ addLoading.value = false
+ return
+ }
+
try {
const res = await api.post('/api/offers', {
title: form.title,
@@ -451,6 +506,7 @@ async function submitOffer() {
status: form.status,
deadline: form.deadline || null,
domains: form.domains,
+ ...(contact ? { contact } : {}),
})
if (res.ok) {
addSuccess.value = true
@@ -762,6 +818,21 @@ function formatDate(iso) {
margin-bottom: 1.5rem;
}
+.contact-section {
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: 1.25rem 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.section-h3 {
+ font-family: 'DM Serif Display', serif;
+ font-size: 16px;
+ color: var(--ink);
+}
+.contact-error { color: var(--accent-mid); }
+
/* Form */
.offer-form { display: flex; flex-direction: column; gap: 16px; }
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
diff --git a/frontend/src/style.css b/frontend/src/style.css
index d271729..e45a3de 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -1081,6 +1081,36 @@ footer {
.card-org { font-size: 12px; color: #888; }
+.card-contact-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding-top: 8px;
+ font-size: 12px;
+ flex-wrap: wrap;
+}
+
+.card-contact-label {
+ color: #aaa;
+ font-weight: 500;
+ flex-shrink: 0;
+}
+
+.card-contact-link {
+ color: #555;
+ text-decoration: none;
+ word-break: break-all;
+}
+
+.card-contact-link:hover {
+ color: var(--ink);
+ text-decoration: underline;
+}
+
+.card-contact-text {
+ color: #555;
+}
+
.card-domains {
display: flex;
flex-wrap: wrap;