···1+# Overview
2+'Lanyard' is a dedicated profile for researchers, built on the AT profile.
3+4+Researchers will use this as an alternative to the ORCID id.
5+6+# Technology Stack
7+* eslint
8+* nextjs (latest version)
9+* postcss
10+* prettier
11+* Relevant @atproto/* npm packages (search https://www.npmjs.com/search?q=%40atproto%2F )
12+* tailwind (v4)
13+* typescript
14+15+## Potential NPM Packages
16+17+> [!IMPORTANT]
18+> These packages are listed as optional and should not be considered essential or mandatory.
19+20+* @atproto/api
21+* @atproto/common
22+* @atproto/identity
23+* @atproto/lex-cli
24+* @atproto/lexicon
25+* @atproto/oauth-client-node
26+* @atproto/sync
27+* @atproto/syntax
28+* @atproto/xrpc-server
29+* cors
30+* dotenv
31+* types
32+* uuid
33+* zod
34+35+> [!IMPORTANT]
36+> NEVER speculate on package version numbers. Always use 'latest' version in the package.json.
37+38+# Features:
39+40+## Account Creation and Sign-In
41+Create accounts using your @bluesky account:
42+43+* Users can create an account with their DID (e.g. a bluesky handle), hosted on *any* PDS, securly using *Oauth* **only**
44+* No email signup supported
45+46+## Researcher Profile
47+48+Display your managed data beautifully
49+50+- Mobile-first (for easy realworld networking)
51+- "Follow on Bluesky" primary action
52+- View profile link as QR Code (for easy sharing at conferences)
53+<!-- - Broadcast via Bluetooth (advertist you) -->
54+55+## Manage Profile
56+57+"Build a rich user profile, designed for **Researchers**"
58+59+### Basics
60+Manage your User Profile
61+62+* Avatar Photo (locked, added from authenticated account)
63+* Description Text (locked, added from authenticated account)
64+* Honorifics
65+ * Add Doctor
66+ * Add Professor
67+* Location
68+ * ISO Codes
69+70+### Affiliations
71+Manage Professional Affiliations
72+73+Manage here means CRUD (create, read, update, remove).
74+75+* allow multiple
76+* required start date
77+* optional end date (marked as `current` if without end date)
78+* optional mark as `primary` (max 1)
79+80+> [!IMPORTANT]
81+> Use Ringgold or Grid for Organisation data
82+83+### Social Network Profiles
84+Manage Social Network Profile Links
85+86+* Bluesky (Only 1 allowed)
87+ * added from authenticated account
88+ * cannot be edited/hidden/deleted
89+* Twitter Profile (Only 1 allowed)
90+ * can be created/edited/deleted
91+* LinkedIn Profile (Only 1 allowed)
92+ * can be created/edited/deleted
93+* ResearchGate Profile (Only 1 allowed)
94+ * can be created/edited/deleted
95+* Google Scholar Profile (Only 1 allowed)
96+ * can be created/edited/deleted
97+* Semble Profile (Only 1 allowed)
98+ * can be created/edited/deleted
99+ * https://semble.so for details
100+101+### Web Links
102+103+Manage Web Links (up to 3)
104+* can be created/edited/deleted
105+106+## Manage Scholarly Contributions
107+108+"Add your research to your profile, using DOIs"
109+110+* Add Research Links
111+ * Type (e.g. Abstract, Poster, Paper, Conference Proceeding)
112+ * No upper limit
113+ * Add DOI only
114+ * Metadata is collected from link destination
115+116+## Manage Academic Events
117+Add your conference presentations
118+119+ * Type (e.g. Conference, Symposium, etc)
120+ * Date of Event (as a single date, or a range)
121+ * Add related Research (as Scholarly Contribution)
122+ * Organiser (as Organisation)
123+124+# Typed Lexicons
125+126+* User
127+* Location (for user, organisation)
128+* Organisation (for Affiliation)
129+* Social Network Profiles
130+* Web Links
131+* Work (for Scholarly Contributions)
132+* Event
133+134+# Leverage Collections in PDS
135+136+Where possible and relevant, use data from collections in the PDS, such as
137+138+* app.bsky.actor.profile
139+* app.bsky.graph.block
140+* app.bsky.graph.follow
141+* app.bsky.graph.verification
142+143+[Future development!!!] Where the user has a semble.so account
144+* network.cosmik.card
145+* network.cosmik.collection
146+* network.cosmik.collectionLink
···1+The experience should be super simple, like creating a Linktree profile.
2+3+# User Journey
4+5+1. Landing Page (for promotion and prompts Create account / Sign in)
6+2. Create account / Sign in
7+3. Dashboard with overview of all features
8+4. Manage Profile
9+ 1. View Profile as Owner
10+ 2. View Profile as Visitor
11+ 3. Edit Profile Details: Edit basic info about yourself (as researcher)
12+ 4. Customise Profile: Basic styling options (out of scop for MVP!!!)
13+ 5. Share profile
14+ 1. View link as QR Code
15+ 2. Copy link to clipboard
16+5. Manage Research Links
17+ 1. View 'All Research' (with Zero Data State)
18+ 2. Add Research: Add a DOI, and system uses CrossRef API to grab title, abstract, authors, publication details etc.
19+ 3. Import from ORCID (out of scop for MVP!!!)
20+ 4. Import from Google Scholar Profile (out of scop for MVP!!!)
21+6. Manage Events
22+ 1. View 'All Events' (with Zero Data State)
23+ 2. Add Event: Add upcoming/past conferences
24+7. Manage WebLinks
25+ 1. View 'All WebLinks' (with Zero Data State)
26+ 2. Add WebLinks Form: inc social media profiles
27+ 3.
28+8. Share profile
29+ 1. accessible from Dashboard, copy to profile
30+ 2. from profile, visible to allV
31+ 1. view as QR code
32+33+# url structure
34+35+> [!IMPORTANT]
36+> State is always preserved in the URL
37+38+- landing page =
39+ - domain root = https://lanyard.at
40+- auth
41+ - on a path
42+ - https://lanyard.at/auth
43+- dashboard =
44+ - on a subdomain =
45+ - https://app.lanyard.at
46+- view content =
47+ - on a path, in the subdomain
48+ - e.g. https://app.lanyard.at/weblinks
49+- edit content =
50+ - on a path, in the subdomain
51+ - e.g. https://app.lanyard.at/weblinks/edit?ID=someID
52+ - e.g. https://app.lanyard.at/weblinks/create
53+- profile =
54+ - path based on [handle]
55+ - https://lanyard.at/[handle]
56+ - https://lanyard.at/@renderg.host
57+ - https://lanyard.at/@alice.bsky.social
58+ - potentially different actions available for authenticated users on their own profile
59+60+# Responsiveness
61+62+The majority of users will use this from their phone, so keep design single-column and user cards, not tables, for lists of objects (e.g. list of research works).
63+64+> [!IMPORTANT]
65+> Desktop breakpoints are not important in the MVP!!!
66+>
67+> [!NOTE]
68+> Focus on the 'sm': '640px' tailwind breakpoint and below!
69+70+* Landing Page = Mobile First
71+* Dashboard = Mobile First
72+* Forms + Flows = Mobile First
73+* Public Profile = Mobile First
74+
···1import { redirect } from 'next/navigation';
2import { getSession } from '@/lib/auth/session';
3import { getAgent } from '@/lib/auth/atproto';
4-import { ResearcherRepository } from '@/lib/data/repository';
5import Link from 'next/link';
6import ShareProfileButton from '@/components/profile/ShareProfileButton';
7···18 redirect('/auth');
19 }
2021- const repo = new ResearcherRepository(agent);
2223 try {
24 // Get profile
···1import { redirect } from 'next/navigation';
2import { getSession } from '@/lib/auth/session';
3import { getAgent } from '@/lib/auth/atproto';
4+import { ProfileRepository } from '@/lib/data/repository';
5import Link from 'next/link';
6import ShareProfileButton from '@/components/profile/ShareProfileButton';
7···18 redirect('/auth');
19 }
2021+ const repo = new ProfileRepository(agent);
2223 try {
24 // Get profile
+16-6
src/app/dashboard/profile/edit/page.tsx
···1import { redirect } from 'next/navigation';
2import { getSession } from '@/lib/auth/session';
3import { getAgent } from '@/lib/auth/atproto';
4-import { ResearcherRepository } from '@/lib/data/repository';
5import ProfileForm from '@/components/profile/ProfileForm';
6import Link from 'next/link';
7···18 redirect('/auth');
19 }
2021- const repo = new ResearcherRepository(agent);
22 const profile = await repo.getProfile(session.did);
2324 if (!profile) {
···49 </svg>
50 Back
51 </Link>
52- <h1 className="text-xl font-bold">Edit Profile</h1>
53- <p className="text-sm text-gray-600 mt-1">
54- Update your researcher profile information
55- </p>
000000000056 </div>
57 </div>
58
···1import { redirect } from 'next/navigation';
2import { getSession } from '@/lib/auth/session';
3import { getAgent } from '@/lib/auth/atproto';
4+import { ProfileRepository } from '@/lib/data/repository';
5import ProfileForm from '@/components/profile/ProfileForm';
6import Link from 'next/link';
7···18 redirect('/auth');
19 }
2021+ const repo = new ProfileRepository(agent);
22 const profile = await repo.getProfile(session.did);
2324 if (!profile) {
···49 </svg>
50 Back
51 </Link>
52+ <div className="flex items-center justify-between">
53+ <div>
54+ <h1 className="text-xl font-bold">Edit Profile</h1>
55+ <p className="text-sm text-gray-600 mt-1">
56+ Update your researcher profile information
57+ </p>
58+ </div>
59+ <Link
60+ href={`/${profile.handle}`}
61+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
62+ >
63+ View Profile
64+ </Link>
65+ </div>
66 </div>
67 </div>
68
+2-2
src/app/dashboard/research/create/page.tsx
···34 </svg>
35 Back
36 </Link>
37- <h1 className="text-xl font-bold">Add Publication</h1>
38 <p className="text-sm text-gray-600 mt-1">
39- Enter a DOI to add a publication to your profile
40 </p>
41 </div>
42 </div>
···34 </svg>
35 Back
36 </Link>
37+ <h1 className="text-xl font-bold">Add Research</h1>
38 <p className="text-sm text-gray-600 mt-1">
39+ Enter a DOI to add research to your profile
40 </p>
41 </div>
42 </div>
···4 */
56import type {
7- AtLanyardResearcher,
08 AtLanyardWork,
9 AtLanyardEvent,
10 AtLanyardLink,
···14} from './generated';
1516// Main record types
17-export type Researcher = AtLanyardResearcher.Record;
018export type Work = AtLanyardWork.Record;
19export type Event = AtLanyardEvent.Record;
20export type Link = AtLanyardLink.Record;
···22export type Publication = AtLanyardPublication.Main;
23export type Location = AtLanyardLocation.Main;
2425-// Nested types
26-export type Affiliation = AtLanyardResearcher.Affiliation;
27-28// Convenience type aliases for enums and unions
29-export type Honorific = 'Dr' | 'Prof';
30export type WorkType = Work['type'];
31export type EventType = Event['type'];
32export type LinkType = Link['type'];
···4 */
56import type {
7+ AtLanyardProfile,
8+ AtLanyardAffiliation,
9 AtLanyardWork,
10 AtLanyardEvent,
11 AtLanyardLink,
···15} from './generated';
1617// Main record types
18+export type Profile = AtLanyardProfile.Record;
19+export type Affiliation = AtLanyardAffiliation.Record;
20export type Work = AtLanyardWork.Record;
21export type Event = AtLanyardEvent.Record;
22export type Link = AtLanyardLink.Record;
···24export type Publication = AtLanyardPublication.Main;
25export type Location = AtLanyardLocation.Main;
2600027// Convenience type aliases for enums and unions
28+export type Honorific = 'none' | 'Dr' | 'Prof';
29export type WorkType = Work['type'];
30export type EventType = Event['type'];
31export type LinkType = Link['type'];