├── .github └── workflows │ ├── deploy-app.yml │ └── deploy-website.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── assets └── sc-timetable.png ├── biome.json ├── client ├── .env ├── .gitignore ├── README.md ├── app │ ├── components │ │ ├── anchor.tsx │ │ ├── audio-item.tsx │ │ ├── audio-recorder.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── common-head.tsx │ │ ├── community-info.tsx │ │ ├── community-mod.tsx │ │ ├── content.tsx │ │ ├── crowdsource-notice.tsx │ │ ├── days-header.tsx │ │ ├── discussions-empty.tsx │ │ ├── dropdown-menu.tsx │ │ ├── error-boundary.tsx │ │ ├── file-input.tsx │ │ ├── file-menu.tsx │ │ ├── file-select-item.tsx │ │ ├── footer.tsx │ │ ├── input.tsx │ │ ├── knust-login-direction.tsx │ │ ├── large-select.tsx │ │ ├── lesson-form.tsx │ │ ├── lesson-item.tsx │ │ ├── login-comment.tsx │ │ ├── logout-modal.tsx │ │ ├── media-item.tsx │ │ ├── media-preview.tsx │ │ ├── modal.tsx │ │ ├── navbar.tsx │ │ ├── nested-comments.tsx │ │ ├── non-image-thumb.tsx │ │ ├── parlon-logo.tsx │ │ ├── peer-video-panel.tsx │ │ ├── pending-ui.tsx │ │ ├── post-content.tsx │ │ ├── post-input.tsx │ │ ├── post-item.tsx │ │ ├── post-menu.tsx │ │ ├── post-people.tsx │ │ ├── post-time.tsx │ │ ├── product-form.tsx │ │ ├── product-item.tsx │ │ ├── select.tsx │ │ ├── self-video-panel.tsx │ │ ├── sym-outline.tsx │ │ ├── tag-input.tsx │ │ ├── tag-select.tsx │ │ ├── tags-filter.tsx │ │ ├── tags.tsx │ │ ├── textarea.tsx │ │ ├── timetable-filter.tsx │ │ ├── timetable-save-to-calendar.tsx │ │ ├── username.tsx │ │ └── votes.tsx │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── lib │ │ ├── boat-client.ts │ │ ├── boat-relay.server.ts │ │ ├── check-auth.ts │ │ ├── check-mod.ts │ │ ├── cookies.server.ts │ │ ├── create-post-notification.ts │ │ ├── create-post.ts │ │ ├── create-tags-query.ts │ │ ├── days.ts │ │ ├── ellipsize.ts │ │ ├── files.ts │ │ ├── get-moderators.ts │ │ ├── include-votes.ts │ │ ├── is-image.ts │ │ ├── jwt.server.ts │ │ ├── linkify-mentions.ts │ │ ├── logout.server.ts │ │ ├── mail.server.ts │ │ ├── parlon-context.tsx │ │ ├── password.server.ts │ │ ├── prisma.server.ts │ │ ├── random-str.ts │ │ ├── remove-code-trail.ts │ │ ├── render-bio.server.ts │ │ ├── render-stripped.server.ts │ │ ├── render-summary.server.ts │ │ ├── render.server.ts │ │ ├── request-state.ts │ │ ├── request-status.ts │ │ ├── responses.ts │ │ ├── restrict-usernames.ts │ │ ├── s3.server.ts │ │ ├── send-email-verification.ts │ │ ├── slugify.ts │ │ ├── tag-use-data.ts │ │ ├── time.ts │ │ ├── upload-media.ts │ │ ├── use-async-fetcher.ts │ │ ├── use-audio-recorder.ts │ │ ├── use-color-scheme.ts │ │ ├── use-comments.ts │ │ ├── use-countdown.ts │ │ ├── use-courses.ts │ │ ├── use-mounted.ts │ │ ├── use-post-people.ts │ │ ├── use-programmes.ts │ │ ├── use-tag-courses.ts │ │ ├── use-tag-programmes.ts │ │ ├── username-regex.tsx │ │ ├── values.server.ts │ │ └── with-user-prefs.ts │ ├── root.tsx │ ├── routes │ │ ├── _index.tsx │ │ ├── account-created.tsx │ │ ├── comments.ts │ │ ├── communities.tsx │ │ ├── communities_.$slug.members.tsx │ │ ├── communities_.$slug.mod.tsx │ │ ├── communities_.$slug.tsx │ │ ├── communities_.created.tsx │ │ ├── communities_.new.tsx │ │ ├── courses.tsx │ │ ├── create-account.tsx │ │ ├── discussions.tsx │ │ ├── discussions_.$id.tsx │ │ ├── discussions_.$id_.$.tsx │ │ ├── downloads.events.$id.tsx │ │ ├── downloads.timetable.$year.$programme.$level.$sem.tsx │ │ ├── events.tsx │ │ ├── events_.$id.tsx │ │ ├── events_.add.tsx │ │ ├── forgot-password.tsx │ │ ├── games.tsx │ │ ├── instructors.tsx │ │ ├── lessons.$id.tsx │ │ ├── library.tsx │ │ ├── library_.$id.tsx │ │ ├── login.tsx │ │ ├── logout.tsx │ │ ├── manifest[.]webmanifest.ts │ │ ├── market.tsx │ │ ├── market_.$id.tsx │ │ ├── market_.$id_.edit.tsx │ │ ├── market_.add.tsx │ │ ├── market_.profile.tsx │ │ ├── md.ts │ │ ├── media.tsx │ │ ├── notifications.tsx │ │ ├── notifications_.$id.tsx │ │ ├── p.$username.catalog.tsx │ │ ├── p.$username.communities.tsx │ │ ├── p.$username.tsx │ │ ├── parlon.call.tsx │ │ ├── parlon.tsx │ │ ├── people.ts │ │ ├── programmes.tsx │ │ ├── resend-verification.tsx │ │ ├── reset-password.tsx │ │ ├── timetable.tsx │ │ ├── timetable_.$year.$programme.$level.$sem.$day.add.tsx │ │ ├── timetable_.$year.$programme.$level.$sem.$day.tsx │ │ ├── verify-email.tsx │ │ └── vote.$post.ts │ └── style.css ├── env.d.ts ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240121162725_init │ │ │ └── migration.sql │ │ ├── 20240123221212_rename_code_add_location │ │ │ └── migration.sql │ │ ├── 20240202080342_add_user │ │ │ └── migration.sql │ │ ├── 20240202080501_add_timestamp │ │ │ └── migration.sql │ │ ├── 20240202155452_add_verified_field │ │ │ └── migration.sql │ │ ├── 20240203071938_password_reset_email_verification │ │ │ └── migration.sql │ │ ├── 20240203081443_add_used │ │ │ └── migration.sql │ │ ├── 20240203202105_add_posts │ │ │ └── migration.sql │ │ ├── 20240203202520_add_counts │ │ │ └── migration.sql │ │ ├── 20240204090729_votes │ │ │ └── migration.sql │ │ ├── 20240204152813_vote_cascade │ │ │ └── migration.sql │ │ ├── 20240204153235_vote_delete_noaction │ │ │ └── migration.sql │ │ ├── 20240204155458_soft_delete │ │ │ └── migration.sql │ │ ├── 20240205144235_media │ │ │ └── migration.sql │ │ ├── 20240205144512_media_field │ │ │ └── migration.sql │ │ ├── 20240205145508_add_file_name │ │ │ └── migration.sql │ │ ├── 20240207103046_add_tags │ │ │ └── migration.sql │ │ ├── 20240304200456_events │ │ │ └── migration.sql │ │ ├── 20240304201721_event_date │ │ │ └── migration.sql │ │ ├── 20240304205118_event_poster │ │ │ └── migration.sql │ │ ├── 20240311163736_repository │ │ │ └── migration.sql │ │ ├── 20240311164350_repository_tags │ │ │ └── migration.sql │ │ ├── 20240404130325_post_path │ │ │ └── migration.sql │ │ ├── 20240406004831_notifications │ │ │ └── migration.sql │ │ ├── 20240413062208_use_singular │ │ │ └── migration.sql │ │ ├── 20240413063354_rename_typo_notification_subscriber │ │ │ └── migration.sql │ │ ├── 20240413233932_nocase_username │ │ │ └── migration.sql │ │ ├── 20240421170420_communities │ │ │ └── migration.sql │ │ ├── 20240421173802_communities_post │ │ │ └── migration.sql │ │ ├── 20240423165936_communities_unique_index │ │ │ └── migration.sql │ │ ├── 20240423192541_community_post │ │ │ └── migration.sql │ │ ├── 20240425224659_bio │ │ │ └── migration.sql │ │ ├── 20240505180831_market │ │ │ └── migration.sql │ │ ├── 20240505190217_category_title_unique │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── seed.mjs ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── icons │ │ ├── android-chrome-256x256.png │ │ ├── android-chrome-512x512.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ └── apple-touch-icon.png │ ├── sym-outline.svg │ ├── sym.svg │ └── zasplat_connected_ding.mp3 ├── res │ ├── knust.json │ ├── ug.json │ └── umat.json ├── tsconfig.json ├── uno.config.ts └── vite.config.ts ├── launch.sh ├── package.json ├── website ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── public │ ├── CNAME │ ├── compa-swag.svg │ ├── favicon.ico │ ├── favicon.svg │ ├── sym-outline.svg │ └── sym.svg ├── src │ ├── components │ │ ├── CompaSwag.astro │ │ ├── Footer.astro │ │ ├── LogoDuo.astro │ │ ├── Navbar.astro │ │ └── tmp.astro │ ├── env.d.ts │ ├── layouts │ │ └── Shell.astro │ ├── pages │ │ ├── index.astro │ │ └── schools.astro │ └── styles.css ├── tsconfig.json └── uno.config.ts └── yarn.lock /.github/workflows/deploy-app.yml: -------------------------------------------------------------------------------- 1 | name: Deploy App 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'client/**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | max-parallel: 1 16 | matrix: 17 | instance: ['knust', 'ug', 'umat'] 18 | steps: 19 | - name: Cloning repo 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Push to dokku 25 | uses: dokku/github-action@master 26 | # enable verbose ssh output 27 | env: 28 | GIT_SSH_COMMAND: 'ssh -vvv' 29 | with: 30 | # enable verbose git output 31 | git_push_flags: '-vvv' 32 | git_remote_url: 'ssh://dokku@85.159.211.246:22/${{ matrix.instance }}' 33 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} 34 | # enable shell trace mode 35 | trace: '1' 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website to GitHub Pages 2 | 3 | on: 4 | # Trigger the workflow every time you push to the `main` branch 5 | # Using a different branch name? Replace `main` with your branch’s name 6 | push: 7 | branches: [ master ] 8 | paths: 9 | - 'website/**' 10 | # Allows you to run this workflow manually from the Actions tab on GitHub. 11 | workflow_dispatch: 12 | 13 | # Allow this job to clone the repo and create a page deployment 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 1 27 | 28 | - name: Print commit id, message and tag 29 | run: | 30 | git show -s --format='%h %s' 31 | echo "github.ref -> {{ github.ref }}" 32 | 33 | - name: Set up Node.js and Yarn 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: '20.10.0' 37 | registry-url: 'https://registry.yarnpkg.com' 38 | 39 | - name: Install dependencies 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Build 43 | run: yarn build:website 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v1 47 | with: 48 | path: ./website/dist 49 | 50 | deploy: 51 | needs: build 52 | runs-on: ubuntu-latest 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | steps: 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "[prisma]": { 4 | "editor.defaultFormatter": "Prisma.prisma" 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | } 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

The COMPA Initiative

4 |

compa.so

5 | 6 |

7 | 8 | # Compa 9 | 10 | A companion application for students to manage and access resources at their higher education institution. 11 | 12 | ## About 13 | 14 | As a fresher, settling into school could be easier. You usually need to find: 15 | 16 | - Communities/Clubs you are interested in 17 | - Semester timetables 18 | - Resources from past semesters 19 | 20 | For continuing students, it's just as hard to find documents or class notes when conversations are scattered across multiple platforms. 21 | 22 | Being a student is hard enough, that's why Compa aims to be the go-to resource for higher education institutions and save you from one source of stress. 23 | 24 | Your open, compact, companion and compass. That is _Compa_. 25 | 26 | ## Features 27 | 28 | - [x] Timetable: See the lecture schedule for a semester and import it to your calendar. ❇️ 29 | 30 | - [x] Discussions: Ask questions, share ideas and interact with other students. 31 | 32 | - [ ] Communities: Find and join groups that interest you. 33 | 34 | ## Schools 35 | 36 | - [x] Kwame Nkrumah University of Science and Technology (KNUST) - 37 | - [x] University of Ghana (UG) - 38 | - [x] University of Mines and Technology (UMAT) - 39 | 40 | ### Deploy an instance for your school 41 | 42 | If you'd like to deploy an instance for your school: 43 | 44 | 1. Submit a PR with the title: `School Request: `. The PR should be submitted with a file in `client/res` named `.json`. The file's content should follow the format in [knust.json](/client/res/knust.json) 45 | 46 | 1. After your PR is reviewed and merged, click on the **Deploy on Railway** button below to deploy your instance. 47 | 48 | 1. Share your app's IP with us under the same PR so we can add a subdomain to compa for your school. 49 | 50 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/VCnpoP) 51 | 52 | Note that you bear the cost of hosting which is $5/month on Railway. You'll also need to set up an AWS compatible bucket. I recommend [Linode Object Storage](https://www.linode.com/docs/guides/platform/object-storage/) which also costs $5/month. 53 | 54 | For email, [Resend](https://resend.com) is used. It's free for 100 mails per day. 55 | 56 | > If you need any help, please reach us by mail mail@degreat.co.uk 57 | 58 | ## Run locally 59 | 60 | Clone the project: 61 | 62 | ```bash 63 | git clone https://github.com/blackmann/compa 64 | cd compa 65 | ``` 66 | 67 | Run the following command to install dependencies and setup Prisma migrations: 68 | 69 | ```bash 70 | yarn setup:all 71 | ``` 72 | 73 | Start the project in dev mode: 74 | 75 | ```bash 76 | yarn dev:client 77 | ``` 78 | 79 | ## Contributing 80 | 81 | Contributions are always welcome! We don't have a code of conduct right now, but we will soon! 82 | 83 | ## Roadmap 84 | 85 | Coming soon... 86 | -------------------------------------------------------------------------------- /assets/sc-timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/compa/f2fbdd0379272ef2a4c69a0881488c9ad3223fae/assets/sc-timetable.png -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "suspicious": { 11 | "noExplicitAny": "off" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="file:./dev.db" 8 | SCHOOL=knust 9 | COOKIE_SECRET=secret1,secret2 10 | SECRET_KEY=secret1 11 | RESEND_API_KEY=123 12 | AWS_UPLOAD_ENDPOINT="eu-central-1.linodeobjects.com" 13 | AWS_REGION="eu-central-1" 14 | AWS_ACCESS_KEY_ID= 15 | AWS_SECRET_ACCESS_KEY= 16 | AWS_BUCKET=compa 17 | AWS_BUCKET_DIR=compa # eg. knust-compa, ug-compa 18 | VITE_OBSERVE_APP_ID= # observe.so 19 | VITE_BOAT_URL=http://localhost:3003 20 | VITE_BOAT_SIGNAL=ws://localhost:3003 21 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | /public/entry.worker.js 7 | 8 | dev.db 9 | dev.db-journal 10 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # compa client 2 | 3 | ## Common dev issues 4 | 5 | When you see an error like below when running `yarn dev`: 6 | 7 | ``` 8 | Error: Could not load the "sharp" module using the darwin-arm64 runtime 9 | ``` 10 | 11 | Run the following command to remedy it: 12 | 13 | ```sh 14 | yarn workspace client add sharp --ignore-engines 15 | ``` 16 | -------------------------------------------------------------------------------- /client/app/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | import { RemixLinkProps } from "@remix-run/react/dist/components"; 3 | import clsx from "clsx"; 4 | import React from "react"; 5 | 6 | interface Props extends RemixLinkProps { 7 | variant?: "primary" | "neutral"; 8 | } 9 | 10 | const Anchor = React.forwardRef( 11 | ({ className, variant = "primary", ...props }, ref) => { 12 | return ( 13 | 25 | ); 26 | }, 27 | ); 28 | 29 | export { Anchor }; 30 | -------------------------------------------------------------------------------- /client/app/components/audio-item.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | import { ellipsizeFilename, humanizeSize } from "~/lib/files"; 4 | 5 | interface Props { 6 | url: string; 7 | name: string; 8 | size: number; 9 | onRemove?: VoidFunction; 10 | noPlay?: boolean; 11 | } 12 | 13 | type DownloadState = "idle" | "downloading" | "downloaded"; 14 | 15 | function AudioItem({ name, url, noPlay, onRemove, size }: Props) { 16 | const [playing, setPlaying] = React.useState(); 17 | const [downloadState, setDownloadState] = 18 | React.useState("idle"); 19 | 20 | const audioRef = React.useRef(); 21 | 22 | function togglePlay(e: React.MouseEvent) { 23 | if (noPlay) { 24 | return; 25 | } 26 | 27 | e.preventDefault(); 28 | e.stopPropagation(); 29 | 30 | if (downloadState === "downloading") { 31 | return; 32 | } 33 | 34 | if (downloadState !== "downloaded") { 35 | setDownloadState("downloading"); 36 | 37 | fetch(url, { mode: "no-cors" }) 38 | .then(async (res) => { 39 | // this will bring the audio to cache 40 | const audio = new Audio(url); 41 | audioRef.current = audio; 42 | 43 | audio.addEventListener("ended", () => { 44 | setPlaying(false); 45 | }); 46 | 47 | setDownloadState("downloaded"); 48 | }) 49 | .catch(() => setDownloadState("idle")); 50 | 51 | return; 52 | } 53 | 54 | if (!audioRef.current) { 55 | return; 56 | } 57 | 58 | playing ? audioRef.current.pause() : audioRef.current.play(); 59 | setPlaying(!playing); 60 | } 61 | 62 | React.useEffect(() => { 63 | return () => { 64 | audioRef.current?.pause(); 65 | }; 66 | }, []); 67 | 68 | return ( 69 |
70 |
71 | 85 |
86 | 87 |
88 |
89 | {ellipsizeFilename(name)} 90 |
91 |
92 | {humanizeSize(size)} 93 |
94 |
95 | 96 | {onRemove && ( 97 |
98 | 105 |
106 | )} 107 |
108 | ); 109 | } 110 | 111 | export { AudioItem }; 112 | -------------------------------------------------------------------------------- /client/app/components/audio-recorder.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | import { useAudioRecorder } from "~/lib/use-audio-recorder"; 4 | 5 | interface Props { 6 | onRecorded?: (blob: Blob) => void; 7 | onRecording?: (recording: boolean) => void; 8 | } 9 | 10 | function AudioRecorder({ onRecorded, onRecording }: Props) { 11 | const { 12 | isPaused, 13 | isRecording, 14 | recordingTime, 15 | startRecording, 16 | stopRecording, 17 | togglePauseResume, 18 | recordingBlob, 19 | clear, 20 | } = useAudioRecorder({ 21 | echoCancellation: true, 22 | }); 23 | 24 | React.useEffect(() => { 25 | if (recordingBlob) { 26 | onRecorded?.(recordingBlob); 27 | clear(); 28 | } 29 | }, [clear, recordingBlob, onRecorded]); 30 | 31 | React.useEffect(() => { 32 | onRecording?.(isRecording); 33 | }, [isRecording, onRecording]); 34 | 35 | return ( 36 |
42 | {isRecording ? ( 43 | <> 44 | {!isPaused ? ( 45 |
46 | ) : ( 47 |
48 | )} 49 |
50 | {secondsToMinuteSeconds(recordingTime)} 51 |
52 | 53 | 60 | 61 | 64 | 65 | ) : ( 66 | 74 | )} 75 |
76 | ); 77 | } 78 | 79 | function secondsToMinuteSeconds(seconds: number) { 80 | const minute = Math.floor(seconds / 60); 81 | const second = seconds % 60; 82 | 83 | return `${minute}:${second.toString().padStart(2, "0")}`; 84 | } 85 | 86 | export { AudioRecorder }; 87 | -------------------------------------------------------------------------------- /client/app/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import BoringAvatar, { type AvatarProps } from "boring-avatars"; 2 | import clsx from "clsx"; 3 | 4 | interface Props { 5 | name: string; 6 | size?: number; 7 | className?: string; 8 | square?: boolean; 9 | variant?: AvatarProps["variant"]; 10 | } 11 | 12 | const colors = ["#ffe12e", "#4d8c3a", "#0060ff", "#ff7d10", "#4e412b"]; 13 | 14 | const BA = 15 | typeof BoringAvatar.default !== "undefined" 16 | ? BoringAvatar.default 17 | : BoringAvatar; 18 | 19 | function Avatar({ 20 | className, 21 | name, 22 | size = 28, 23 | square, 24 | variant = "beam", 25 | }: Props) { 26 | return ( 27 |
28 | 35 |
36 | ); 37 | } 38 | 39 | export { Avatar }; 40 | -------------------------------------------------------------------------------- /client/app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | 4 | interface Props extends React.ComponentProps<"button"> { 5 | variant?: "primary" | "neutral" | "secondary"; 6 | } 7 | 8 | const Button = React.forwardRef( 9 | ({ className, variant = "primary", ...props }, ref) => { 10 | return ( 11 |