├── .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 | ![Game Section Example](/github/spotify-example.png) 165 | 166 | #### Game preview 167 | 168 | ![Game Section Example](/github/game-example.png) 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 | ![i18n example](/github/member-since-i18n.png) 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 | Discord profile picture 38 |
    44 |
    50 |
    51 | 52 |
    53 | 67 |
    68 | {badges && } 69 |
    70 |
    71 |
    {children}
    72 |
    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 | 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 | {badge.name} 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
      {children}
      ; 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 |
      29 | 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 | {album} 62 | 63 | ) : ( 64 | {album} 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 |
      31 |
      35 |
      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 | --------------------------------------------------------------------------------