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" } }