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