Merge pull request #109 from BRAVO68WEB/feature/api-integration

Feature/api integration
This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-06-25 01:47:16 +05:30 committed by GitHub
commit f65cac9912
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1511 additions and 144 deletions

View File

@ -27,11 +27,11 @@ class ApiSdk {
if (typeof window === 'undefined') {
const { cookies } = await import('next/headers');
config.headers['x-shx-api-key'] = cookies().get('apiKey')?.value;
config.baseURL = cookies().get('instanceUrl')?.value;
} else {
config.headers['x-shx-api-key'] = Cookies.get().apiKey ?? '';
config.baseURL = Cookies.get('instanceUrl') ?? '';
}
config.baseURL = process.env.NEXT_PUBLIC_INSTANCE_URL
return config;
});

View File

@ -6,24 +6,27 @@ export class Notes {
constructor(axios: Axios) {
this.axios = axios;
}
async getAllNotes(search?:string) {
const res = await this.axios.get('/gist',{params:{search}});
async getAllNotes(search?: string) {
const res = await this.axios.get('/gist', { params: { search } });
const data = res.data.data as INote[];
return data;
}
async getSingleNote(id: string, passkey = '') {
const res = await this.axios.get(`/gist/${id}`, {
params: passkey ? { passkey } : {},
});
const data = res.data.data as INote;
return data;
}
async uploadSingleNote(data: AddNoteType) {
console.log("uploading")
if(!data.passkey){
delete data.passkey
}
console.log('uploading');
if (!data.passkey) {
delete data.passkey;
}
const res = await this.axios.post('/gist', data);
return res.data.data as INote;
}
async deleteSingleNote({
noteID,
}: {
noteID: string;
}) {
async deleteSingleNote({ noteID }: { noteID: string }) {
const res = await this.axios.delete(`/gist/${noteID}`);
return res;
}

View File

@ -14,6 +14,12 @@ export class Settings {
const res = await this.axios.post('/settings', {key, value});
return res
}
async getInstanceInfo(){
const res = await this.axios.get('/info/sys');
delete res.data.data.cpuUsage
delete res.data.data.memoryUsage
return res.data.data as ISysSettings
}
}
export default Settings;

View File

@ -0,0 +1,11 @@
"use client"
import React from 'react'
function ErrorPage() {
return (
<div>ErrorPage</div>
)
}
export default ErrorPage

View File

@ -1,23 +1,30 @@
import { sidebarGroups } from '@/lib/sidebar';
import { cookies } from 'next/headers';
import Link from 'next/link';
import React from 'react';
import { redirect } from 'next/navigation';
interface DashboardLayoutProps {
children: React.ReactNode;
}
const Layout = ({ children }: DashboardLayoutProps) => {
const cookieList = cookies();
const apiKey = cookieList.has('apiKey');
const masterKey = cookieList.has('masterKey');
if (!apiKey || !masterKey) redirect('/');
return (
<div className="flex h-screen">
<div className="sidebar w-full max-w-xs h-screen bg-black flex flex-col overflow-x-hidden overflow-y-auto">
{sidebarGroups.map((sidebarGrp, index) => {
return (
<div key={index} className="menu-group p-4">
<p className="title text-lg text-primary mb-2">{sidebarGrp.name}</p>
<p className="title text-lg text-primary mb-2">
{sidebarGrp.name}
</p>
{sidebarGrp.items.map((item, index) => {
return (
<Link
key={index}
key={index}
href={item.href}
className="p-2.5 block text-sm hover:bg-gray-900 w-full rounded-md"
>
@ -29,7 +36,7 @@ const Layout = ({ children }: DashboardLayoutProps) => {
);
})}
</div>
<main className='w-full h-full overflow-y-auto p-5'>{children}</main>
<main className="w-full h-full overflow-y-auto p-5">{children}</main>
</div>
);
};

View File

@ -1,9 +1,14 @@
import Markdown from '@/components/Markdown';
import axios from 'axios';
import React from 'react';
const Dashboard = () => {
const Dashboard = async () => {
const { data } = await axios.get(
'https://raw.githubusercontent.com/BRAVO68WEB/shx/dev/README.md'
);
return (
<>
<h1 className="text-5xl">Dasbhboard</h1>
<Markdown markdown={data} />
</>
);
};

View File

@ -1,37 +1,94 @@
'use client';
import Button from '@/components/ui/Button';
import axios from 'axios';
import Cookies from 'js-cookie';
import { Download } from 'lucide-react';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
const configs = [
{
name: 'Lorem Ipsum Config',
name: 'Image Config',
filename: 'image.sxcu',
url: `${process.env.NEXT_PUBLIC_INSTANCE_URL}/config/image.sxcu?apikey=`,
id: '1',
},
{
name: 'Lorem Cnofig',
id: '12',
name: 'Gist Config',
filename: 'gist.sxcu',
url: `${process.env.NEXT_PUBLIC_INSTANCE_URL}/config/gist.sxcu?apikey=`,
id: '2',
},
{
name: 'Lorem Config Config',
id: '13',
name: 'URL Config',
filename: 'url.sxcu',
url: `${process.env.NEXT_PUBLIC_INSTANCE_URL}/config/url.sxcu?apikey=`,
id: '3',
},
{
name: 'Lorem ',
id: '14',
name: 'File Config',
filename: 'file.sxcu',
url: `${process.env.NEXT_PUBLIC_INSTANCE_URL}/config/file.sxcu?apikey=`,
id: '4',
},
];
const downloadFile = ({
data,
fileName,
fileType,
}: {
data: string;
fileName: string;
fileType: string;
}) => {
// Create a blob with the data we want to download as a file
const blob = new Blob([data], { type: fileType });
// Create an anchor element and dispatch a click event on it
// to trigger a download
const a = document.createElement('a');
a.download = fileName;
a.href = window.URL.createObjectURL(blob);
const clickEvt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
});
a.dispatchEvent(clickEvt);
a.remove();
};
function Page() {
const [apiKey, setApiKey] = useState('');
useEffect(() => {
setApiKey(Cookies.get('apiKey') ?? '');
},[]);
async function onDownloadFile(url: string, filename: string) {
try {
const { data } = await axios.get(url);
console.log(data);
downloadFile({
data: JSON.stringify(data),
fileName: filename,
fileType: 'text/json',
});
} catch {
toast.error('Error downloading file');
}
}
return (
<>
{configs.map(({ name, id }) => {
{configs.map(({ name, id, url, filename }) => {
return (
<div
key={id}
className="w-full my-3 p-4 flex items-center justify-between bg-gray-900 rounded"
>
<p>{name}</p>
<Button size={'icon'} variant={'transparent'}>
<Button
size={'icon'}
onClick={() => onDownloadFile(`${url}${apiKey}`, filename)}
>
<Download />
</Button>
</div>

View File

@ -1,13 +1,9 @@
import api from '@/api';
import React from 'react';
function Page() {
const urls = [
{
id: 'afdas',
originalURL: 'https://www.google.com',
shortenedURL: 'https://www.google.com',
},
];
async function Page() {
const res = await api.settings.getInstanceInfo();
const data = Object.entries(res);
return (
<div>
@ -29,19 +25,13 @@ function Page() {
</tr>
</thead>
<tbody className="divide-y p-2">
{urls.map(({ originalURL, shortenedURL, id }) => (
{data.map((val, id) => (
<tr className="bg-gray-900 rounded" key={id}>
<td className="whitespace-nowrap py-5 pl-4 pr-20 text-sm font-medium text-white">
<div className="flex items-center gap-3">
{originalURL}
</div>
<div className="flex items-center gap-3">{val[0]}</div>
</td>
<td className="whitespace-nowrap pl-4 text-sm font-medium text-white">
<div className="flex items-center gap-3">
{shortenedURL}
</div>
<div className="flex items-center gap-3">{val[1]}</div>
</td>
</tr>
))}

View File

@ -2,7 +2,6 @@
import React, { ChangeEventHandler, useEffect, useState } from 'react';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import TagInput from '@/components/TagInput';
import api from '@/api';
import { toast } from 'react-hot-toast';
@ -112,21 +111,16 @@ function Page() {
Save
</Button>
</div>
<div className="flex items-center w-full gap-4">
<Input
id="instance-url"
withLabel
label="Instance URL"
type="text"
className="w-full"
/>
<Button className="w-20 h-min ">Save</Button>
</div>
<div className="flex gap-4">
<TagInput
tags={settings.imageExtensions}
onAddTags={addImageExt}
placeholder="Image Extensions"
onChange={value =>
setSettings(old => {
return { ...old, imageExtensions: value };
})
}
/>
<Button
@ -143,6 +137,11 @@ function Page() {
tags={settings.fileExtensions}
onAddTags={addFileExt}
placeholder="File Extensions"
onChange={value =>
setSettings(old => {
return { ...old, fileExtensions: value };
})
}
/>
<Button
onClick={() =>

View File

@ -1,19 +1,31 @@
*{
margin:0;
padding:0;
box-sizing: border-box;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root{
--primary:#3A86FF;
--secondary:#8338EC;
:root {
--primary: #3a86ff;
--secondary: #8338ec;
}
body{
color:white
body {
color: white;
}
.markdown-body p:nth-child(3) {
display: flex;
gap: 10px;
}
.markdown-body p:nth-child(4) {
display: flex;
gap: 10px;
}
.markdown-body p:nth-child(5) {
display: flex;
gap: 10px;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;

View File

@ -1,22 +1,25 @@
import './globals.css'
import { Source_Code_Pro } from 'next/font/google'
import React from 'react';
import './globals.css';
import { Source_Code_Pro } from 'next/font/google';
import Providers from '@/components/Providers';
const inter = Source_Code_Pro({subsets:['latin']})
const inter = Source_Code_Pro({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
return (
<html lang="en">
<body className={`${inter.className} bg-black text-white min-h-screen`}>{children}</body>
<body className={`${inter.className} bg-black text-white min-h-screen`}>
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@ -0,0 +1,53 @@
'use client';
import api from '@/api';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { Lock } from 'lucide-react';
import React, { FormEventHandler, useState } from 'react';
import { toast } from 'react-hot-toast';
interface NoteContentProps {
data: INote | null;
id: string;
}
function NoteContent({ data, id }: NoteContentProps) {
const [note, setNote] = useState(data);
const [input, setInput] = useState('');
const onSubmit: FormEventHandler = async evt => {
evt.preventDefault();
try {
const data = await api.notes.getSingleNote(id, input);
setNote(data);
} catch (err) {
console.error(err);
toast.error('Wrong passkey');
setNote(null);
}
};
return (
<div className="w-1/2 p-10 border-primary border max-h-full overflow-y-auto">
{note?.content ?? (
<form
onSubmit={onSubmit}
className="flex items-center justify-between gap-4 w-full"
>
<Input
withLabel={true}
placeholder="Enter passkey"
label="Enter passkey"
className="w-full"
onChange={evt => setInput(evt.target.value)}
value={input}
/>
<Button size={'icon'}>
<Lock />
</Button>
</form>
)}
</div>
);
}
export default NoteContent;

View File

@ -0,0 +1,9 @@
import React from 'react'
function Loading() {
return (
<div>Loading</div>
)
}
export default Loading

View File

@ -0,0 +1,25 @@
import api from '@/api';
import React from 'react';
import NoteContent from './NoteContent';
interface NotePageProps {
params: {
id: string;
};
}
async function Page({ params: { id } }: NotePageProps) {
let data: INote | null = null;
try {
data = await api.notes.getSingleNote(id);
} catch {
data = null;
}
return (
<div className="w-full h-screen p-20 flex justify-center items-center">
<NoteContent data={data} id={id} />
</div>
);
}
export default Page;

View File

@ -2,6 +2,9 @@
import api from '@/api';
import Button from '@/components/ui/Button';
import { parseDate } from '@/lib/utils';
import Cookies from 'js-cookie';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
@ -11,9 +14,16 @@ interface ApiKeyListProps {
function ApiKeyList({ data }: ApiKeyListProps) {
const [apiKeys, setApiKeys] = useState(data);
const onDisableApiKey = async (id: string) => {
const router = useRouter();
const onDisableApiKey = async (id: string, key: string) => {
try {
const apiKey = Cookies.get().apiKey as string;
await api.apiKeys.disableApiKey(id);
if (apiKey.endsWith(key.substring(9))) {
Cookies.remove('apiKey');
Cookies.remove('masterKey');
router.replace('/');
}
setApiKeys(old => old.filter(key => key.keyID !== id));
} catch (err) {
console.error('Error Deleting api key');
@ -37,10 +47,13 @@ function ApiKeyList({ data }: ApiKeyListProps) {
</tr>
</thead>
<tbody className="divide-y p-2">
{apiKeys.map(({ key, keyID }) => (
{apiKeys.map(({ key, keyID, last_used }) => (
<tr className="bg-gray-900 rounded" key={keyID}>
<td className="whitespace-nowrap pl-4 text-sm font-medium text-white">
<p className="text-xl">{key}</p>
<p className="text-xs text-gray-400">
Last Used: {parseDate(last_used)}
</p>
</td>
<td className="relative whitespace-nowrap py-4 px-4 text-right text-sm font-medium icons flex center items-center gap-3">
<Button
@ -49,7 +62,7 @@ function ApiKeyList({ data }: ApiKeyListProps) {
aria-label="Disable Api Key"
title="Disable Api Key"
className="rounded-full p-2 bg-red-100 text-red-600"
onClick={() => onDisableApiKey(keyID)}
onClick={() => onDisableApiKey(keyID, key)}
>
Disable
</Button>

View File

@ -1,6 +1,6 @@
import Button from '@/components/ui/Button';
import { Edit, Trash2 } from 'lucide-react';
import React from 'react';
import { ArrowUpRight, Copy, Trash2 } from 'lucide-react';
import Link from 'next/link';
interface NotesLitsProps {
data: INote[];
@ -8,6 +8,11 @@ interface NotesLitsProps {
}
function NotesList({ data, onDeleteNote }: NotesLitsProps) {
function copyNoteLink(id: string) {
navigator.clipboard.writeText(
`${window.location.origin.toString()}/notes/${id}`
);
}
return (
<div className="flex flex-col w-full gap-1">
{data.map(note => {
@ -30,12 +35,23 @@ function NotesList({ data, onDeleteNote }: NotesLitsProps) {
<Button
variant="transparent"
size={'icon'}
aria-label="Edit Note"
title="Edit Note"
aria-label="Copy Note"
title="Copy Note"
className="rounded-full hover:bg-black"
onClick={() => copyNoteLink(note.gist_url_key)}
>
<Edit className="h-5 w-5 " />
<Copy className="h-5 w-5 " />
</Button>
<Link
href={`/notes/${note.gist_url_key}`}
target="_blank"
referrerPolicy="no-referrer"
aria-label="Go to Note"
title="Go to Note"
className="rounded-full hover:bg-black p-3 h-min w-min"
>
<ArrowUpRight className="h-5 w-5 " />
</Link>
</div>
</div>
);

View File

@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import Button from '@/components/ui/Button';
import { Trash, Edit2, ArrowUpRight, X } from 'lucide-react';
import URLControls from './URLControls';
@ -9,7 +9,6 @@ import Input from '@/components/ui/Input';
import api from '@/api';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import Cookies from 'js-cookie';
interface ShortenUrlListProps {
data: IUrl[];
@ -27,7 +26,7 @@ function ShortenUrlList({ data }: ShortenUrlListProps) {
state: false,
});
const [input, setInput] = useState('');
const [instanceUrl, setInstanceURL] = useState('');
const onAddURL = async (url: string) => {
try {
await api.url.uploadUrl(url);
@ -59,9 +58,6 @@ function ShortenUrlList({ data }: ShortenUrlListProps) {
}
};
useEffect(() => {
setInstanceURL(Cookies.get('instanceUrl') ?? '');
}, []);
return (
<>
<URLControls onAddURL={onAddURL} />
@ -103,10 +99,10 @@ function ShortenUrlList({ data }: ShortenUrlListProps) {
</td>
<td className="whitespace-nowrap pl-4 text-sm font-medium text-white">
<div className="flex items-center gap-3">
<p className="w-80 truncate">{`${instanceUrl}/${short_key}`}</p>
<p className="w-80 truncate">{`${process.env.NEXT_PUBLIC_INSTANCE_URL}/${short_key}`}</p>
<a
referrerPolicy="no-referrer"
href={`${instanceUrl}/${short_key}`}
href={`${process.env.NEXT_PUBLIC_INSTANCE_URL}/${short_key}`}
target="_blank"
className="p-2 bg-white bg-opacity-10 rounded cursor-pointer"
>

View File

@ -1,7 +1,7 @@
import Button from '@/components/ui/Button';
import { ArrowUpRight, Trash } from 'lucide-react';
import React from 'react';
import { cn } from '@/lib/utils';
import { cn, parseDate } from '@/lib/utils';
import { UploadsListComponentProps } from '@/types/list';
export default function LinearList({
@ -10,9 +10,6 @@ export default function LinearList({
onDelete,
}: UploadsListComponentProps) {
// parse a date from gmt format to iso format
const parseDate = (date: string) => {
return new Date(date).toISOString().split('T')[0];
};
return (
<div className="flex flex-col w-full gap-2 ">

View File

@ -18,9 +18,8 @@ const LoginForm = () => {
resolver: zodResolver(LoginSchema),
});
const onSubmit = async ({ masterkey, instanceUrl }: LoginType) => {
const onSubmit = async ({ masterkey }: LoginType) => {
Cookies.set('masterKey', masterkey);
Cookies.set('instanceUrl', instanceUrl);
try {
const res = await api.apiKeys.createKey();
if (res) {
@ -28,14 +27,12 @@ const LoginForm = () => {
router.push('/dashboard');
} else {
Cookies.remove('masterKey');
Cookies.remove('instanceUrl');
Cookies.remove('apiKey');
}
} catch (err) {
console.error(err);
toast.error('error');
Cookies.remove('masterKey');
Cookies.remove('instanceUrl');
Cookies.remove('apiKey');
}
};
@ -53,14 +50,6 @@ const LoginForm = () => {
type="text"
id="masterKey"
/>
<Input
{...register('instanceUrl')}
label="Instance Url"
withLabel={true}
placeholder="Instance Url"
type="text"
id="instanceUrl"
/>
<Button type="submit" className="mt-8">
Sign in
</Button>

View File

@ -0,0 +1,25 @@
'use client';
import React, { useEffect, useRef } from 'react';
import showdown from 'showdown';
import 'github-markdown-css';
const converter = new showdown.Converter();
interface MarkdownProps {
markdown: string;
}
function Markdown({ markdown }: MarkdownProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current?.innerHTML) ref.current.innerHTML = converter.makeHtml(markdown);
}, [markdown]);
return (
<div className="markdown-body w-full p-10" ref={ref}>
Markdown
</div>
);
}
export default Markdown;

View File

@ -0,0 +1,13 @@
'use client';
import { Toaster } from 'react-hot-toast';
function Providers({ children }: { children: React.ReactNode }) {
return (
<>
<Toaster position={'top-right'} reverseOrder={false} />
{children}
</>
);
}
export default Providers;

View File

@ -5,10 +5,11 @@ import Button from './ui/Button';
interface TagInput {
tags: string[];
onAddTags: (value: string) => void;
onChange: (value: string[]) => void;
placeholder?: string;
}
function TagInput({ tags, placeholder, onAddTags }: TagInput) {
function TagInput({ tags, placeholder, onAddTags, onChange }: TagInput) {
const inputRef = useRef<HTMLInputElement>(null);
const onSubmit: FormEventHandler<HTMLFormElement> = evt => {
evt.preventDefault();
@ -17,10 +18,17 @@ function TagInput({ tags, placeholder, onAddTags }: TagInput) {
inputRef.current.value = '';
}
};
const onDelete = (i: number) => {
onChange(tags.filter((_tag, index) => i !== index));
};
return (
<div className="w-full flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Button key={index} className="rounded w-min m-0 items-center">
<Button
onClick={() => onDelete(index)}
key={index}
className="rounded w-min m-0 items-center"
>
{tag}
</Button>
))}

View File

@ -32,12 +32,12 @@ export const sidebarGroups: SidebarGroup[] = [
href: '/dashboard/utilities/instance-info',
},
{
name: 'Uploads',
name: 'Settings',
href: '/dashboard/utilities/settings',
},
{
name: 'Notes',
href: '/dashboard/utilites/download-config',
name: 'Download Config',
href: '/dashboard/utilities/download-config',
},
],
},

View File

@ -4,3 +4,7 @@ import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const parseDate = (date: string) => {
return new Date(date).toISOString().split('T')[0];
};

View File

@ -2,7 +2,6 @@ import { z } from 'zod';
export const LoginSchema = z.object({
masterkey: z.string().max(256),
instanceUrl: z.string().url(),
});
export type LoginType = z.infer<typeof LoginSchema>;

View File

@ -23,15 +23,19 @@
"clsx": "^1.2.1",
"eslint": "8.41.0",
"eslint-config-next": "13.4.5",
"github-markdown-css": "^5.2.0",
"install": "^0.13.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.221.0",
"next": "^13.4.7",
"next-auth": "^4.22.1",
"npm": "^9.7.2",
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.44.3",
"react-hot-toast": "^2.4.1",
"showdown": "^2.1.0",
"tailwind-merge": "^1.12.0",
"tailwindcss": "3.3.2",
"typescript": "5.0.4",
@ -46,6 +50,7 @@
"@storybook/react": "^7.0.17",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/js-cookie": "^3.0.3",
"@types/showdown": "^2.0.1",
"eslint-plugin-storybook": "^0.6.12",
"storybook": "^7.0.17"
}

View File

@ -1,4 +1,5 @@
interface IApiKey {
key: string;
keyID: string;
last_used: string;
}

View File

@ -3,4 +3,13 @@ interface ISettings {
language: string;
imageExtensions: string[];
fileExtensions: string[];
}
}
interface ISysSettings {
platform: string;
arch: string;
nodeVersion: string;
uptime: string;
kernelVersion: string;
hostname: string;
}

1162
yarn.lock

File diff suppressed because it is too large Load Diff