···11+# Overview
22+'Lanyard' is a dedicated profile for researchers, built on the AT profile.
33+44+Researchers will use this as an alternative to the ORCID id.
55+66+# Technology Stack
77+* eslint
88+* nextjs (latest version)
99+* postcss
1010+* prettier
1111+* Relevant @atproto/* npm packages (search https://www.npmjs.com/search?q=%40atproto%2F )
1212+* tailwind (v4)
1313+* typescript
1414+1515+## Potential NPM Packages
1616+1717+> [!IMPORTANT]
1818+> These packages are listed as optional and should not be considered essential or mandatory.
1919+2020+* @atproto/api
2121+* @atproto/common
2222+* @atproto/identity
2323+* @atproto/lex-cli
2424+* @atproto/lexicon
2525+* @atproto/oauth-client-node
2626+* @atproto/sync
2727+* @atproto/syntax
2828+* @atproto/xrpc-server
2929+* cors
3030+* dotenv
3131+* types
3232+* uuid
3333+* zod
3434+3535+> [!IMPORTANT]
3636+> NEVER speculate on package version numbers. Always use 'latest' version in the package.json.
3737+3838+# Features:
3939+4040+## Account Creation and Sign-In
4141+Create accounts using your @bluesky account:
4242+4343+* Users can create an account with their DID (e.g. a bluesky handle), hosted on *any* PDS, securly using *Oauth* **only**
4444+* No email signup supported
4545+4646+## Researcher Profile
4747+4848+Display your managed data beautifully
4949+5050+- Mobile-first (for easy realworld networking)
5151+- "Follow on Bluesky" primary action
5252+- View profile link as QR Code (for easy sharing at conferences)
5353+<!-- - Broadcast via Bluetooth (advertist you) -->
5454+5555+## Manage Profile
5656+5757+"Build a rich user profile, designed for **Researchers**"
5858+5959+### Basics
6060+Manage your User Profile
6161+6262+* Avatar Photo (locked, added from authenticated account)
6363+* Description Text (locked, added from authenticated account)
6464+* Honorifics
6565+ * Add Doctor
6666+ * Add Professor
6767+* Location
6868+ * ISO Codes
6969+7070+### Affiliations
7171+Manage Professional Affiliations
7272+7373+Manage here means CRUD (create, read, update, remove).
7474+7575+* allow multiple
7676+* required start date
7777+* optional end date (marked as `current` if without end date)
7878+* optional mark as `primary` (max 1)
7979+8080+> [!IMPORTANT]
8181+> Use Ringgold or Grid for Organisation data
8282+8383+### Social Network Profiles
8484+Manage Social Network Profile Links
8585+8686+* Bluesky (Only 1 allowed)
8787+ * added from authenticated account
8888+ * cannot be edited/hidden/deleted
8989+* Twitter Profile (Only 1 allowed)
9090+ * can be created/edited/deleted
9191+* LinkedIn Profile (Only 1 allowed)
9292+ * can be created/edited/deleted
9393+* ResearchGate Profile (Only 1 allowed)
9494+ * can be created/edited/deleted
9595+* Google Scholar Profile (Only 1 allowed)
9696+ * can be created/edited/deleted
9797+* Semble Profile (Only 1 allowed)
9898+ * can be created/edited/deleted
9999+ * https://semble.so for details
100100+101101+### Web Links
102102+103103+Manage Web Links (up to 3)
104104+* can be created/edited/deleted
105105+106106+## Manage Scholarly Contributions
107107+108108+"Add your research to your profile, using DOIs"
109109+110110+* Add Research Links
111111+ * Type (e.g. Abstract, Poster, Paper, Conference Proceeding)
112112+ * No upper limit
113113+ * Add DOI only
114114+ * Metadata is collected from link destination
115115+116116+## Manage Academic Events
117117+Add your conference presentations
118118+119119+ * Type (e.g. Conference, Symposium, etc)
120120+ * Date of Event (as a single date, or a range)
121121+ * Add related Research (as Scholarly Contribution)
122122+ * Organiser (as Organisation)
123123+124124+# Typed Lexicons
125125+126126+* User
127127+* Location (for user, organisation)
128128+* Organisation (for Affiliation)
129129+* Social Network Profiles
130130+* Web Links
131131+* Work (for Scholarly Contributions)
132132+* Event
133133+134134+# Leverage Collections in PDS
135135+136136+Where possible and relevant, use data from collections in the PDS, such as
137137+138138+* app.bsky.actor.profile
139139+* app.bsky.graph.block
140140+* app.bsky.graph.follow
141141+* app.bsky.graph.verification
142142+143143+[Future development!!!] Where the user has a semble.so account
144144+* network.cosmik.card
145145+* network.cosmik.collection
146146+* network.cosmik.collectionLink
+74
.docs/ux.md
···11+The experience should be super simple, like creating a Linktree profile.
22+33+# User Journey
44+55+1. Landing Page (for promotion and prompts Create account / Sign in)
66+2. Create account / Sign in
77+3. Dashboard with overview of all features
88+4. Manage Profile
99+ 1. View Profile as Owner
1010+ 2. View Profile as Visitor
1111+ 3. Edit Profile Details: Edit basic info about yourself (as researcher)
1212+ 4. Customise Profile: Basic styling options (out of scop for MVP!!!)
1313+ 5. Share profile
1414+ 1. View link as QR Code
1515+ 2. Copy link to clipboard
1616+5. Manage Research Links
1717+ 1. View 'All Research' (with Zero Data State)
1818+ 2. Add Research: Add a DOI, and system uses CrossRef API to grab title, abstract, authors, publication details etc.
1919+ 3. Import from ORCID (out of scop for MVP!!!)
2020+ 4. Import from Google Scholar Profile (out of scop for MVP!!!)
2121+6. Manage Events
2222+ 1. View 'All Events' (with Zero Data State)
2323+ 2. Add Event: Add upcoming/past conferences
2424+7. Manage WebLinks
2525+ 1. View 'All WebLinks' (with Zero Data State)
2626+ 2. Add WebLinks Form: inc social media profiles
2727+ 3.
2828+8. Share profile
2929+ 1. accessible from Dashboard, copy to profile
3030+ 2. from profile, visible to allV
3131+ 1. view as QR code
3232+3333+# url structure
3434+3535+> [!IMPORTANT]
3636+> State is always preserved in the URL
3737+3838+- landing page =
3939+ - domain root = https://lanyard.at
4040+- auth
4141+ - on a path
4242+ - https://lanyard.at/auth
4343+- dashboard =
4444+ - on a subdomain =
4545+ - https://app.lanyard.at
4646+- view content =
4747+ - on a path, in the subdomain
4848+ - e.g. https://app.lanyard.at/weblinks
4949+- edit content =
5050+ - on a path, in the subdomain
5151+ - e.g. https://app.lanyard.at/weblinks/edit?ID=someID
5252+ - e.g. https://app.lanyard.at/weblinks/create
5353+- profile =
5454+ - path based on [handle]
5555+ - https://lanyard.at/[handle]
5656+ - https://lanyard.at/@renderg.host
5757+ - https://lanyard.at/@alice.bsky.social
5858+ - potentially different actions available for authenticated users on their own profile
5959+6060+# Responsiveness
6161+6262+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).
6363+6464+> [!IMPORTANT]
6565+> Desktop breakpoints are not important in the MVP!!!
6666+>
6767+> [!NOTE]
6868+> Focus on the 'sm': '640px' tailwind breakpoint and below!
6969+7070+* Landing Page = Mobile First
7171+* Dashboard = Mobile First
7272+* Forms + Flows = Mobile First
7373+* Public Profile = Mobile First
7474+
···11import { redirect } from 'next/navigation';
22import { getSession } from '@/lib/auth/session';
33import { getAgent } from '@/lib/auth/atproto';
44-import { ResearcherRepository } from '@/lib/data/repository';
44+import { ProfileRepository } from '@/lib/data/repository';
55import Link from 'next/link';
66import ShareProfileButton from '@/components/profile/ShareProfileButton';
77···1818 redirect('/auth');
1919 }
20202121- const repo = new ResearcherRepository(agent);
2121+ const repo = new ProfileRepository(agent);
22222323 try {
2424 // Get profile
+16-6
src/app/dashboard/profile/edit/page.tsx
···11import { redirect } from 'next/navigation';
22import { getSession } from '@/lib/auth/session';
33import { getAgent } from '@/lib/auth/atproto';
44-import { ResearcherRepository } from '@/lib/data/repository';
44+import { ProfileRepository } from '@/lib/data/repository';
55import ProfileForm from '@/components/profile/ProfileForm';
66import Link from 'next/link';
77···1818 redirect('/auth');
1919 }
20202121- const repo = new ResearcherRepository(agent);
2121+ const repo = new ProfileRepository(agent);
2222 const profile = await repo.getProfile(session.did);
23232424 if (!profile) {
···4949 </svg>
5050 Back
5151 </Link>
5252- <h1 className="text-xl font-bold">Edit Profile</h1>
5353- <p className="text-sm text-gray-600 mt-1">
5454- Update your researcher profile information
5555- </p>
5252+ <div className="flex items-center justify-between">
5353+ <div>
5454+ <h1 className="text-xl font-bold">Edit Profile</h1>
5555+ <p className="text-sm text-gray-600 mt-1">
5656+ Update your researcher profile information
5757+ </p>
5858+ </div>
5959+ <Link
6060+ href={`/${profile.handle}`}
6161+ 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"
6262+ >
6363+ View Profile
6464+ </Link>
6565+ </div>
5666 </div>
5767 </div>
5868
+2-2
src/app/dashboard/research/create/page.tsx
···3434 </svg>
3535 Back
3636 </Link>
3737- <h1 className="text-xl font-bold">Add Publication</h1>
3737+ <h1 className="text-xl font-bold">Add Research</h1>
3838 <p className="text-sm text-gray-600 mt-1">
3939- Enter a DOI to add a publication to your profile
3939+ Enter a DOI to add research to your profile
4040 </p>
4141 </div>
4242 </div>
+10-45
src/app/dashboard/research/edit/page.tsx
···11import { redirect } from 'next/navigation';
22import { getSession } from '@/lib/auth/session';
33import { getAgent } from '@/lib/auth/atproto';
44-import { ResearcherRepository } from '@/lib/data/repository';
55-import ResearchForm from '@/components/research/ResearchForm';
66-import Link from 'next/link';
44+import { ProfileRepository } from '@/lib/data/repository';
55+import EditResearchClient from '@/components/research/EditResearchClient';
7687export default async function EditResearchPage({
98 searchParams,
109}: {
1111- searchParams: { doi?: string };
1010+ searchParams: Promise<{ rkey?: string }>;
1211}) {
1312 const session = await getSession();
1413···1615 redirect('/auth');
1716 }
18171919- if (!searchParams.doi) {
1818+ const params = await searchParams;
1919+2020+ if (!params.rkey) {
2021 redirect('/dashboard/research');
2122 }
2223···2627 redirect('/auth');
2728 }
28292929- const repo = new ResearcherRepository(agent);
3030+ const repo = new ProfileRepository(agent);
3031 const works = await repo.listWorks(session.did);
31323232- // Find the work with the matching DOI
3333- const work = works.find((w) => w.doi === searchParams.doi);
3333+ // Find the work with the matching rkey
3434+ const work = works.find((w) => w.rkey === params.rkey);
34353536 if (!work) {
3637 redirect('/dashboard/research');
3738 }
38393939- return (
4040- <main className="min-h-screen bg-gray-50">
4141- {/* Header */}
4242- <div className="bg-white border-b border-gray-200">
4343- <div className="max-w-2xl mx-auto px-4 py-4">
4444- <Link
4545- href="/dashboard/research"
4646- className="inline-flex items-center text-sm text-gray-600 hover:text-gray-900 mb-2"
4747- >
4848- <svg
4949- className="w-4 h-4 mr-1"
5050- fill="none"
5151- stroke="currentColor"
5252- viewBox="0 0 24 24"
5353- >
5454- <path
5555- strokeLinecap="round"
5656- strokeLinejoin="round"
5757- strokeWidth={2}
5858- d="M15 19l-7-7 7-7"
5959- />
6060- </svg>
6161- Back
6262- </Link>
6363- <h1 className="text-xl font-bold">Edit Publication</h1>
6464- <p className="text-sm text-gray-600 mt-1">
6565- Update publication details
6666- </p>
6767- </div>
6868- </div>
6969-7070- {/* Content */}
7171- <div className="max-w-2xl mx-auto px-4 py-6">
7272- <ResearchForm mode="edit" initialData={work} />
7373- </div>
7474- </main>
7575- );
4040+ return <EditResearchClient work={work} />;
7641}
···44 */
5566import type {
77- AtLanyardResearcher,
77+ AtLanyardProfile,
88+ AtLanyardAffiliation,
89 AtLanyardWork,
910 AtLanyardEvent,
1011 AtLanyardLink,
···1415} from './generated';
15161617// Main record types
1717-export type Researcher = AtLanyardResearcher.Record;
1818+export type Profile = AtLanyardProfile.Record;
1919+export type Affiliation = AtLanyardAffiliation.Record;
1820export type Work = AtLanyardWork.Record;
1921export type Event = AtLanyardEvent.Record;
2022export type Link = AtLanyardLink.Record;
···2224export type Publication = AtLanyardPublication.Main;
2325export type Location = AtLanyardLocation.Main;
24262525-// Nested types
2626-export type Affiliation = AtLanyardResearcher.Affiliation;
2727-2827// Convenience type aliases for enums and unions
2929-export type Honorific = 'Dr' | 'Prof';
2828+export type Honorific = 'none' | 'Dr' | 'Prof';
3029export type WorkType = Work['type'];
3130export type EventType = Event['type'];
3231export type LinkType = Link['type'];