├── .eslintrc.cjs
├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE
├── README.md
├── example-assets
├── banner.png
├── developer-badge.png
├── html.webp
├── minecraft.png
├── pfp.webp
└── pickaxe.webp
├── github
├── card-preview-v2.png
├── card-preview.png
├── game-example.png
├── member-since-i18n.png
└── spotify-example.png
├── index.html
├── package-lock.json
├── package.json
├── src
├── App.css
├── App.tsx
├── components
│ ├── Role.tsx
│ ├── Separator.tsx
│ ├── base-discord-card.tsx
│ ├── discord-card.tsx
│ ├── discord-link.tsx
│ ├── lanyard-discord-card.tsx
│ ├── section-title.tsx
│ ├── sections
│ │ ├── about-me.tsx
│ │ ├── activity.tsx
│ │ ├── badge.tsx
│ │ ├── base.tsx
│ │ ├── basic-info.tsx
│ │ ├── member-since.tsx
│ │ ├── message.tsx
│ │ ├── note.tsx
│ │ ├── role.tsx
│ │ ├── spotify.tsx
│ │ └── status.tsx
│ ├── seek-bar.tsx
│ └── spotify-logo.tsx
├── helpers
│ └── helper-functions.ts
├── hooks
│ └── useAutosizeTextArea.ts
├── index.ts
├── main.tsx
├── styles
│ ├── AboutMeSection.module.css
│ ├── ActivitySection.module.css
│ ├── BadgeSection.module.css
│ ├── BaseDiscordCard.module.css
│ ├── BasicInfoSection.module.css
│ ├── DiscordCardPreflight.module.css
│ ├── DiscordLink.module.css
│ ├── MemberSinceSection.module.css
│ ├── MessageSection.module.css
│ ├── NoteSection.module.css
│ ├── Role.module.css
│ ├── RoleSection.module.css
│ ├── SectionTitle.module.css
│ ├── SeekBar.module.css
│ ├── Separator.module.css
│ ├── SpotifySection.module.css
│ └── StatusSection.module.css
├── types.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example-assets
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @miguelhigueradev:registry=https://npm.pkg.github.com
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Miguel Higuera
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # discord-card-react
2 |
3 | A React Component that renders an (old) Discord profile card. Can sync with your Discord status in real time.
4 |
5 | Live Demo
6 |
7 |
8 |
9 |
10 |
11 | ## Table of Contents:
12 |
13 | 1. [Features](#features)
14 | 2. [How to use](#how-to-use)
15 | 3. [State and input handler example](#state-and-input-handler-example)
16 | 4. [Translations (i18n)](#translations-i18n)
17 | 5. [How to contribute](#how-to-contribute)
18 | 6. [Credits](#credits)
19 |
20 | ## Features:
21 |
22 | - 😀 Easy to use
23 | - 📄 Supports all the features that the real Discord card supports (color gradient, badges, Spotify, and more)
24 | - ⚙️ Highly modular and customizable
25 | - 🏷️ Integrates with [Lanyard](https://github.com/Phineas/lanyard) to sync your _real_ Discord status with this component
26 | - ♿ Accessible
27 | - 🌐 I18n friendly (can translate it to any language)
28 |
29 | ## How to use
30 |
31 | **Requires NodeJS 18 or newer.**
32 |
33 | Install the package with your package manager of choice. Example:
34 |
35 | `npm install discord-card-react`
36 |
37 | Import the card you want to use and the styles:
38 |
39 | ```
40 | // Static card, doesn't sync with your real Discord status
41 | import { DiscordCard } from "discord-card-react";
42 |
43 | // or
44 |
45 | // Dynamic card, uses Lanyard to sync with your Discord status
46 | import { LanyardDiscordCard } from "discord-card-react";
47 |
48 | // DON'T FORGET TO ADD THE STYLES OR THE CARD WILL LOOK UGLY!
49 | import "discord-card-react/styles";
50 | ```
51 |
52 | > [!IMPORTANT]
53 | > Remember to import the styles using import "discord-card-react/styles";
54 |
55 | **Note**: The component now includes scoped Tailwind preflight styles to ensure consistent styling across different environments. These are automatically applied and won't affect your global styles.
56 |
57 | ### Styling and CSS Reset
58 |
59 | The Discord card component includes built-in CSS reset/preflight styles that are automatically scoped to the component. This ensures consistent styling without affecting your application's global styles.
60 |
61 | After importing, copy one of the templates below to add the card(s).
62 |
63 | ### Static Card (\)
64 |
65 | Pass props to it to customize. Like the name says, it's static and doesn't update dynamically based on your real Discord status. **If you want to add a dynamic card that updates with your status, check the next template below.**
66 |
67 | ```js
68 |
160 | ```
161 |
162 | #### Spotify preview
163 |
164 | 
165 |
166 | #### Game preview
167 |
168 | 
169 |
170 | ### Lanyard Card (\)
171 |
172 | Pass your Discord ID as a prop to automatically update the card's status and Spotify/Game sections using [Lanyard](https://github.com/Phineas/lanyard) in WebSocket mode.
173 |
174 | Make sure to set up Lanyard by following [the instructions](https://github.com/Phineas/lanyard) (extremely easy).
175 |
176 | ```js
177 |
266 | ```
267 |
268 | ## State and input handler example
269 |
270 | This is an example on how to implement basic state and input handling for the message and note fields, so you can retrieve its value.
271 |
272 | ```js
273 | const [note, setNote] = useState("");
274 | const [message, setMessage] = useState("");
275 |
276 | function handleNoteChange(event) {
277 | setNote(event.target.value);
278 | }
279 |
280 | function handleMessageChange(event) {
281 | setMessage(event.target.value);
282 | }
283 |
284 | ;
298 | ```
299 |
300 | ## Translations (i18n)
301 |
302 | You can translate all the sections that have titles by passing a `title` prop. (**About Me, Member Since, Playing a Game, Listening to Spotify, Roles, Note**). This will override the default title.
303 |
304 | The Spotify and Game sections provide additional options (check the examples in [How to use!](#how-to-use))
305 |
306 | For example, translating the Member Since section to Spanish:
307 |
308 | ```js
309 | memberSince={{
310 | discordJoinDate: "20 Jul 2016",
311 | title: "Miembro desde",
312 | serverJoinDate: "1 Sep 2020",
313 | serverIconUrl: "https://asdasd.com/icon.png",
314 | serverName: "Servidor X"
315 | }}
316 | ```
317 |
318 | 
319 |
320 | ## How to contribute
321 |
322 | All contributions are greatly appreciated! Please keep PRs short, focusing only in one feature/fix you want to implement, so they can be easily reviewed.
323 |
324 | Follow these instructions if you are a newcomer:
325 |
326 | 1. Fork the repository and clone it
327 | 2. Make a new branch using the command `git switch -c BranchName`
328 | 3. Install dependencies using your package manager of choice (for example: `npm install`)
329 | 4. Start the development server to see your changes live using `npm run dev`
330 | 5. Once you have finished your work, commit your changes and [open a PR!](https://github.com/MiguelHigueraDev/discord-card-react/pulls)
331 |
332 | ## Credits
333 |
334 | Uses [react-use-lanyard](https://www.npmjs.com/package/react-use-lanyard).
335 |
--------------------------------------------------------------------------------
/example-assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/banner.png
--------------------------------------------------------------------------------
/example-assets/developer-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/developer-badge.png
--------------------------------------------------------------------------------
/example-assets/html.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/html.webp
--------------------------------------------------------------------------------
/example-assets/minecraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/minecraft.png
--------------------------------------------------------------------------------
/example-assets/pfp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/pfp.webp
--------------------------------------------------------------------------------
/example-assets/pickaxe.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/example-assets/pickaxe.webp
--------------------------------------------------------------------------------
/github/card-preview-v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/github/card-preview-v2.png
--------------------------------------------------------------------------------
/github/card-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/github/card-preview.png
--------------------------------------------------------------------------------
/github/game-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/github/game-example.png
--------------------------------------------------------------------------------
/github/member-since-i18n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/github/member-since-i18n.png
--------------------------------------------------------------------------------
/github/spotify-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiguelHigueraDev/discord-card-react/9be877a44a87c343d873c2a686802d5186a6bef0/github/spotify-example.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Discord Card
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord-card-react",
3 | "description": "React Discord profile card component",
4 | "type": "module",
5 | "private": false,
6 | "version": "2.2.1",
7 | "license": "MIT",
8 | "main": "dist/index.umd.mjs",
9 | "module": "dist/index.es.mjs",
10 | "types": "dist/index.d.ts",
11 | "keywords": [
12 | "react",
13 | "discord",
14 | "profile",
15 | "card",
16 | "component",
17 | "lanyard"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/MiguelHigueraDev/discord-card-react.git"
22 | },
23 | "exports": {
24 | ".": {
25 | "types": "./dist/index.d.ts",
26 | "import": "./dist/index.es.mjs",
27 | "require": "./dist/index.umd.mjs"
28 | },
29 | "./styles": "./dist/style.css"
30 | },
31 | "files": [
32 | "/dist"
33 | ],
34 | "publishConfig": {
35 | "access": "public"
36 | },
37 | "scripts": {
38 | "dev": "vite",
39 | "build": "tsc && vite build",
40 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
41 | "preview": "vite preview"
42 | },
43 | "dependencies": {
44 | "react": "^18.2.0",
45 | "react-dom": "^18.2.0",
46 | "react-use-lanyard": "^0.3.1"
47 | },
48 | "devDependencies": {
49 | "@types/node": "^20.11.28",
50 | "@types/react": "^18.2.64",
51 | "@types/react-dom": "^18.2.21",
52 | "@typescript-eslint/eslint-plugin": "^7.1.1",
53 | "@typescript-eslint/parser": "^7.1.1",
54 | "@vitejs/plugin-react": "^4.2.1",
55 | "eslint": "^8.57.0",
56 | "eslint-plugin-react-hooks": "^4.6.0",
57 | "eslint-plugin-react-refresh": "^0.4.5",
58 | "typescript": "^5.2.2",
59 | "vite": "^5.1.6",
60 | "vite-plugin-dts": "^3.7.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .main-container {
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | gap: 20px;
11 | justify-content: center;
12 | align-items: center;
13 | min-height: 100vh;
14 | background-color: #292929;
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { useState } from "react";
3 | import DiscordCard from "./components/discord-card";
4 |
5 | // This is an implementation example using Lanyard.
6 | function App() {
7 | const [note, setNote] = useState("");
8 | const [message, setMessage] = useState("");
9 |
10 | function handleNoteChange(event: React.ChangeEvent) {
11 | setNote(event.target.value);
12 | }
13 |
14 | function handleMessageChange(event: React.ChangeEvent) {
15 | setMessage(event.target.value);
16 | }
17 |
18 | return (
19 |
103 | );
104 | }
105 |
106 | export default App;
107 |
--------------------------------------------------------------------------------
/src/components/Role.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Renders a role component with the specified role and color.
3 | *
4 | * @param {string} role - The role to be displayed
5 | * @param {string} color - The color of the role icon
6 | * @return {JSX.Element} The role component JSX
7 | */
8 | import styles from "../styles/Role.module.css";
9 |
10 | const Role = ({ role, color }: { role: string; color: string }) => {
11 | return (
12 |
13 |
14 | {role}
15 |
16 | );
17 | };
18 |
19 | export default Role;
20 |
--------------------------------------------------------------------------------
/src/components/Separator.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles/Separator.module.css";
2 |
3 | /**
4 | * Function that returns a JSX element representing a separator (Discord style).
5 | *
6 | * @return {JSX.Element} JSX element representing a separator
7 | */
8 | const Separator = () => {
9 | return
;
10 | };
11 |
12 | export default Separator;
13 |
--------------------------------------------------------------------------------
/src/components/base-discord-card.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles/BaseDiscordCard.module.css";
2 | import preflightStyles from "../styles/DiscordCardPreflight.module.css";
3 | import BadgeSection from "./sections/badge";
4 | import { Badge, ConnectionStatus } from "../types";
5 |
6 | const BaseDiscordCard = ({
7 | imageUrl,
8 | bannerUrl,
9 | primaryColor,
10 | accentColor,
11 | badges,
12 | connectionStatus = "online",
13 | children,
14 | }: {
15 | imageUrl: string;
16 | bannerUrl: string;
17 | primaryColor: string;
18 | accentColor: string;
19 | badges?: Badge[];
20 | connectionStatus?: ConnectionStatus;
21 | children: React.JSX.Element | React.JSX.Element[];
22 | }) => {
23 | return (
24 |
25 |
31 |
32 |
33 |
38 |
44 |
50 |
51 |
52 |
53 |
67 |
68 | {badges &&
}
69 |
70 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default BaseDiscordCard;
79 |
--------------------------------------------------------------------------------
/src/components/discord-card.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseDiscordCard from "./base-discord-card";
3 | import BasicInfoSection from "./sections/basic-info";
4 | import StatusSection from "./sections/status";
5 | import AboutMeSection from "./sections/about-me";
6 | import MemberSinceSection from "./sections/member-since";
7 | import RoleSection from "./sections/role";
8 | import NoteSection from "./sections/note";
9 | import MessageSection from "./sections/message";
10 | import SpotifySection from "./sections/spotify";
11 | import ActivitySection from "./sections/activity";
12 | import {
13 | BasicInfoSectionProps,
14 | ConnectionStatus,
15 | Badge,
16 | StatusSectionProps,
17 | AboutMeSectionProps,
18 | MemberSinceSectionProps,
19 | RoleSectionProps,
20 | NoteSectionProps,
21 | MessageSectionProps,
22 | SpotifySectionProps,
23 | ActivitySectionProps,
24 | } from "../types";
25 | import styles from "../styles/BaseDiscordCard.module.css";
26 | import Separator from "./separator";
27 |
28 | const DiscordCard = ({
29 | imageUrl,
30 | bannerUrl,
31 | primaryColor,
32 | accentColor,
33 | basicInfo,
34 | connectionStatus = "online",
35 | badges,
36 | status,
37 | aboutMe,
38 | memberSince,
39 | roles,
40 | note,
41 | message,
42 | spotify,
43 | activity,
44 | children,
45 | }: {
46 | imageUrl: string;
47 | bannerUrl: string;
48 | primaryColor: string;
49 | accentColor: string;
50 | basicInfo: BasicInfoSectionProps;
51 | connectionStatus?: ConnectionStatus;
52 | badges?: Badge[];
53 | status?: StatusSectionProps;
54 | aboutMe?: AboutMeSectionProps;
55 | memberSince?: MemberSinceSectionProps;
56 | roles?: RoleSectionProps;
57 | note?: NoteSectionProps;
58 | message?: MessageSectionProps;
59 | spotify?: SpotifySectionProps;
60 | activity?: ActivitySectionProps;
61 | children?: React.JSX.Element | React.JSX.Element[];
62 | }) => {
63 | return (
64 |
72 | <>
73 | <>
74 |
75 | <>{status == null && }>
76 | >
77 | {status && (
78 | <>
79 |
80 |
81 | >
82 | )}
83 |
84 | {aboutMe &&
}
85 | {memberSince &&
}
86 | {spotify && (
87 |
88 | )}
89 | {activity && (
90 |
91 | )}
92 | {roles &&
}
93 | {note &&
}
94 | {message &&
}
95 |
96 | >
97 | <>{children}>
98 |
99 | );
100 | };
101 |
102 | export default DiscordCard;
103 |
--------------------------------------------------------------------------------
/src/components/discord-link.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles/DiscordLink.module.css";
2 |
3 | /**
4 | * Renders a Discord link.
5 | *
6 | * @param {string} href - The URL of the link
7 | * @param {string} text - The text to display for the link, defaults to the link URL if not provided
8 | * @return {JSX.Element} The rendered Discord link component
9 | */
10 | const DiscordLink = ({
11 | href,
12 | text,
13 | }: {
14 | href: string;
15 | text?: string;
16 | }): JSX.Element => {
17 | return (
18 | <>
19 |
20 | {text ? text : href}
21 |
22 | >
23 | );
24 | };
25 |
26 | export default DiscordLink;
27 |
--------------------------------------------------------------------------------
/src/components/lanyard-discord-card.tsx:
--------------------------------------------------------------------------------
1 | import BaseDiscordCard from "./base-discord-card";
2 | import { Activity, useLanyard } from "react-use-lanyard";
3 | import BasicInfoSection from "./sections/basic-info";
4 | import StatusSection from "./sections/status";
5 | import AboutMeSection from "./sections/about-me";
6 | import Separator from "./separator";
7 | import MemberSinceSection from "./sections/member-since";
8 | import RoleSection from "./sections/role";
9 | import NoteSection from "./sections/note";
10 | import MessageSection from "./sections/message";
11 | import SpotifySection from "./sections/spotify";
12 | import ActivitySection from "./sections/activity";
13 | import { ReactNode } from "react";
14 | import {
15 | BasicInfoSectionProps,
16 | Badge,
17 | StatusSectionProps,
18 | AboutMeSectionProps,
19 | MemberSinceSectionProps,
20 | RoleSectionProps,
21 | NoteSectionProps,
22 | MessageSectionProps,
23 | LanyardActivitySectionProps,
24 | LanyardSpotifySectionProps,
25 | ActivityPriority,
26 | } from "../types";
27 | import styles from "../styles/BaseDiscordCard.module.css";
28 |
29 | const LanyardDiscordCard = ({
30 | userId,
31 | apiUrl,
32 | imageUrl,
33 | bannerUrl,
34 | primaryColor,
35 | accentColor,
36 | basicInfo,
37 | badges,
38 | status,
39 | aboutMe,
40 | memberSince,
41 | activity = {
42 | title: "Playing a game",
43 | show: true,
44 | showElapsedTime: true,
45 | timeElapsedText: "elapsed",
46 | timeAlignment: "left",
47 | },
48 | spotify = {
49 | show: true,
50 | title: "Listening to Spotify",
51 | buttonText: "Play on Spotify",
52 | byText: "by",
53 | onText: "on",
54 | },
55 | roles,
56 | note,
57 | message,
58 | priority = "default",
59 | maxActivities = 2,
60 | children,
61 | }: {
62 | userId: string;
63 | apiUrl?: string;
64 | imageUrl: string;
65 | bannerUrl: string;
66 | primaryColor: string;
67 | accentColor: string;
68 | basicInfo: BasicInfoSectionProps;
69 | badges?: Badge[];
70 | status?: StatusSectionProps;
71 | aboutMe?: AboutMeSectionProps;
72 | memberSince?: MemberSinceSectionProps;
73 | activity?: LanyardActivitySectionProps;
74 | spotify?: LanyardSpotifySectionProps;
75 | roles?: RoleSectionProps;
76 | note?: NoteSectionProps;
77 | message?: MessageSectionProps;
78 | priority?: ActivityPriority;
79 | maxActivities?: number;
80 | children?: React.JSX.Element | React.JSX.Element[];
81 | }) => {
82 | const { status: lanyardData } = useLanyard({
83 | userId,
84 | socket: true,
85 | apiUrl,
86 | });
87 |
88 | // Activities with type 0 are games
89 | const activities =
90 | lanyardData && lanyardData.activities
91 | ? lanyardData.activities
92 | .filter((ac) => ac.type === 0)
93 | .slice(0, maxActivities)
94 | : null;
95 |
96 | const renderSpotifySection = () => {
97 | if (spotify.show && lanyardData && lanyardData.spotify) {
98 | return (
99 |
113 | );
114 | }
115 | };
116 |
117 | /*
118 | * This handles external assets in case they are present to display the correct image
119 | */
120 | const getImageUrls = (activity: Activity) => {
121 | const largeImageId = activity.assets?.large_image;
122 | const smallImageId = activity.assets?.small_image;
123 | let largeImageExternalUrl = null;
124 | let smallImageExternalUrl = null;
125 |
126 | if (largeImageId?.startsWith("mp:external")) {
127 | const largeImageUrlParts = largeImageId.split("/");
128 | largeImageExternalUrl = largeImageUrlParts.slice(3).join("/");
129 | }
130 |
131 | if (smallImageId?.startsWith("mp:external")) {
132 | const smallImageUrlParts = smallImageId.split("/");
133 | smallImageExternalUrl = smallImageUrlParts.slice(3).join("/");
134 | }
135 |
136 | return {
137 | largeImageId,
138 | smallImageId,
139 | largeImageExternalUrl,
140 | smallImageExternalUrl,
141 | };
142 | };
143 |
144 | const renderActivitiesSection = (): ReactNode[] => {
145 | if (!activity.show || !activities || activities.length === 0) {
146 | return [];
147 | }
148 |
149 | return activities.map((currentActivity) => {
150 | const {
151 | largeImageId,
152 | smallImageId,
153 | largeImageExternalUrl,
154 | smallImageExternalUrl,
155 | } = getImageUrls(currentActivity);
156 |
157 | const largeImage = largeImageExternalUrl
158 | ? `http://${largeImageExternalUrl}`
159 | : currentActivity.assets?.large_image
160 | ? `https://cdn.discordapp.com/app-assets/${currentActivity.application_id}/${largeImageId}.webp?size=80`
161 | : undefined;
162 |
163 | const smallImage = smallImageExternalUrl
164 | ? `http://${smallImageExternalUrl}`
165 | : currentActivity.assets?.small_image
166 | ? `https://cdn.discordapp.com/app-assets/${currentActivity.application_id}/${smallImageId}.webp?size=80`
167 | : undefined;
168 |
169 | const party = currentActivity.party
170 | ? {
171 | // @ts-expect-error - TS doesn't know that size is an array
172 | currentSize: currentActivity.party.size?.[0] ?? null,
173 | // @ts-expect-error - TS doesn't know that size is an array
174 | maxSize: currentActivity.party.size?.[1] ?? null,
175 | }
176 | : undefined;
177 |
178 | const startTime =
179 | currentActivity.timestamps?.start && activity.showElapsedTime
180 | ? currentActivity.timestamps.start
181 | : undefined;
182 |
183 | const buttonText = currentActivity.buttons?.[0];
184 |
185 | return (
186 |
202 | );
203 | });
204 | };
205 |
206 | const renderSections = () => {
207 | if (priority === "spotify") {
208 | if (spotify.show && lanyardData && lanyardData.spotify) {
209 | return renderSpotifySection();
210 | }
211 | return renderActivitiesSection();
212 | } else if (priority === "activity" || priority === "default") {
213 | if (activity.show && activities && activities.length > 0) {
214 | return renderActivitiesSection();
215 | }
216 | return renderSpotifySection();
217 | } else {
218 | return (
219 | <>
220 | {renderSpotifySection()}
221 | {renderActivitiesSection()}
222 | >
223 | );
224 | }
225 | };
226 |
227 | return (
228 |
236 | <>
237 |
238 | <>{status == null && }>
239 | {status && (
240 | <>
241 |
242 |
243 | >
244 | )}
245 |
246 | {aboutMe &&
}
247 | {memberSince &&
}
248 | {renderSections()}
249 | {roles &&
}
250 | {note &&
}
251 | {message &&
}
252 |
253 | >
254 |
255 | <>{children}>
256 |
257 | );
258 | };
259 |
260 | export default LanyardDiscordCard;
261 |
--------------------------------------------------------------------------------
/src/components/section-title.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles/SectionTitle.module.css";
2 |
3 | const SectionTitle = ({
4 | title,
5 | marginBottom,
6 | }: {
7 | title: string;
8 | marginBottom?: number;
9 | }) => {
10 | return (
11 |
12 | {title}
13 |
14 | );
15 | };
16 |
17 | export default SectionTitle;
18 |
--------------------------------------------------------------------------------
/src/components/sections/about-me.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AboutMeItem } from "../../types";
3 | import SectionTitle from "../section-title";
4 | import styles from "../../styles/AboutMeSection.module.css";
5 |
6 | /**
7 | * Generates the About Me section with a title and optional items.
8 | *
9 | * @param {React.JSX.Element | React.JSX.Element[]} children - The content of the About Me section.
10 | * @param {string} title - The title of the About Me section.
11 | * @param {AboutMeItem[]} items - The items to display in the About Me section, instead of passing children to it.
12 | * @return {JSX.Element} The About Me section component.
13 | */
14 | const AboutMeSection = ({
15 | children,
16 | title,
17 | items,
18 | }: {
19 | children?: React.JSX.Element | React.JSX.Element[];
20 | title?: string;
21 | items?: AboutMeItem[];
22 | }) => {
23 | return (
24 |
25 |
26 | {items &&
27 | items.map((item, index) =>
28 | // Convert it into a link if href attribute is provided
29 | item.href ? (
30 |
37 | {item.text}
38 |
39 | ) : (
40 |
41 | {item.text}
42 |
43 | )
44 | )}
45 | <>{children}>
46 |
47 | );
48 | };
49 |
50 | export default AboutMeSection;
51 |
--------------------------------------------------------------------------------
/src/components/sections/activity.tsx:
--------------------------------------------------------------------------------
1 | import { formatTime } from "../../helpers/helper-functions";
2 | import { Party } from "../../types";
3 | import SectionTitle from "../section-title";
4 | import { useEffect, useState } from "react";
5 | import styles from "../../styles/ActivitySection.module.css";
6 |
7 | /**
8 | * Renders a section for displaying activity information.
9 | *
10 | * @param {string} title - The title of the activity
11 | * @param {string} name - The name of the activity
12 | * @param {string} state - The state of the activity
13 | * @param {string} details - Additional details about the activity
14 | * @param {string} largeImage - URL for the large image related to the activity
15 | * @param {string} smallImage - URL for the small image related to the activity
16 | * @param {Party} party - Object containing information about the party related to the activity
17 | * @param {string} elapsedText - The text to display before the elapsed time (default: elapsed)
18 | * @param {"left" | "right"} timeAlignment - The alignment of the elapsed time (default: left)
19 | * @param {number} startTime - The start time of the activity
20 | * @param {string} primaryColor - The color of the button (inherited from card)
21 | * @param {string} buttonText - The text to display on the button (default: null)
22 | * @return {JSX.Element} The rendered section component
23 | */
24 | const ActivitySection = ({
25 | title,
26 | name,
27 | state,
28 | details,
29 | largeImage,
30 | smallImage,
31 | party,
32 | elapsedText = "elapsed",
33 | timeAlignment = "left",
34 | startTime,
35 | buttonText,
36 | primaryColor,
37 | }: {
38 | title?: string;
39 | applicationId?: string;
40 | name?: string;
41 | state?: string;
42 | details?: string;
43 | largeImage?: string;
44 | smallImage?: string;
45 | party?: Party;
46 | elapsedText?: string;
47 | timeAlignment?: "left" | "right";
48 | startTime?: number;
49 | buttonText?: string;
50 | primaryColor?: string;
51 | }) => {
52 | const [currentDateTime, setCurrentDateTime] = useState(new Date());
53 |
54 | // Update to current time every second
55 | useEffect(() => {
56 | const interval = setInterval(() => setCurrentDateTime(new Date()), 1000);
57 | return () => clearInterval(interval);
58 | }, [startTime]);
59 |
60 | const elapsedTime = formatTime(startTime!, currentDateTime.getTime());
61 |
62 | return (
63 |
64 |
65 |
66 |
67 |
68 | {largeImage ? (
69 |
70 |
71 |
72 | {smallImage && (
73 |
78 | )}
79 |
80 |
81 | ) : (
82 | <>
83 | {smallImage && (
84 |
85 |
90 |
91 | )}
92 | >
93 | )}
94 |
95 | {name &&
{name}
}
96 | {details && (
97 |
98 | {details.length <= 30
99 | ? details
100 | : `${details.substring(0, 30)}...`}
101 |
102 | )}
103 | {state && (
104 | <>
105 | {party && party.currentSize && party.maxSize ? (
106 |
107 | {state.length <= 30
108 | ? `${state} (${party.currentSize}/${party.maxSize})`
109 | : `${state.substring(0, 30)}... (${party.currentSize}/${
110 | party.maxSize
111 | })`}
112 |
113 | ) : (
114 |
115 | {state.length <= 30 ? state : `${state.substring(0, 30)}...`}
116 |
117 | )}
118 | >
119 | )}
120 | {startTime && (
121 |
122 | {timeAlignment === "left"
123 | ? `${elapsedTime} ${elapsedText}`
124 | : `${elapsedText} ${elapsedTime}`}
125 |
126 | )}
127 |
128 |
129 | {buttonText && (
130 |
131 |
136 |
137 | {buttonText}
138 |
139 |
140 |
141 | )}
142 |
143 | );
144 | };
145 |
146 | export default ActivitySection;
147 |
--------------------------------------------------------------------------------
/src/components/sections/badge.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "../../types";
2 | import styles from "../../styles/BadgeSection.module.css";
3 |
4 | /**
5 | * Renders a list of badges in a container.
6 | *
7 | * @param {Badge[]} badges - An array of Badge objects
8 | * @return {JSX.Element} The list of badges rendered as JSX
9 | */
10 | const BadgeSection = ({ badges }: { badges: Badge[] }) => {
11 | return (
12 |
13 | {badges.map((badge) => (
14 |
15 |
20 |
21 | ))}
22 |
23 | );
24 | };
25 |
26 | export default BadgeSection;
27 |
--------------------------------------------------------------------------------
/src/components/sections/base.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | /**
3 | * A base section component.
4 | * This should be deleted in a future release because it doesn't do anything besides
5 | * rendering a section element. (it used to be styled)
6 | * @deprecated
7 | */
8 | const BaseSection = ({
9 | children,
10 | }: {
11 | children: React.ReactNode | React.ReactNode[];
12 | }) => {
13 | return ;
14 | };
15 |
16 | export default BaseSection;
17 |
--------------------------------------------------------------------------------
/src/components/sections/basic-info.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/BasicInfoSection.module.css";
2 |
3 | /**
4 | * Renders a basic info section with display name, username, and pronouns.
5 | *
6 | * @param {{displayname?: string; username?: string; pronouns?: string;}} - Object with optional display name, username, and pronouns
7 | * @return {JSX.Element} - The rendered basic info section
8 | */
9 | const BasicInfoSection = ({
10 | displayname,
11 | username,
12 | pronouns,
13 | }: {
14 | displayname?: string;
15 | username?: string;
16 | pronouns?: string;
17 | }) => {
18 | return (
19 |
20 | {displayname && {displayname} }
21 | {username && {username} }
22 | {username && {pronouns}
}
23 |
24 | );
25 | };
26 |
27 | export default BasicInfoSection;
28 |
--------------------------------------------------------------------------------
/src/components/sections/member-since.tsx:
--------------------------------------------------------------------------------
1 | import SectionTitle from "../section-title";
2 | import styles from "../../styles/MemberSinceSection.module.css";
3 | /**
4 | * Renders a section displaying the member's join dates on Discord and the server.
5 | *
6 | * @param {string} title - Optional title for the section
7 | * @param {string} discordJoinDate - The date the member joined Discord
8 | * @param {string} serverJoinDate - Optional date the member joined the server
9 | * @param {string} serverIconUrl - Optional URL for the server icon
10 | * @param {string} serverName - Optional name of the server (Used for accessibility in the alt attribute)
11 | * @return {JSX.Element} The rendered section component
12 | */
13 | const MemberSinceSection = ({
14 | title,
15 | discordJoinDate,
16 | serverJoinDate,
17 | serverIconUrl,
18 | serverName,
19 | }: {
20 | title?: string;
21 | discordJoinDate: string;
22 | serverJoinDate?: string;
23 | serverIconUrl?: string;
24 | serverName?: string;
25 | }) => {
26 | return (
27 |
28 |
29 |
30 | {serverJoinDate ? (
31 | <>
32 |
43 |
47 |
48 |
{discordJoinDate}
49 |
50 | {serverIconUrl && (
51 |
57 | )}
58 |
{serverJoinDate}
59 | >
60 | ) : (
61 |
{discordJoinDate}
62 | )}
63 |
64 |
65 | );
66 | };
67 |
68 | export default MemberSinceSection;
69 |
--------------------------------------------------------------------------------
/src/components/sections/message.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 | import useAutosizeTextArea from "../../hooks/useAutosizeTextArea";
3 | import styles from "../../styles/MessageSection.module.css";
4 | /**
5 | * Generates a Discord message section with a textarea for input.
6 | *
7 | * @param {string} message - The message to display in the textarea
8 | * @param {string} placeholder - The placeholder text for the textarea
9 | * @param {string} accentColor - The color to use for the textarea border
10 | * @param {function} handleInput - The function to handle input changes in the textarea
11 | * @return {JSX.Element} The JSX element representing the message section
12 | */
13 | const MessageSection = ({
14 | message = "",
15 | placeholder,
16 | accentColor,
17 | handleInput,
18 | }: {
19 | message?: string;
20 | placeholder?: string;
21 | accentColor?: string;
22 | handleInput: (event: React.ChangeEvent) => void;
23 | }) => {
24 | const messageRef = useRef(null);
25 | useAutosizeTextArea(messageRef.current, message);
26 |
27 | return (
28 |
42 | );
43 | };
44 |
45 | export default MessageSection;
46 |
--------------------------------------------------------------------------------
/src/components/sections/note.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "react";
2 | import useAutosizeTextArea from "../../hooks/useAutosizeTextArea";
3 | import SectionTitle from "../section-title";
4 | import styles from "../../styles/NoteSection.module.css";
5 | /**
6 | * Renders a Discord note section with a title, note content, and input field for adding notes.
7 | *
8 | * @param {string} title - The title of the note section
9 | * @param {string} note - The current note content
10 | * @param {string} placeholder - The placeholder text for the input field
11 | * @param {function} handleInput - The function to handle input changes
12 | * @return {JSX.Element} A section component with a title, note input field, and note content
13 | */
14 | const NoteSection = ({
15 | title,
16 | note = "",
17 | placeholder,
18 | handleInput,
19 | }: {
20 | title?: string;
21 | note?: string;
22 | placeholder?: string;
23 | handleInput: (event: React.ChangeEvent) => void;
24 | }) => {
25 | const noteRef = useRef(null);
26 |
27 | useAutosizeTextArea(noteRef.current, note);
28 |
29 | return (
30 |
31 | {title ? : }
32 |
46 |
47 | );
48 | };
49 |
50 | export default NoteSection;
51 |
--------------------------------------------------------------------------------
/src/components/sections/role.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Role as RoleItem } from "../../types";
3 | import SectionTitle from "../section-title";
4 | import Role from "../role";
5 | import styles from "../../styles/RoleSection.module.css";
6 |
7 | /**
8 | * Render a section for displaying roles with an optional alternative title and list of roles.
9 | *
10 | * @param {string} title - Optional title for the section
11 | * @param {React.JSX.Element | React.JSX.Element[]} children - Optional children elements
12 | * @param {Role[]} roles - List of roles to display
13 | * @return {React.JSX.Element} The JSX element representing the role section
14 | */
15 | const RoleSection = ({
16 | title,
17 | children,
18 | roles,
19 | }: {
20 | title?: string;
21 | children?: React.JSX.Element | React.JSX.Element[];
22 | roles?: RoleItem[];
23 | }) => {
24 | const childrenCount = React.Children.count(children);
25 |
26 | return (
27 |
28 |
32 |
33 | {roles &&
34 | roles.map((role, index) => (
35 |
36 | ))}
37 | {children}
38 |
39 |
40 | );
41 | };
42 |
43 | export default RoleSection;
44 |
--------------------------------------------------------------------------------
/src/components/sections/spotify.tsx:
--------------------------------------------------------------------------------
1 | import SectionTitle from "../section-title";
2 | import SeekBar from "../seek-bar";
3 | import styles from "../../styles/SpotifySection.module.css";
4 | import SpotifyLogo from "../spotify-logo";
5 | /**
6 | * Renders a section displaying Spotify song information.
7 | *
8 | * @param {string} title - The title of the section
9 | * @param {string} song - The name of the song
10 | * @param {string} artist - The name of the artist
11 | * @param {string} album - The name of the album
12 | * @param {string} artUrl - The URL of the album art
13 | * @param {string} trackUrl - The URL of the track on Spotify
14 | * @param {number} startTimeMs - The start time of the song in milliseconds
15 | * @param {number} endTimeMs - The end time of the song in milliseconds
16 | * @param {string} primaryColor - The color of the button (inherited from card)
17 | * @param {string} playOnSpotifyText - The text to display on the button (default: Play on Spotify)
18 | * @param {string} byString - The string to display before the artist's name (default: by)
19 | * @param {string} onString - The string to display before the album's name (default: on)
20 | * @return {JSX.Element} The rendered Spotify section
21 | */
22 |
23 | const SpotifySection = ({
24 | title,
25 | song,
26 | artist,
27 | album,
28 | artUrl,
29 | trackUrl,
30 | startTimeMs,
31 | endTimeMs,
32 | primaryColor,
33 | playOnSpotifyText,
34 | byText = "by",
35 | onText = "on",
36 | }: {
37 | title?: string;
38 | song: string;
39 | artist: string;
40 | album: string;
41 | artUrl?: string;
42 | trackUrl?: string;
43 | startTimeMs?: number;
44 | endTimeMs?: number;
45 | primaryColor?: string;
46 | playOnSpotifyText?: string;
47 | byText?: string;
48 | onText?: string;
49 | }) => {
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 | {artUrl && (
58 |
59 | {trackUrl ? (
60 |
61 |
62 |
63 | ) : (
64 |
65 | )}
66 |
67 | )}
68 |
69 |
70 | {song.length <= 27 ? song : `${song.substring(0, 27)}...`}
71 |
72 |
73 | {byText}{" "}
74 | {artist.length <= 27 ? artist : `${artist.substring(0, 27)}...`}
75 |
76 |
77 | {onText}{" "}
78 | {album.length <= 27 ? album : `${album.substring(0, 27)}...`}
79 |
80 |
81 |
82 |
83 | {startTimeMs && endTimeMs && (
84 |
85 | )}
86 |
87 | {trackUrl && (
88 |
102 | )}
103 |
104 | );
105 | };
106 |
107 | export default SpotifySection;
108 |
--------------------------------------------------------------------------------
/src/components/sections/status.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/StatusSection.module.css";
2 |
3 | /**
4 | * Renders a status section component with an optional icon, emoji, and status text.
5 | *
6 | * @param {string} iconUrl - The URL for the icon image.
7 | * @param {string} emoji - The emoji to display.
8 | * @param {string} status - The status text to display.
9 | * @return {JSX.Element} The rendered status section component.
10 | */
11 | const StatusSection = ({
12 | iconUrl,
13 | emoji,
14 | status,
15 | }: {
16 | iconUrl?: string;
17 | emoji?: string;
18 | status: string;
19 | }) => {
20 | return (
21 |
22 | {iconUrl && }
23 | {emoji && {emoji}
}
24 | {status}
25 |
26 | );
27 | };
28 |
29 | export default StatusSection;
30 |
--------------------------------------------------------------------------------
/src/components/seek-bar.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Renders the seek bar for the SpotifySection component.
3 | */
4 | import { useEffect, useState } from "react";
5 | import { formatTime } from "../helpers/helper-functions";
6 | import styles from "../styles/SeekBar.module.css";
7 |
8 | const SeekBar = ({
9 | startTimeMs,
10 | endTimeMs,
11 | }: {
12 | startTimeMs: number;
13 | endTimeMs: number;
14 | }) => {
15 | const [currentDateTime, setCurrentDateTime] = useState(new Date());
16 |
17 | useEffect(() => {
18 | const interval = setInterval(() => setCurrentDateTime(new Date()), 1000);
19 | return () => clearInterval(interval);
20 | }, [startTimeMs]);
21 |
22 | const currentPosition = formatTime(startTimeMs, currentDateTime.getTime());
23 | const songDuration = formatTime(startTimeMs, endTimeMs);
24 | const percentage =
25 | ((currentDateTime.getTime() - startTimeMs) / (endTimeMs - startTimeMs)) *
26 | 100;
27 |
28 | return (
29 |
30 |
36 |
37 |
{currentPosition}
38 |
{songDuration}
39 |
40 |
41 | );
42 | };
43 |
44 | export default SeekBar;
45 |
--------------------------------------------------------------------------------
/src/components/spotify-logo.tsx:
--------------------------------------------------------------------------------
1 | export const SpotifyLogo = ({color = "#00DA5A", size = 20}:{color?: string, size?: number}) => {
2 | return (
3 |
10 |
17 |
22 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default SpotifyLogo
--------------------------------------------------------------------------------
/src/helpers/helper-functions.ts:
--------------------------------------------------------------------------------
1 | // Formats the time in milliseconds to a string in the format of "hh:mm:ss" or "mm:ss" if the time is less than an hour.
2 | export const formatTime = (startTimeMs: number, endTimeMs: number) => {
3 | const secondAsMilliseconds = 1000;
4 | const minuteAsMilliseconds = secondAsMilliseconds * 60;
5 | const hourAsMilliseconds = minuteAsMilliseconds * 60;
6 |
7 | const distance = endTimeMs - startTimeMs;
8 | const seconds = Math.floor((distance / secondAsMilliseconds) % 60).toString().padStart(2, "0");
9 | let minutes = Math.floor((distance / minuteAsMilliseconds) % 60).toString()
10 | if (distance < hourAsMilliseconds) return `${minutes}:${seconds}`;
11 |
12 | minutes = minutes.padStart(2, "0");
13 | const hours = Math.floor(distance / hourAsMilliseconds).toString()
14 | return `${hours}:${minutes}:${seconds}`;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/hooks/useAutosizeTextArea.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | // Credit: https://medium.com/@oherterich/creating-a-textarea-with-dynamic-height-using-react-and-typescript-5ed2d78d9848
4 |
5 | const useAutosizeTextArea = (
6 | textAreaRef: HTMLTextAreaElement | null,
7 | value: string
8 | ) => {
9 | useEffect(() => {
10 | if (textAreaRef) {
11 | textAreaRef.style.height = "0px";
12 | const scrollHeight = textAreaRef.scrollHeight;
13 | textAreaRef.style.height = scrollHeight + "px";
14 | }
15 | }, [textAreaRef, value]);
16 | };
17 |
18 | export default useAutosizeTextArea;
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import DiscordCard from "./components/discord-card";
2 | import LanyardDiscordCard from "./components/lanyard-discord-card";
3 | import BaseDiscordCard from "./components/base-discord-card";
4 | import "./App.css";
5 |
6 | export { DiscordCard, LanyardDiscordCard, BaseDiscordCard };
7 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/src/styles/AboutMeSection.module.css:
--------------------------------------------------------------------------------
1 | .aboutMeSection {
2 | margin-bottom: 0.25rem;
3 | font-size: 14px;
4 | }
5 |
6 | .aboutMeLink {
7 | display: block;
8 | color: rgb(59, 130, 246);
9 | }
10 |
11 | .aboutMeLink:hover {
12 | color: rgb(96, 165, 250);
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/ActivitySection.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: space-between;
4 | margin-bottom: 6px;
5 | }
6 |
7 | .content {
8 | display: flex;
9 | align-items: center;
10 | gap: 0.75rem;
11 | }
12 |
13 | .imageContainer {
14 | min-width: 65px;
15 | align-self: flex-start;
16 | }
17 |
18 | .imageWrapper {
19 | position: relative;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .largeImage {
25 | width: 65px;
26 | height: 65px;
27 | user-select: none;
28 | object-fit: cover;
29 | border-radius: 0.375rem;
30 | }
31 |
32 | .smallImageOverlay {
33 | position: absolute;
34 | bottom: -3px;
35 | right: -6px;
36 | width: 20px;
37 | height: 20px;
38 | border-radius: 9999px;
39 | user-select: none;
40 | object-fit: cover;
41 | }
42 |
43 | .smallImageContainer {
44 | min-width: 65px;
45 | align-self: flex-start;
46 | }
47 |
48 | .smallImageStandalone {
49 | width: 65px;
50 | height: 65px;
51 | user-select: none;
52 | object-fit: cover;
53 | border-radius: 0.375rem;
54 | }
55 |
56 | .textContainer {
57 | font-size: 0.875rem;
58 | font-weight: 400;
59 | }
60 |
61 | .activityName {
62 | font-size: 0.875rem;
63 | line-height: 1.25rem;
64 | font-weight: 700;
65 | }
66 |
67 | .activityDetails {
68 | font-size: 0.875rem;
69 | line-height: 1.25rem;
70 | font-weight: 400;
71 | }
72 |
73 | .activityState {
74 | font-size: 0.875rem;
75 | line-height: 1.25rem;
76 | font-weight: 400;
77 | }
78 |
79 | .activityTime {
80 | font-size: 0.875rem;
81 | line-height: 1.25rem;
82 | font-weight: 400;
83 | }
84 |
85 | .button {
86 | display: block;
87 | width: 100%;
88 | font-size: 0.875rem;
89 | opacity: 0.7;
90 | cursor: not-allowed;
91 | line-height: 1.25rem;
92 | padding: 6px 4px;
93 | text-align: center;
94 | border-radius: 0.375rem;
95 | color: white;
96 | transition: filter 0.15s;
97 | text-decoration: none;
98 | }
99 |
100 | .buttonWrapper {
101 | margin-top: 8px;
102 | }
103 |
104 | .buttonContent {
105 | display: flex;
106 | justify-content: center;
107 | align-items: center;
108 | gap: 0.5rem;
109 | }
110 |
--------------------------------------------------------------------------------
/src/styles/BadgeSection.module.css:
--------------------------------------------------------------------------------
1 | .badgeContainer {
2 | position: absolute;
3 | max-width: 196px;
4 | display: flex;
5 | align-items: flex-end;
6 | justify-content: flex-end;
7 | flex-wrap: wrap;
8 | z-index: 20;
9 | bottom: -38px;
10 | right: 14px;
11 | background-color: #00000059;
12 | border-radius: 0.375rem;
13 | padding: 3px;
14 | user-select: none;
15 | }
16 |
17 | .badgeIcon {
18 | width: 22px;
19 | height: 22px;
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles/BaseDiscordCard.module.css:
--------------------------------------------------------------------------------
1 | .discord-card-outer-body {
2 | width: 320px;
3 | background: #0000008c;
4 | backdrop-filter: blur(10px);
5 | border-radius: 0 0 8px 8px;
6 | padding: 30px 2px 2px 2px;
7 | margin: 0 4px 4px 4px;
8 | }
9 |
10 | .discord-card-inner-body {
11 | width: 300px;
12 | background: #0000008c;
13 | border-radius: 6px;
14 | padding: 12px 16px 16px 16px;
15 | margin: 20px auto 10px auto;
16 | }
17 |
18 | .container {
19 | display: flex;
20 | }
21 |
22 | .cardWrapper {
23 | line-height: 1.5rem;
24 | font-weight: 400;
25 | color: rgb(255, 255, 255);
26 | background-color: #242424;
27 | border-radius: 0.5rem;
28 | z-index: 20;
29 | width: 328px;
30 | }
31 |
32 | .cardContent {
33 | padding: 0;
34 | margin: 0;
35 | width: 328px;
36 | position: relative;
37 | user-select: none;
38 | }
39 |
40 | .profileSection {
41 | position: absolute;
42 | z-index: 10;
43 | }
44 |
45 | .profileImage {
46 | top: 65px;
47 | left: 20px;
48 | position: relative;
49 | border-radius: 9999px;
50 | padding: 0.25rem;
51 | width: 85px;
52 | height: 85px;
53 | border: 2px solid black;
54 | z-index: 30;
55 | }
56 |
57 | .profileBackgroundGradient {
58 | top: -20px;
59 | left: 20px;
60 | position: relative;
61 | border-radius: 9999px;
62 | padding: 0.25rem;
63 | width: 85px;
64 | height: 85px;
65 | border: 2px solid black;
66 | z-index: 10;
67 | }
68 |
69 | .profileShadowOverlay {
70 | top: -105px;
71 | left: 20px;
72 | position: relative;
73 | border-radius: 9999px;
74 | padding: 0.25rem;
75 | width: 85px;
76 | height: 85px;
77 | border: 2px solid black;
78 | z-index: 10;
79 | }
80 |
81 | .bannerImage {
82 | width: 328px;
83 | height: 116px;
84 | padding-top: 4px;
85 | padding-right: 4px;
86 | padding-bottom: 0px;
87 | padding-left: 4px;
88 | border-top-left-radius: 0.5rem;
89 | border-top-right-radius: 0.5rem;
90 | }
91 |
92 | .statusIndicator {
93 | position: absolute;
94 | width: 24px;
95 | height: 24px;
96 | border-radius: 9999px;
97 | background-color: #222222ef;
98 | top: 125px;
99 | right: 226px;
100 | z-index: 30;
101 | display: flex;
102 | align-items: center;
103 | justify-content: center;
104 | user-select: none;
105 | }
106 |
107 | .statusIcon {
108 | top: 125px;
109 | right: 226px;
110 | width: 14px;
111 | height: 14px;
112 | display: flex;
113 | }
114 |
115 | /* Utility class equivalent to Tailwind's space-y-2 */
116 | .spaceY2 > * + * {
117 | margin-top: 0.5rem;
118 | }
119 |
--------------------------------------------------------------------------------
/src/styles/BasicInfoSection.module.css:
--------------------------------------------------------------------------------
1 | .section {
2 | margin-bottom: 0.5rem;
3 | }
4 |
5 | .displayName {
6 | font-size: 1.25rem;
7 | font-weight: bold;
8 | }
9 |
10 | .username {
11 | font-weight: 600;
12 | font-size: 1rem;
13 | }
14 |
15 | .pronouns {
16 | font-weight: normal;
17 | font-size: 1rem;
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/DiscordCardPreflight.module.css:
--------------------------------------------------------------------------------
1 | /* Scoped Tailwind Preflight for Discord Card Component */
2 | .discordCardScope,
3 | .discordCardScope *,
4 | .discordCardScope ::before,
5 | .discordCardScope ::after {
6 | box-sizing: border-box;
7 | border-width: 0;
8 | border-style: solid;
9 | border-color: #e5e7eb;
10 | }
11 |
12 | .discordCardScope ::before,
13 | .discordCardScope ::after {
14 | --tw-content: "";
15 | }
16 |
17 | .discordCardScope {
18 | line-height: 1.5;
19 | -webkit-text-size-adjust: 100%;
20 | -moz-tab-size: 4;
21 | tab-size: 4;
22 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
23 | "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
24 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
25 | font-feature-settings: normal;
26 | font-variation-settings: normal;
27 | }
28 |
29 | .discordCardScope hr {
30 | height: 0;
31 | color: inherit;
32 | border-top-width: 1px;
33 | }
34 |
35 | .discordCardScope abbr:where([title]) {
36 | text-decoration: underline dotted;
37 | }
38 |
39 | .discordCardScope a {
40 | color: inherit;
41 | text-decoration: inherit;
42 | }
43 |
44 | .discordCardScope b,
45 | .discordCardScope strong {
46 | font-weight: bolder;
47 | }
48 |
49 | .discordCardScope code,
50 | .discordCardScope kbd,
51 | .discordCardScope samp,
52 | .discordCardScope pre {
53 | font-family: ui-monospace, SFMono-Regular, "Roboto Mono", "Cascadia Code",
54 | "Source Code Pro", Consolas, "Liberation Mono", Menlo, Monaco, "Courier New",
55 | monospace;
56 | font-size: 1em;
57 | }
58 |
59 | .discordCardScope small {
60 | font-size: 80%;
61 | }
62 |
63 | .discordCardScope sub,
64 | .discordCardScope sup {
65 | font-size: 75%;
66 | line-height: 0;
67 | position: relative;
68 | vertical-align: baseline;
69 | }
70 |
71 | .discordCardScope sub {
72 | bottom: -0.25em;
73 | }
74 |
75 | .discordCardScope sup {
76 | top: -0.5em;
77 | }
78 |
79 | .discordCardScope table {
80 | text-indent: 0;
81 | border-color: inherit;
82 | border-collapse: collapse;
83 | }
84 |
85 | .discordCardScope button,
86 | .discordCardScope input,
87 | .discordCardScope optgroup,
88 | .discordCardScope select,
89 | .discordCardScope textarea {
90 | font-family: inherit;
91 | font-feature-settings: inherit;
92 | font-variation-settings: inherit;
93 | font-size: 100%;
94 | font-weight: inherit;
95 | line-height: inherit;
96 | color: inherit;
97 | margin: 0;
98 | padding: 0;
99 | }
100 |
101 | .discordCardScope button,
102 | .discordCardScope select {
103 | text-transform: none;
104 | }
105 |
106 | .discordCardScope button,
107 | .discordCardScope [type="button"],
108 | .discordCardScope [type="reset"],
109 | .discordCardScope [type="submit"] {
110 | -webkit-appearance: button;
111 | background-color: transparent;
112 | background-image: none;
113 | }
114 |
115 | .discordCardScope :-moz-focusring {
116 | outline: auto;
117 | }
118 |
119 | .discordCardScope :-moz-ui-invalid {
120 | box-shadow: none;
121 | }
122 |
123 | .discordCardScope progress {
124 | vertical-align: baseline;
125 | }
126 |
127 | .discordCardScope ::-webkit-inner-spin-button,
128 | .discordCardScope ::-webkit-outer-spin-button {
129 | height: auto;
130 | }
131 |
132 | .discordCardScope [type="search"] {
133 | -webkit-appearance: textfield;
134 | outline-offset: -2px;
135 | }
136 |
137 | .discordCardScope ::-webkit-search-decoration {
138 | -webkit-appearance: none;
139 | }
140 |
141 | .discordCardScope ::-webkit-file-upload-button {
142 | -webkit-appearance: button;
143 | font: inherit;
144 | }
145 |
146 | .discordCardScope summary {
147 | display: list-item;
148 | }
149 |
150 | .discordCardScope blockquote,
151 | .discordCardScope dl,
152 | .discordCardScope dd,
153 | .discordCardScope h1,
154 | .discordCardScope h2,
155 | .discordCardScope h3,
156 | .discordCardScope h4,
157 | .discordCardScope h5,
158 | .discordCardScope h6,
159 | .discordCardScope hr,
160 | .discordCardScope figure,
161 | .discordCardScope p,
162 | .discordCardScope pre {
163 | margin: 0;
164 | }
165 |
166 | .discordCardScope fieldset {
167 | margin: 0;
168 | padding: 0;
169 | }
170 |
171 | .discordCardScope legend {
172 | padding: 0;
173 | }
174 |
175 | .discordCardScope ol,
176 | .discordCardScope ul,
177 | .discordCardScope menu {
178 | list-style: none;
179 | margin: 0;
180 | padding: 0;
181 | }
182 |
183 | .discordCardScope dialog {
184 | padding: 0;
185 | }
186 |
187 | .discordCardScope textarea {
188 | resize: vertical;
189 | }
190 |
191 | .discordCardScope input::placeholder,
192 | .discordCardScope textarea::placeholder {
193 | opacity: 1;
194 | color: #9ca3af;
195 | }
196 |
197 | .discordCardScope button,
198 | .discordCardScope [role="button"] {
199 | cursor: pointer;
200 | }
201 |
202 | .discordCardScope :disabled {
203 | cursor: default;
204 | }
205 |
206 | .discordCardScope img,
207 | .discordCardScope svg,
208 | .discordCardScope video,
209 | .discordCardScope canvas,
210 | .discordCardScope audio,
211 | .discordCardScope iframe,
212 | .discordCardScope embed,
213 | .discordCardScope object {
214 | display: block;
215 | vertical-align: middle;
216 | }
217 |
218 | .discordCardScope img,
219 | .discordCardScope video {
220 | max-width: 100%;
221 | height: auto;
222 | }
223 |
224 | .discordCardScope [hidden] {
225 | display: none;
226 | }
227 |
228 | /* Additional utility classes that may be used */
229 | .discordCardScope .space-y-2 > :not([hidden]) ~ :not([hidden]) {
230 | --tw-space-y-reverse: 0;
231 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
232 | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
233 | }
234 |
--------------------------------------------------------------------------------
/src/styles/DiscordLink.module.css:
--------------------------------------------------------------------------------
1 | .link {
2 | color: #3b82f6;
3 | transition: color 0.15s ease-in-out;
4 | }
5 |
6 | .link:hover {
7 | color: #60a5fa;
8 | }
9 |
--------------------------------------------------------------------------------
/src/styles/MemberSinceSection.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-start;
5 | gap: 0.5rem;
6 | }
7 |
8 | .discordIcon {
9 | width: 14px;
10 | height: 14px;
11 | }
12 |
13 | .text {
14 | font-size: 15px;
15 | }
16 |
17 | .separator {
18 | width: 0.25rem;
19 | height: 0.25rem;
20 | border-radius: 50%;
21 | background-color: #4b4b4b;
22 | }
23 |
24 | .serverIcon {
25 | width: 15px;
26 | height: 15px;
27 | border-radius: 50%;
28 | }
29 |
--------------------------------------------------------------------------------
/src/styles/MessageSection.module.css:
--------------------------------------------------------------------------------
1 | .section {
2 | }
3 |
4 | .textarea {
5 | margin-top: 0.5rem;
6 | width: 100%;
7 | height: 44px;
8 | border-radius: 0.5rem;
9 | background-color: transparent;
10 | resize: none;
11 | outline: none;
12 | font-size: 0.875rem;
13 | scrollbar-width: none;
14 | }
15 |
16 | .textarea::-webkit-scrollbar {
17 | display: none;
18 | }
19 |
20 | .textarea:focus {
21 | outline: none;
22 | }
23 |
--------------------------------------------------------------------------------
/src/styles/NoteSection.module.css:
--------------------------------------------------------------------------------
1 | .noteTextarea {
2 | font-size: 0.8rem;
3 | padding: 4px;
4 | width: 100%;
5 | background-color: transparent;
6 | border: 0;
7 | resize: none;
8 | outline: none;
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/Role.module.css:
--------------------------------------------------------------------------------
1 | .roleContainer {
2 | background-color: rgba(75, 75, 75, 0.25);
3 | padding: 8px;
4 | border-radius: 4px;
5 | height: 25px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | gap: 5px;
10 | user-select: none;
11 | border: 0.5px solid rgba(136, 135, 135, 0.415);
12 | }
13 |
14 | .roleIcon {
15 | width: 12px;
16 | height: 12px;
17 | border-radius: 50%;
18 | }
19 |
20 | .roleText {
21 | font-size: 0.73rem;
22 | padding: 0;
23 | margin: 0;
24 | text-align: center;
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/RoleSection.module.css:
--------------------------------------------------------------------------------
1 | .rolesList {
2 | display: flex;
3 | flex-wrap: wrap;
4 | gap: 6px;
5 | margin-bottom: 12px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/SectionTitle.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | text-transform: uppercase;
3 | font-size: 0.875rem;
4 | line-height: 1.25rem;
5 | font-weight: 600;
6 | margin-bottom: 0.125rem;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/SeekBar.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | font-size: 0.75rem;
3 | margin-top: 0.75rem;
4 | }
5 |
6 | .bar {
7 | height: 0.25rem;
8 | background-color: rgb(64, 64, 64);
9 | border-radius: 0.75rem;
10 | }
11 |
12 | .progress {
13 | height: 0.25rem;
14 | background-color: white;
15 | border-radius: 0.75rem;
16 | }
17 |
18 | .timeContainer {
19 | display: flex;
20 | justify-content: space-between;
21 | align-items: flex-start;
22 | }
23 |
24 | .timeText {
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/Separator.module.css:
--------------------------------------------------------------------------------
1 | .separator {
2 | width: 100%;
3 | height: 1px;
4 | background-color: #5a5757;
5 | margin: 12px 0;
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/SpotifySection.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: space-between;
4 | margin-bottom: 6px;
5 | }
6 |
7 | .content {
8 | display: flex;
9 | align-items: center;
10 | gap: 0.75rem;
11 | }
12 |
13 | .albumArtContainer {
14 | min-width: 65px;
15 | align-self: flex-start;
16 | }
17 |
18 | .albumArt {
19 | width: 65px;
20 | height: 65px;
21 | user-select: none;
22 | border-radius: 0.375rem;
23 | }
24 |
25 | .songTitle {
26 | font-size: 0.875rem;
27 | line-height: 1.25rem;
28 | font-weight: 700;
29 | }
30 |
31 | .songInfo {
32 | font-size: 0.875rem;
33 | line-height: 1.25rem;
34 | font-weight: 400;
35 | }
36 |
37 | .playButton {
38 | display: block;
39 | width: 100%;
40 | font-size: 0.875rem;
41 | line-height: 1.25rem;
42 | padding: 6px 4px;
43 | text-align: center;
44 | margin-top: 8px;
45 | border-radius: 0.375rem;
46 | color: white;
47 | transition: filter 0.15s;
48 | text-decoration: none;
49 | }
50 |
51 | .playButtonContent {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | gap: 0.5rem;
56 | }
57 |
58 | .lighten:hover {
59 | filter: brightness(1.1);
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/StatusSection.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-start;
5 | gap: 0.5rem;
6 | }
7 |
8 | .icon {
9 | width: 1.25rem;
10 | height: 1.25rem;
11 | font-size: 0.8rem;
12 | }
13 |
14 | .emoji {
15 | width: 1.25rem;
16 | height: 1.25rem;
17 | font-size: 0.8rem;
18 | }
19 |
20 | .status {
21 | font-size: 0.95rem;
22 | text-align: start;
23 | }
24 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Basic Types and Enums
3 | // =============================================================================
4 |
5 | export type ActivityPriority = "spotify" | "activity" | "default" | "none";
6 | export type ConnectionStatus = "online" | "dnd" | "idle" | "offline";
7 |
8 | // =============================================================================
9 | // Core Interfaces
10 | // =============================================================================
11 |
12 | export interface AboutMeItem {
13 | text: string;
14 | marginBottom?: number;
15 | href?: string;
16 | }
17 |
18 | export interface Badge {
19 | name: string;
20 | iconUrl: string;
21 | }
22 |
23 | export interface Party {
24 | currentSize: number;
25 | maxSize: number;
26 | }
27 |
28 | export interface Role {
29 | name: string;
30 | color: string;
31 | }
32 |
33 | // =============================================================================
34 | // Component Props Interfaces
35 | // =============================================================================
36 |
37 | export interface AboutMeSectionProps {
38 | title?: string;
39 | items: AboutMeItem[];
40 | }
41 |
42 | export interface ActivitySectionProps {
43 | title?: string;
44 | name: string;
45 | state?: string;
46 | details?: string;
47 | largeImage?: string;
48 | smallImage?: string;
49 | party?: Party;
50 | buttonText?: string;
51 | primaryColor?: string;
52 | }
53 |
54 | export interface BasicInfoSectionProps {
55 | displayname: string;
56 | username: string;
57 | pronouns?: string;
58 | }
59 |
60 | export interface MemberSinceSectionProps {
61 | title?: string;
62 | discordJoinDate: string;
63 | serverJoinDate?: string;
64 | serverIconUrl?: string;
65 | serverName?: string;
66 | }
67 |
68 | export interface MessageSectionProps {
69 | message?: string;
70 | placeholder?: string;
71 | accentColor?: string;
72 | handleInput: (event: React.ChangeEvent) => void;
73 | }
74 |
75 | export interface NoteSectionProps {
76 | title?: string;
77 | note?: string;
78 | placeholder?: string;
79 | handleInput: (event: React.ChangeEvent) => void;
80 | }
81 |
82 | export interface RoleSectionProps {
83 | title?: string;
84 | roles: Role[];
85 | }
86 |
87 | export interface SpotifySectionProps {
88 | title?: string;
89 | song: string;
90 | artist: string;
91 | album: string;
92 | artUrl?: string;
93 | trackUrl?: string;
94 | startTimeMs?: number;
95 | endTimeMs?: number;
96 | primaryColor?: string;
97 | playOnSpotifyText?: string;
98 | byText?: string;
99 | onText?: string;
100 | }
101 |
102 | export interface StatusSectionProps {
103 | iconUrl?: string;
104 | status: string;
105 | }
106 |
107 | // =============================================================================
108 | // Lanyard-specific Interfaces
109 | // =============================================================================
110 |
111 | export interface LanyardActivitySectionProps {
112 | title?: string;
113 | show?: boolean;
114 | showElapsedTime?: boolean;
115 | timeElapsedText?: string;
116 | timeAlignment?: "left" | "right";
117 | }
118 |
119 | export interface LanyardSpotifySectionProps {
120 | // Controls Spotify section visibility
121 | show?: boolean;
122 | title?: string;
123 | buttonText?: string;
124 | byText?: string;
125 | onText?: string;
126 | }
127 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | /* The "typeRoots" configuration specifies the locations where
19 | TypeScript looks for type definitions (.d.ts files) to
20 | include in the compilation process.*/
21 | "typeRoots": ["./dist/index.d.ts", "node_modules/@types"]
22 | },
23 | /* include /index.ts*/
24 | "include": ["src", "./index.ts"],
25 | "references": [{ "path": "./tsconfig.node.json" }]
26 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 | import dts from "vite-plugin-dts";
5 |
6 | export default defineConfig({
7 | build: {
8 | //Specifies that the output of the build will be a library.
9 | lib: {
10 | //Defines the entry point for the library build. It resolves
11 | //to src/index.ts,indicating that the library starts from this file.
12 | entry: path.resolve(__dirname, "src/index.ts"),
13 | name: "react-jp-ui",
14 | //A function that generates the output file
15 | //name for different formats during the build
16 | fileName: (format) => `index.${format}.mjs`,
17 | },
18 | rollupOptions: {
19 | external: ["react", "react-dom"],
20 | output: {
21 | globals: {
22 | react: "React",
23 | "react-dom": "ReactDOM",
24 | },
25 | },
26 | },
27 | //Generates sourcemaps for the built files,
28 | //aiding in debugging.
29 | sourcemap: true,
30 | //Clears the output directory before building.
31 | emptyOutDir: true,
32 | },
33 | //react() enables React support.
34 | //dts() generates TypeScript declaration files (*.d.ts)
35 | //during the build.
36 | plugins: [react(), dts()],
37 | });
38 |
--------------------------------------------------------------------------------