Witaj @name! Czas stworzyć Crew Connections - profesjonalną sieć dla piratów szukających załogi i współpracy. 💼
To będzie jak LinkedIn - piraci prezentują swoje umiejętności, doświadczenie na morzu i szukają pozycji w załogach.
Twitter/Instagram/YouTube:
LinkedIn:
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}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}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}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}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}✅ 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
W następnym ćwiczeniu dodamy job posts i feed!
Do zobaczenia! 🚀