From d63c9eeecff6c1c55afc81872b440677994c94f7 Mon Sep 17 00:00:00 2001 From: Can Sirin Date: Sun, 23 Jul 2023 15:07:34 -0700 Subject: [PATCH] feat(@kampus/ui-next): Add other Shadcn components (#501) # Description Adds shadcn components for our ui library. Added components: - Time ago - User avatar - Theme provider - Theme toggle - Social Media Buttons ### Checklist - [x] discord username: `username#0001` - [x] Closes #412 - [x] Closes #505 - [x] PR must be created for an issue from issues under "In progress" column from [our project board](https://github.com/orgs/kamp-us/projects/2/views/1). - [x] A descriptive and understandable title: The PR title should clearly describe the nature and purpose of the changes. The PR title should be the first thing displayed when the PR is opened. And it should follow the semantic commit rules, and should include the app/package/service name in the title. For example, a title like "docs(@kampus-apps/pano): Add README.md" can be used. - [x] Related file selection: Only relevant files should be touched and no other files should be affected. - [ ] I ran `npx turbo run` at the root of the repository, and build was successful. - [x] I installed the npm packages using `npm install --save-exact ` so my package is pinned to a specific npm version. Leave empty if no package was installed. Leave empty if no package was installed with this PR. ### How were these changes tested? Please describe the tests you did to test the changes you made. Please also specify your test configuration. --- .../ui/.storybook/{preview.ts => preview.tsx} | 15 +++++ apps/ui/stories/Avatar.stories.tsx | 17 +++--- .../ui/stories/SocialMediaButtons.stories.tsx | 28 ++++++++++ apps/ui/stories/ThemeToggle.stories.tsx | 12 ++++ apps/ui/stories/TimeAgo.stories.tsx | 15 +++++ package-lock.json | 56 ++++++++++++++++++- packages/ui/components/index.ts | 5 ++ .../ui/components/social-media-buttons.tsx | 15 +++-- packages/ui/components/theme-provider.tsx | 9 +++ packages/ui/components/theme-toggle.tsx | 38 +++++++++++++ packages/ui/components/time-ago.tsx | 50 +++++++++++++++++ packages/ui/components/user-avatar.tsx | 23 ++++++++ packages/ui/package.json | 4 +- 13 files changed, 271 insertions(+), 16 deletions(-) rename apps/ui/.storybook/{preview.ts => preview.tsx} (58%) create mode 100644 apps/ui/stories/SocialMediaButtons.stories.tsx create mode 100644 apps/ui/stories/ThemeToggle.stories.tsx create mode 100644 apps/ui/stories/TimeAgo.stories.tsx rename deprecated/kampus-ui/src/SocialMediaButtons.tsx => packages/ui/components/social-media-buttons.tsx (61%) create mode 100644 packages/ui/components/theme-provider.tsx create mode 100644 packages/ui/components/theme-toggle.tsx create mode 100644 packages/ui/components/time-ago.tsx create mode 100644 packages/ui/components/user-avatar.tsx diff --git a/apps/ui/.storybook/preview.ts b/apps/ui/.storybook/preview.tsx similarity index 58% rename from apps/ui/.storybook/preview.ts rename to apps/ui/.storybook/preview.tsx index 1b24caa1..35a0e84c 100644 --- a/apps/ui/.storybook/preview.ts +++ b/apps/ui/.storybook/preview.tsx @@ -1,6 +1,11 @@ import "@kampus/kampus/app/globals.css"; import "../styles.css"; +import React from "react"; +import type { StoryContext, StoryFn } from "@storybook/react"; + +import { ThemeProvider } from "@kampus/ui-next/components/theme-provider"; + export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { @@ -27,3 +32,13 @@ export const parameters = { ], }, }; + +const withThemeProvider = (StoryFn: StoryFn) => { + return ( + + + + ); +}; + +export const decorators = [withThemeProvider]; diff --git a/apps/ui/stories/Avatar.stories.tsx b/apps/ui/stories/Avatar.stories.tsx index ce294fd0..8eb35274 100644 --- a/apps/ui/stories/Avatar.stories.tsx +++ b/apps/ui/stories/Avatar.stories.tsx @@ -1,19 +1,18 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Avatar, AvatarFallback, AvatarImage } from "@kampus/ui-next/components/avatar"; +import { UserAvatar } from "@kampus/ui-next/components/user-avatar"; const meta = { - component: Avatar, -} satisfies Meta; + component: UserAvatar, +} satisfies Meta; export default meta; type Story = StoryObj; export const Default = { - render: () => ( - - - K - - ), + render: ({ login, src }) => , + args: { + login: "kampus", + src: "https://github.com/kamp-us.png", + }, } satisfies Story; diff --git a/apps/ui/stories/SocialMediaButtons.stories.tsx b/apps/ui/stories/SocialMediaButtons.stories.tsx new file mode 100644 index 00000000..52c44911 --- /dev/null +++ b/apps/ui/stories/SocialMediaButtons.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + FacebookShare, + RedditShare, + TwitterShare, +} from "@kampus/ui-next/components/social-media-buttons"; + +const meta = { + component: TwitterShare || FacebookShare || RedditShare, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = { + args: { + postUrl: "https://kampus.rs", + size: 32, + }, + render: ({ postUrl, size }) => ( +
+ + + +
+ ), +} satisfies Story; diff --git a/apps/ui/stories/ThemeToggle.stories.tsx b/apps/ui/stories/ThemeToggle.stories.tsx new file mode 100644 index 00000000..ced18a80 --- /dev/null +++ b/apps/ui/stories/ThemeToggle.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { ThemeToggle } from "@kampus/ui-next/components/theme-toggle"; + +const meta = { + component: ThemeToggle, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/apps/ui/stories/TimeAgo.stories.tsx b/apps/ui/stories/TimeAgo.stories.tsx new file mode 100644 index 00000000..640092e1 --- /dev/null +++ b/apps/ui/stories/TimeAgo.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { TimeAgo } from "@kampus/ui-next/components/time-ago"; + +const meta = { + component: TimeAgo, + args: { + date: new Date(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/package-lock.json b/package-lock.json index 23cc3153..cefbaa96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10920,6 +10920,11 @@ } } }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-stack": { "version": "2.2.0", "dev": true, @@ -15987,6 +15992,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "dependencies": { + "debug": "^2.1.3" + } + }, + "node_modules/jsonp/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/jsonp/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "license": "MIT", @@ -17694,6 +17720,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.14", "funding": [ @@ -19866,6 +19902,22 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-share": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.1.tgz", + "integrity": "sha512-AJ9m9RiJssqvYg7MoJUc9J0D7b/liWrsfQ99ndKc5vJ4oVHHd4Fy87jBlKEQPibT40oYA3AQ/a9/oQY6/yaigw==", + "dependencies": { + "classnames": "^2.3.2", + "jsonp": "^0.2.1" + }, + "engines": { + "node": ">=6.9.0", + "npm": ">=5.0.0" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17 || ^18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "license": "MIT", @@ -24047,7 +24099,9 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", "class-variance-authority": "0.6.0", - "lucide-react": "0.241.0" + "lucide-react": "0.241.0", + "next-themes": "0.2.1", + "react-share": "4.4.1" }, "devDependencies": { "@kampus/tailwind": "*" diff --git a/packages/ui/components/index.ts b/packages/ui/components/index.ts index 845c3f8d..e271e06b 100644 --- a/packages/ui/components/index.ts +++ b/packages/ui/components/index.ts @@ -3,3 +3,8 @@ export * from "./input-with-button"; export * from "./input"; export * from "./button"; export * from "./top-nav"; +export * from "./theme-provider"; +export * from "./theme-toggle"; +export * from "./user-avatar"; +export * from "./time-ago"; +export * from "./social-media-buttons"; diff --git a/deprecated/kampus-ui/src/SocialMediaButtons.tsx b/packages/ui/components/social-media-buttons.tsx similarity index 61% rename from deprecated/kampus-ui/src/SocialMediaButtons.tsx rename to packages/ui/components/social-media-buttons.tsx index b7e72b6f..3a5499a5 100644 --- a/deprecated/kampus-ui/src/SocialMediaButtons.tsx +++ b/packages/ui/components/social-media-buttons.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FacebookIcon, FacebookShareButton, @@ -7,12 +9,13 @@ import { TwitterShareButton, } from "react-share"; -type SocialMediaButtonProps = { +interface SocialMediaButtonProps { postUrl: string; size: number; -}; +} -export const FacebookShare = ({ postUrl, size }: SocialMediaButtonProps) => { +export const FacebookShare = (props: SocialMediaButtonProps) => { + const { postUrl, size } = props; return ( @@ -20,7 +23,8 @@ export const FacebookShare = ({ postUrl, size }: SocialMediaButtonProps) => { ); }; -export const TwitterShare = ({ postUrl, size }: SocialMediaButtonProps) => { +export const TwitterShare = (props: SocialMediaButtonProps) => { + const { postUrl, size } = props; return ( @@ -28,7 +32,8 @@ export const TwitterShare = ({ postUrl, size }: SocialMediaButtonProps) => { ); }; -export const RedditShare = ({ postUrl, size }: SocialMediaButtonProps) => { +export const RedditShare = (props: SocialMediaButtonProps) => { + const { postUrl, size } = props; return ( diff --git a/packages/ui/components/theme-provider.tsx b/packages/ui/components/theme-provider.tsx new file mode 100644 index 00000000..b0ff2660 --- /dev/null +++ b/packages/ui/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { type ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/packages/ui/components/theme-toggle.tsx b/packages/ui/components/theme-toggle.tsx new file mode 100644 index 00000000..23bd5df3 --- /dev/null +++ b/packages/ui/components/theme-toggle.tsx @@ -0,0 +1,38 @@ +"use client"; + +import * as React from "react"; +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { cn } from "../utils"; +import { Button } from "./button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./dropdown-menu"; + +export function ThemeToggle() { + const { setTheme } = useTheme(); + + const baseClasses = "h-[1.2rem] w-[1.2rem] transition all"; + + return ( + + + + + + setTheme("light")}>Light + setTheme("dark")}>Dark + setTheme("system")}>System + + + ); +} diff --git a/packages/ui/components/time-ago.tsx b/packages/ui/components/time-ago.tsx new file mode 100644 index 00000000..ae94a6cc --- /dev/null +++ b/packages/ui/components/time-ago.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const formatter = new Intl.RelativeTimeFormat("tr", { + // numeric: "auto", +}); + +const DIVISIONS = [ + { amount: 60, name: "seconds" }, + { amount: 60, name: "minutes" }, + { amount: 24, name: "hours" }, + { amount: 7, name: "days" }, + { amount: 4.34524, name: "weeks" }, + { amount: 12, name: "months" }, + { amount: Number.POSITIVE_INFINITY, name: "years" }, +]; + +export function formatTimeAgo(date: Date) { + const now = new Date(); + let duration = (date.getTime() - now.getTime()) / 1000; + + for (let i = 0; i <= DIVISIONS.length; i++) { + const division = DIVISIONS[i] as (typeof DIVISIONS)[number]; + if (Math.abs(duration) < division.amount) { + return formatter.format(Math.round(duration), division.name as Intl.RelativeTimeFormatUnit); + } + duration /= division.amount; + } +} + +interface TimeagoProps { + date: Date; +} + +export const TimeAgo = (props: TimeagoProps) => { + const [timeAgo, setTimeAgo] = useState(formatTimeAgo(props.date)); + + useEffect(() => { + const timer = setInterval(() => { + setTimeAgo(formatTimeAgo(props.date)); + }, 10000); + + return () => clearInterval(timer); + }, [props.date]); + + // If we don't suppress the hydration warning, we get this error: https://nextjs.org/docs/messages/react-hydration-error + // https://nextjs.org/docs/messages/react-hydration-error#solution-3-using-suppresshydrationwarning + return
{timeAgo}
; +}; diff --git a/packages/ui/components/user-avatar.tsx b/packages/ui/components/user-avatar.tsx new file mode 100644 index 00000000..e00d5c3c --- /dev/null +++ b/packages/ui/components/user-avatar.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React, { forwardRef } from "react"; + +import { Avatar, AvatarFallback, AvatarImage } from "./avatar"; + +export interface UserAvatarProps { + login: string; + src?: string; +} + +export const UserAvatar = forwardRef(({ login, src }, ref) => { + const firstLetter = login.charAt(0).toUpperCase(); + + return ( + + + {firstLetter} + + ); +}); + +UserAvatar.displayName = "UserAvatar"; diff --git a/packages/ui/package.json b/packages/ui/package.json index 951226de..4f1b490e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,6 +26,8 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", "class-variance-authority": "0.6.0", - "lucide-react": "0.241.0" + "lucide-react": "0.241.0", + "next-themes": "0.2.1", + "react-share": "4.4.1" } }