diff --git a/src/admin/index.html b/src/admin/index.html index 4fe18a5..3d3a361 100644 --- a/src/admin/index.html +++ b/src/admin/index.html @@ -2,9 +2,16 @@ - + admin + + + +
diff --git a/src/admin/public/favicon.png b/src/admin/public/favicon.png new file mode 100644 index 0000000..7388543 Binary files /dev/null and b/src/admin/public/favicon.png differ diff --git a/src/admin/public/favicon.svg b/src/admin/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/src/admin/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/admin/src/components/AdminHeader.tsx b/src/admin/src/components/AdminHeader.tsx index d1614ee..56803f0 100644 --- a/src/admin/src/components/AdminHeader.tsx +++ b/src/admin/src/components/AdminHeader.tsx @@ -1,22 +1,48 @@ import { useAuthMe } from '../api/auth'; +import { useRouterState } from '@tanstack/react-router'; import { LogOut, CircleUser } from 'lucide-react'; +const TITLES: Array<{ match: RegExp; title: string }> = [ + { match: /^\/events\/new$/, title: 'New event' }, + { match: /^\/events\/[^/]+\/edit$/, title: 'Edit event' }, + { match: /^\/events/, title: 'Events' }, + { match: /^\/partners/, title: 'Partners' }, + { match: /^\/banners\/new$/, title: 'New banner' }, + { match: /^\/banners\/[^/]+\/edit$/, title: 'Edit banner' }, + { match: /^\/banners/, title: 'Banners' }, + { match: /^\/apps\/register$/, title: 'Register application' }, + { match: /^\/apps/, title: 'Applications' }, + { match: /^\/$/, title: 'Dashboard' }, +]; + +function titleForPath(pathname: string): string { + return TITLES.find((t) => t.match.test(pathname))?.title ?? ''; +} + export function AdminHeader() { const { data: user } = useAuthMe(); + const { location } = useRouterState(); + const title = titleForPath(location.pathname); return ( -
-
-
+
+
+ {title && ( +

{title}

+ )} +
+
{user && ( -
- - {user.email} +
+ + {user.email}
)} diff --git a/src/admin/src/index.css b/src/admin/src/index.css index 9e13947..f873601 100644 --- a/src/admin/src/index.css +++ b/src/admin/src/index.css @@ -39,6 +39,9 @@ ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); + } + + html { font-family: Lexend, 'Noto Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-weight: 300; } @@ -47,6 +50,10 @@ @apply bg-gray-50 text-gray-900 antialiased; } + input, select, textarea, button { + font: inherit; + } + input, select, textarea { @apply text-sm; } @@ -124,6 +131,20 @@ .section-title { @apply text-sm font-medium text-gray-500 uppercase tracking-wider; } + + /* Eyebrow — small uppercase tracked label used above section headings. */ + .eyebrow { + @apply inline-flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500; + } + + .eyebrow::before { + content: ''; + display: inline-block; + width: 22px; + height: 1px; + background: currentColor; + opacity: 0.5; + } } /* Scrollbar */ diff --git a/src/app/Components/Components/ActivityCard.razor b/src/app/Components/Components/ActivityCard.razor index c8bb0f3..9a7904b 100644 --- a/src/app/Components/Components/ActivityCard.razor +++ b/src/app/Components/Components/ActivityCard.razor @@ -1,18 +1,23 @@ -
- .NET Cameroon @Title -
-

@Title

-

@Description

+
+
+ .NET Cameroon @Title
-
- @if(!string.IsNullOrWhiteSpace(Link) && !string.IsNullOrWhiteSpace(LinkText)) +
+

@Title

+

@Description

+ @if (!string.IsNullOrWhiteSpace(Link) && !string.IsNullOrWhiteSpace(LinkText)) { - @LinkText + + @LinkText + + }
-
+
-@code{ +@code { [Parameter] public string Title { get; set; } = string.Empty; diff --git a/src/app/Components/Components/BannerStack.razor b/src/app/Components/Components/BannerStack.razor index fed3f4a..78251b2 100644 --- a/src/app/Components/Components/BannerStack.razor +++ b/src/app/Components/Components/BannerStack.razor @@ -17,15 +17,9 @@
- @if (!string.IsNullOrEmpty(_job.Salary)) + + @if (_job.Skills.Length > 0) { -
-
- - @_job.Salary +
+

Skills

+
+ @foreach (var skill in _job.Skills) + { + @skill + }
-
+ } - -
-

Required Skills

-
- @foreach (var skill in _job.Skills) - { - @skill - } -
-
- -
-

Job Description

-
+
+

Role

+
@((MarkupString)_job.FullDescription)
-
+ @if (_job.Responsibilities.Length > 0) { -
-

Responsibilities

-
    - @foreach (var responsibility in _job.Responsibilities) +
    +

    Responsibilities

    +
      + @foreach (var r in _job.Responsibilities) { -
    • @responsibility
    • +
    • + + @r +
    • }
    -
+ } @if (_job.Requirements.Length > 0) { -
-

Requirements

-
    - @foreach (var requirement in _job.Requirements) +
    +

    Requirements

    +
      + @foreach (var r in _job.Requirements) { -
    • @requirement
    • +
    • + + @r +
    • }
    -
+ } @if (_job.Benefits.Length > 0) { -
-

Benefits

-
    - @foreach (var benefit in _job.Benefits) +
    +

    Benefits

    +
      + @foreach (var b in _job.Benefits) { -
    • @benefit
    • +
    • + + @b +
    • }
    -
+ } -
-
+ - -
- -
-

Apply for this position

- - @if (!string.IsNullOrEmpty(_job.ApplicationUrl)) - { - - Apply Now - - } - else if (!string.IsNullOrEmpty(_job.ContactEmail)) - { - - Send Application - - } + +
- -
-
-

Similar Opportunities

-
+ +
+
+
+
+ Also open +

Similar opportunities

+
+ +
+
@foreach (var similarJob in _similarJobs.Take(3)) { -
-

@similarJob.Title

-

@similarJob.Company

- + }
diff --git a/src/app/Components/Pages/Jobs/Index.razor b/src/app/Components/Pages/Jobs/Index.razor index de0a458..f64006c 100644 --- a/src/app/Components/Pages/Jobs/Index.razor +++ b/src/app/Components/Pages/Jobs/Index.razor @@ -1,5 +1,6 @@ @page "/jobs" @using Microsoft.Extensions.Localization +@using app.Components.Pages.Jobs.Components @attribute [StreamRendering] @inject Microsoft.Extensions.Localization.IStringLocalizerFactory LocalizerFactory @inject NavigationManager NavigationManager @@ -26,87 +27,100 @@
- - -
-
-
-

Find Your Next Opportunity

-

- Discover .NET and software development jobs in Cameroon and worldwide -

- - Post a Job + + +
+
+
- -
-
-
-
- +
+
+
+
+ + + placeholder="Search by title, skill, or company" + class="w-full pl-10 pr-4 py-2.5 text-sm bg-gray-50/70 border border-gray-200 rounded-lg focus:outline-none focus:border-secondary focus:bg-white focus:ring-2 focus:ring-secondary/15 transition-colors" />
-
- + - + -
- - @if (!string.IsNullOrEmpty(_searchQuery) || !string.IsNullOrEmpty(_selectedLocation) || !string.IsNullOrEmpty(_selectedType)) + + @if (HasActiveFilters) {
- Active filters: + Filters @if (!string.IsNullOrEmpty(_searchQuery)) { - + "@_searchQuery" - } @if (!string.IsNullOrEmpty(_selectedLocation)) { - + @_selectedLocation - } @if (!string.IsNullOrEmpty(_selectedType)) { - + @_selectedType - } -
@@ -114,330 +128,136 @@
- -
-
-
-

How It Works

-

- Whether you're looking for your next opportunity or searching for talented .NET developers, - our platform makes the process simple and efficient. -

-
- -
- -
-
-
- -
-

For Job Seekers

-
- -
- -
-
- 1 -
-
-

Browse Opportunities

-

- Explore our curated list of .NET and software development jobs. Use filters to find positions that match your skills and preferences. -

-
-
- - -
-
- 2 -
-
-

Review Job Details

-

- Click on any job to see complete information including responsibilities, requirements, salary range, and company details. -

-
-
- - -
-
- 3 -
-
-

Apply Directly

-

- Submit your application through the provided email or external link. Each job posting includes clear application instructions and deadlines. -

-
-
- - -
-
- 4 -
-
-

Get Hired

-

- Connect with employers and showcase your .NET expertise. Join our vibrant community of developers. -

-
-
-
- -
-
- - Tip: Join our Discord community to stay updated on the latest job opportunities and connect with other developers! -
-
+ +
+
+
+
+

Latest openings

+

@FilteredJobs.Length position@(FilteredJobs.Length != 1 ? "s" : "")

- - -
-
-
- -
-

For Employers

-
- -
- -
-
- 1 -
-
-

Post Your Job

-

- Click on "Post a Job" and fill out our comprehensive form with your job details, requirements, and company information. -

-
-
- - -
-
- 2 -
-
-

Review & Approval

-

- Our team reviews your submission to ensure quality and relevance. This typically takes 24-48 hours. -

-
-
- - -
-
- 3 -
-
-

Go Live

-

- Once approved, your job posting goes live and is visible to thousands of skilled .NET developers in our community. -

-
-
- - -
-
- 4 -
-
-

Receive Applications

-

- Candidates apply directly through your provided contact method. Review applications and find your perfect match! -

-
-
-
- -
-
- - Bonus: Promoted jobs get featured placement for maximum visibility and reach! -
-
+
+ +
- -
- -
-
- -
-

Ready to Get Started?

-

- Join hundreds of .NET developers and leading companies connecting through our platform + @if (FilteredJobs.Length == 0) + { +

+ +

Job board coming soon

+

+ We're building a job board to connect .NET developers with opportunities in Cameroon and beyond. + Meanwhile, join the community to stay updated on new openings.

-
- - Post a Job + -
-
- - Free to Post -
-
- - Quality Candidates -
-
- - Fast Approval -
-
- - Community Trusted -
-
-
+ } + else + { +
+ @foreach (var job in FilteredJobs) + { + + } +
+ }
- -
-
-
-

Latest Job Opportunities

-

@FilteredJobs.Length position@(FilteredJobs.Length != 1 ? "s" : "") found

-
-
- - + +
+
+
+ How it works +

A simple flow on both sides.

+

+ Whether you're searching for your next role or looking to hire, the path is straightforward. +

-
- @if (FilteredJobs.Length == 0) - { - -
-
- -

Job Board Coming Soon

-

- We're currently building our job board to connect talented .NET developers with amazing opportunities. - In the meantime, join our community to stay updated on job openings shared by our members. -

-
- - Join Discord - - - Follow on LinkedIn - +
+ +
+
+ + + +

For developers

+
    + @foreach (var (step, i) in _seekerSteps.Select((s, i) => (s, i + 1))) + { +
  1. + @i +
    +

    @step.Title

    +

    @step.Body

    +
    +
  2. + } +
-
- } - else - { - -
- @foreach (var job in FilteredJobs) - { -
-
-
-

@job.Title

-

@job.Company

-
- @job.Type -
-
- @job.Location - @job.PostedDate - @if (!string.IsNullOrEmpty(job.Salary)) - { - @job.Salary - } -
-
- @foreach (var skill in job.Skills) - { - @skill - } -
-

@job.Description

- - View Details - -
- } -
- } - - @if (_exampleJobs.Length > 0) - { -
-

How It Will Work

-
- @foreach (var example in _exampleJobs) - { -
-
+ +
+
+ + + +

For employers

+
+
    + @foreach (var (step, i) in _employerSteps.Select((s, i) => (s, i + 1))) + { +
  1. + @i
    -

    @example.Title

    -

    @example.Company

    +

    @step.Title

    +

    @step.Body

    - @example.Type -
-
- @example.Location - @example.PostedDate -
-
- @foreach (var skill in example.Skills) - { - @skill - } -
-

@example.Description

- -
- } + + } +
- } -
- -
-
-

Are You Hiring?

-

- If you're looking to hire talented .NET developers, reach out to us. - We can help you connect with skilled professionals from our community. -

- @@ -453,11 +273,30 @@ private string _selectedType = ""; private ViewMode _viewMode = ViewMode.Grid; - private enum ViewMode - { - Grid, - List - } + private bool HasActiveFilters => + !string.IsNullOrEmpty(_searchQuery) || + !string.IsNullOrEmpty(_selectedLocation) || + !string.IsNullOrEmpty(_selectedType); + + private enum ViewMode { Grid, List } + + private record StepItem(string Title, string Body); + + private readonly StepItem[] _seekerSteps = + [ + new("Browse openings", "Use filters to narrow down roles by location, type, and required skills."), + new("Review the details", "Open any listing for full description, requirements, salary range, and company info."), + new("Apply directly", "Each posting includes the employer's contact method or external application link."), + new("Stay connected", "Join the community to hear about new roles as they're posted."), + ]; + + private readonly StepItem[] _employerSteps = + [ + new("Submit a posting", "Fill in the role details, requirements, and how candidates should reach you."), + new("Quality review", "We review submissions within 24–48 hours to keep the bar high."), + new("Goes live", "Once approved, your role is visible to thousands of .NET developers."), + new("Receive applications", "Candidates reach out through your preferred channel."), + ]; private class JobExample { @@ -527,24 +366,15 @@ }) .ToArray(); - private void ApplyFilters() - { - StateHasChanged(); - } + private void ApplyFilters() => StateHasChanged(); private void ClearFilter(string filterName) { switch (filterName) { - case nameof(_searchQuery): - _searchQuery = ""; - break; - case nameof(_selectedLocation): - _selectedLocation = ""; - break; - case nameof(_selectedType): - _selectedType = ""; - break; + case nameof(_searchQuery): _searchQuery = ""; break; + case nameof(_selectedLocation): _selectedLocation = ""; break; + case nameof(_selectedType): _selectedType = ""; break; } StateHasChanged(); } diff --git a/src/app/Components/Pages/Jobs/Submit.razor b/src/app/Components/Pages/Jobs/Submit.razor index f448f04..eb025a4 100644 --- a/src/app/Components/Pages/Jobs/Submit.razor +++ b/src/app/Components/Pages/Jobs/Submit.razor @@ -18,253 +18,225 @@
- -
-
- -
- - Back to jobs + +
+
+
+ + + All jobs -

Post a Job Opportunity

-

- Fill out the form below to post your job opportunity. All submissions will be reviewed before publication. -

-
- @if (_submitted) - { - -
- -

Job Submitted Successfully!

-

- Thank you for submitting your job opportunity. Our team will review it and publish it shortly. - You will receive a confirmation email once it's live. +

+ New posting +

Post a role

+

+ Fill in the details below. Submissions are reviewed within 24–48 hours before publication.

-
- - View All Jobs - - -
- } - else - { - -
- -
-

Company Information

- -
- - -
-
- - + @if (_submitted) + { +
+
+
- -
- - +

Submitted

+

+ Your job has been submitted for review. You'll get an email confirmation once it's published. +

+
+ + View all jobs + + +
+ } + else + { + + +
+ Company - -
-

Job Details

- -
- - -
+
+ + +
-
- - + +
- - + +
-
+
-
- - -
+ +
+ The role -
- - -

Minimum 100 characters

-
+
+ + +
-
- - -

Separate skills with commas

-
+
+
+ + +
+
+ + +
+
-
- - -

One responsibility per line

-
+
+ + +
-
- - -

One requirement per line

-
+
+ + +

Minimum 100 characters.

+
-
- - -

One benefit per line (optional)

-
-
+
+ + +

Separate skills with commas.

+
- -
-

Application Information

- -
- - -
+
+ + +

One per line.

+
-
- - -

If you have an online application form

-
+
+ + +

One per line.

+
-
- - -
-
+
+ + +

One per line. Optional.

+
+ - -
- -
+ +
+ How to apply - @if (!string.IsNullOrEmpty(_errorMessage)) - { -
- @_errorMessage -
- } +
+ + +
- -
-
+ + +
+ + + @if (!string.IsNullOrEmpty(_errorMessage)) { - - Submit Job +
+ + @_errorMessage +
} - - - Cancel - -
- - } + +
+ + Cancel +
+
+ + } +
diff --git a/src/app/styles/input.css b/src/app/styles/input.css index 38e05c6..6113e14 100644 --- a/src/app/styles/input.css +++ b/src/app/styles/input.css @@ -92,6 +92,21 @@ } } +/* Eyebrow label — small uppercase tracked text used above section headings + instead of loud pill badges. Pair with a leading dot for color accent. */ +.eyebrow { + @apply inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em]; +} + +.eyebrow::before { + content: ''; + display: inline-block; + width: 28px; + height: 1px; + background: currentColor; + opacity: 0.5; +} + /* Headings */ .heading { @apply font-heading; @@ -488,25 +503,3 @@ } } -.banner-shine::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient( - 115deg, - transparent 30%, - rgba(255, 255, 255, 0.55) 50%, - transparent 70% - ); - transform: translateX(-100%); - animation: banner-shine 4.5s ease-in-out 1s infinite; -} - -@keyframes banner-shine { - 0% { - transform: translateX(-100%); - } - 60%, 100% { - transform: translateX(100%); - } -}