We use cookies to enhance your experience on the site
CodeWorlds

Professional Profiles - CV and Experience

Welcome @name! Time to build Crew Connections - a professional network for pirates looking for crew members and collaboration. 💼

This will be like LinkedIn - pirates showcase their skills, sea experience, and look for positions in crews.

LinkedIn vs Other Platforms

Twitter/Instagram/YouTube:

  • Personal content
  • Entertainment and social
  • Following for fun

LinkedIn:

  • Professional profiles (online CV)
  • Business networking
  • Job posts and recruitment
  • Endorsements and recommendations
  • Professional updates

Profile Structure

1interface Profile {
2  id: string;
3  name: string;
4  headline: string; // "Senior Navigator at Black Pearl"
5  avatar: string;
6  banner: string;
7  location: string;
8  connections: number;
9
10  about: string; // Professional summary
11
12  experience: Experience[];
13  education: Education[];
14  skills: Skill[];
15
16  endorsements: Endorsement[];
17  recommendations: Recommendation[];
18}
19
20interface Experience {
21  id: string;
22  title: string; // "First Mate"
23  company: string; // "Black Pearl Crew"
24  companyLogo?: string;
25  location: string;
26  startDate: Date;
27  endDate?: Date; // null = currently working
28  description: string;
29  skills: string[];
30}
31
32interface Education {
33  id: string;
34  school: string;
35  degree: string; // "Master of Navigation"
36  field: string; // "Cartography & Maritime Studies"
37  startDate: Date;
38  endDate: Date;
39  description?: string;
40}
41
42interface Skill {
43  id: string;
44  name: string;
45  endorsements: number;
46}
47
48interface Endorsement {
49  skillId: string;
50  endorsedBy: {
51    id: string;
52    name: string;
53    avatar: string;
54  };
55  date: Date;
56}
57
58interface Recommendation {
59  id: string;
60  author: {
61    id: string;
62    name: string;
63    avatar: string;
64    headline: string;
65  };
66  relationship: string; // "worked together at..."
67  text: string;
68  date: Date;
69}

Profile Header

1'use client';
2
3import { useState } from 'react';
4
5export default function ProfileHeader({ profile }: { profile: Profile }) {
6  const [isConnected, setIsConnected] = useState(false);
7  const [showContact, setShowContact] = useState(false);
8
9  return (
10    <div className="bg-white rounded-lg shadow mb-4 overflow-hidden">
11      {/* Banner */}
12      <div
13        className="h-48 bg-gradient-to-r from-blue-600 to-cyan-600"
14        style={{
15          backgroundImage: `url(${profile.banner})`,
16          backgroundSize: 'cover',
17        }}
18      />
19
20      {/* Profile Info */}
21      <div className="px-6 pb-6">
22        <div className="flex items-start gap-6 -mt-20">
23          {/* Avatar */}
24          <img
25            src={profile.avatar}
26            alt={profile.name}
27            className="w-40 h-40 rounded-full border-4 border-white shadow-lg"
28          />
29
30          <div className="flex-1 mt-20">
31            <div className="flex items-start justify-between">
32              <div>
33                <h1 className="text-3xl font-bold mb-1">{profile.name}</h1>
34                <p className="text-xl text-gray-700 mb-2">{profile.headline}</p>
35                <div className="flex items-center gap-3 text-gray-600 text-sm mb-3">
36                  <span>{profile.location}</span>
37                  <span></span>
38                  <button className="text-blue-600 hover:underline">
39                    {profile.connections} connections
40                  </button>
41                </div>
42              </div>
43
44              {/* Action Buttons */}
45              <div className="flex items-center gap-2">
46                <button
47                  onClick={() => setIsConnected(!isConnected)}
48                  className={`px-6 py-2 rounded-full font-semibold transition ${
49                    isConnected
50                      ? 'border-2 border-blue-600 text-blue-600'
51                      : 'bg-blue-600 text-white hover:bg-blue-700'
52                  }`}
53                >
54                  {isConnected ? '✓ Connected' : '+ Connect'}
55                </button>
56                <button className="px-6 py-2 border-2 border-gray-300 rounded-full font-semibold hover:border-gray-400">
57                  Message
58                </button>
59                <button
60                  onClick={() => setShowContact(true)}
61                  className="px-6 py-2 border-2 border-gray-300 rounded-full font-semibold hover:border-gray-400"
62                >
63                  More
64                </button>
65              </div>
66            </div>
67
68            {/* About Section */}
69            {profile.about && (
70              <div className="mt-4">
71                <h2 className="font-bold text-lg mb-2">About</h2>
72                <p className="text-gray-700 whitespace-pre-wrap">{profile.about}</p>
73              </div>
74            )}
75          </div>
76        </div>
77      </div>
78    </div>
79  );
80}

Experience Section

1function ExperienceCard({ experience }: { experience: Experience }) {
2  const isCurrentJob = !experience.endDate;
3
4  const formatDate = (date: Date) => {
5    return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
6  };
7
8  const calculateDuration = (start: Date, end?: Date) => {
9    const endDate = end || new Date();
10    const months = (endDate.getFullYear() - start.getFullYear()) * 12 +
11                   (endDate.getMonth() - start.getMonth());
12    const years = Math.floor(months / 12);
13    const remainingMonths = months % 12;
14
15    if (years === 0) return `${remainingMonths} mos`;
16    if (remainingMonths === 0) return `${years} yrs`;
17    return `${years} yrs ${remainingMonths} mos`;
18  };
19
20  return (
21    <div className="flex gap-4 py-4 border-b last:border-0">
22      {/* Company Logo */}
23      {experience.companyLogo ? (
24        <img
25          src={experience.companyLogo}
26          alt={experience.company}
27          className="w-12 h-12 rounded"
28        />
29      ) : (
30        <div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xl">
31          🏢
32        </div>
33      )}
34
35      <div className="flex-1">
36        <h3 className="font-bold text-lg">{experience.title}</h3>
37        <p className="text-gray-700">{experience.company}</p>
38        <p className="text-sm text-gray-600 mb-2">
39          {formatDate(experience.startDate)} -{' '}
40          {isCurrentJob ? 'Present' : formatDate(experience.endDate!)}{' '}
41          {calculateDuration(experience.startDate, experience.endDate)}
42        </p>
43        <p className="text-sm text-gray-600 mb-2">{experience.location}</p>
44
45        {experience.description && (
46          <p className="text-gray-700 text-sm mb-3 whitespace-pre-wrap">
47            {experience.description}
48          </p>
49        )}
50
51        {/* Skills */}
52        {experience.skills.length > 0 && (
53          <div className="flex flex-wrap gap-2">
54            {experience.skills.map((skill, idx) => (
55              <span
56                key={idx}
57                className="px-3 py-1 bg-gray-100 rounded-full text-sm font-medium"
58              >
59                {skill}
60              </span>
61            ))}
62          </div>
63        )}
64      </div>
65    </div>
66  );
67}
68
69export default function ExperienceSection({ experiences }: { experiences: Experience[] }) {
70  return (
71    <div className="bg-white rounded-lg shadow p-6 mb-4">
72      <h2 className="text-2xl font-bold mb-4">Experience</h2>
73      <div>
74        {experiences.map(exp => (
75          <ExperienceCard key={exp.id} experience={exp} />
76        ))}
77      </div>
78    </div>
79  );
80}

Skills Section with Endorsements

1'use client';
2
3import { useState } from 'react';
4
5interface SkillItemProps {
6  skill: Skill;
7  onEndorse: (skillId: string) => void;
8  isEndorsed: boolean;
9}
10
11function SkillItem({ skill, onEndorse, isEndorsed }: SkillItemProps) {
12  return (
13    <div className="flex items-center justify-between py-3 border-b">
14      <div className="flex-1">
15        <h3 className="font-semibold">{skill.name}</h3>
16        <p className="text-sm text-gray-600">
17          {skill.endorsements} endorsements
18        </p>
19      </div>
20
21      <button
22        onClick={() => onEndorse(skill.id)}
23        disabled={isEndorsed}
24        className={`px-4 py-2 rounded-full font-semibold transition ${
25          isEndorsed
26            ? 'bg-gray-100 text-gray-500 cursor-not-allowed'
27            : 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'
28        }`}
29      >
30        {isEndorsed ? '✓ Endorsed' : '+ Endorse'}
31      </button>
32    </div>
33  );
34}
35
36export default function SkillsSection({
37  skills,
38  onEndorse
39}: {
40  skills: Skill[];
41  onEndorse: (skillId: string) => void;
42}) {
43  const [endorsedSkills, setEndorsedSkills] = useState<Set<string>>(new Set());
44
45  const handleEndorse = (skillId: string) => {
46    setEndorsedSkills(new Set([...endorsedSkills, skillId]));
47    onEndorse(skillId);
48  };
49
50  return (
51    <div className="bg-white rounded-lg shadow p-6 mb-4">
52      <div className="flex items-center justify-between mb-4">
53        <h2 className="text-2xl font-bold">Skills</h2>
54        <button className="text-blue-600 hover:underline font-semibold">
55          + Add skill
56        </button>
57      </div>
58
59      <div>
60        {skills.map(skill => (
61          <SkillItem
62            key={skill.id}
63            skill={skill}
64            onEndorse={handleEndorse}
65            isEndorsed={endorsedSkills.has(skill.id)}
66          />
67        ))}
68      </div>
69    </div>
70  );
71}

Recommendations Section

1function RecommendationCard({ recommendation }: { recommendation: Recommendation }) {
2  return (
3    <div className="py-4 border-b last:border-0">
4      <div className="flex gap-3 mb-3">
5        <img
6          src={recommendation.author.avatar}
7          alt={recommendation.author.name}
8          className="w-12 h-12 rounded-full"
9        />
10        <div>
11          <h3 className="font-bold">{recommendation.author.name}</h3>
12          <p className="text-sm text-gray-600">{recommendation.author.headline}</p>
13          <p className="text-xs text-gray-500 mt-1">
14            {recommendation.relationship}{' '}
15            {recommendation.date.toLocaleDateString('en-US', {
16              month: 'long',
17              year: 'numeric'
18            })}
19          </p>
20        </div>
21      </div>
22
23      <p className="text-gray-700 italic">" {recommendation.text} "</p>
24    </div>
25  );
26}
27
28export default function RecommendationsSection({
29  recommendations
30}: {
31  recommendations: Recommendation[];
32}) {
33  return (
34    <div className="bg-white rounded-lg shadow p-6 mb-4">
35      <div className="flex items-center justify-between mb-4">
36        <h2 className="text-2xl font-bold">
37          Recommendations
38          <span className="text-gray-600 font-normal text-lg ml-2">
39            ({recommendations.length})
40          </span>
41        </h2>
42        <button className="text-blue-600 hover:underline font-semibold">
43          Ask for recommendation
44        </button>
45      </div>
46
47      <div>
48        {recommendations.map(rec => (
49          <RecommendationCard key={rec.id} recommendation={rec} />
50        ))}
51      </div>
52    </div>
53  );
54}

Summary 🎓

Profile structure - comprehensive professional data ✅ Profile header - banner, avatar, headline, connections ✅ Experience section - job history with duration calculation ✅ Education section - degrees and schools ✅ Skills section - with endorsement buttons ✅ Recommendations - testimonials from connections ✅ Connect button - networking functionality ✅ About section - professional summary

In the next exercise we'll add job posts and the feed!

See you next time! 🚀

Go to CodeWorlds