Layout
{% extends 'base.html' %} {% load static %} {% block content %} <div id="layout" v-cloak> <div class="flex items-center justify-center h-screen" v-if="isPageLoading"> <div class="loading-spinner"> <div></div> <div></div> <div></div> <div></div> <div></div> <div></div> </div> </div> <div v-else> <!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. --> <div v-if="isSideNavOpen" class="relative z-50 lg:hidden" role="dialog" aria-modal="true"> <!-- Off-canvas menu backdrop, show/hide based on off-canvas menu state. Entering: "transition-opacity ease-linear duration-300" From: "opacity-0" To: "opacity-100" Leaving: "transition-opacity ease-linear duration-300" From: "opacity-100" To: "opacity-0" --> <div class="fixed inset-0 bg-gray-900/80"></div> <div class="fixed inset-0 flex"> <!-- Off-canvas menu, show/hide based on off-canvas menu state. Entering: "transition ease-in-out duration-300 transform" From: "-translate-x-full" To: "translate-x-0" Leaving: "transition ease-in-out duration-300 transform" From: "translate-x-0" To: "-translate-x-full" --> <div class="relative mr-16 flex w-full max-w-xs flex-1"> <!-- Close button, show/hide based on off-canvas menu state. Entering: "ease-in-out duration-300" From: "opacity-0" To: "opacity-100" Leaving: "ease-in-out duration-300" From: "opacity-100" To: "opacity-0" --> <div class="absolute left-full top-0 flex w-16 justify-center pt-5"> <button @click="openSideNavOpen" type="button" class="-m-2.5 p-2.5"> <span class="sr-only">Close sidebar</span> <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/> </svg> </button> </div> <!-- Sidebar component, swap this element with another sidebar if you like --> <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4"> <div class="flex h-16 shrink-0 items-center mt-5"> <img class="mx-auto h-10 w-auto" src="{% static 'logo.png' %}"> </div> <nav class="flex flex-1 flex-col"> <ul role="list" class="flex flex-1 flex-col gap-y-7"> <li> <ul role="list" class="-mx-2 space-y-1"> <li> <!-- Current: "bg-gray-50 text-indigo-600", Default: "text-gray-700 hover:text-indigo-600 hover:bg-gray-50" --> <a href="/" class="bg-gray-50 text-indigo-600 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/> </svg> Homepage </a> </li> <li> <a href="/chats/room" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/> </svg> Chat Room </a> </li> <li> <a href="/tasks" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"/> </svg> Tasks </a> </li> </ul> </li> </ul> </nav> </div> </div> </div> </div> <!-- Static sidebar for desktop --> <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col"> <!-- Sidebar component, swap this element with another sidebar if you like --> <div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4"> <div class="flex h-16 shrink-0 items-center mt-20"> <img class="mx-auto h-24 w-auto" src="{% static 'logo.png' %}"> </div> <nav class="mt-10 flex flex-1 flex-col"> <ul role="list" class="flex flex-1 flex-col gap-y-7"> <li> <ul role="list" class="-mx-2 space-y-1"> <li> <!-- Current: "bg-gray-50 text-indigo-600", Default: "text-gray-700 hover:text-indigo-600 hover:bg-gray-50" --> <a href="/" class="bg-gray-50 text-indigo-600 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/> </svg> Homepage </a> </li> <li> <a href="/chats/room" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/> </svg> Chat Room </a> </li> <li> <a href="/tasks" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> <svg class="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"/> </svg> Tasks </a> </li> </ul> </li> </ul> </nav> </div> </div> <div class="lg:pl-72"> <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"> <button @click="openSideNavOpen" type="button" class="-m-2.5 p-2.5 text-gray-700 lg:hidden"> <span class="sr-only">Open sidebar</span> <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/> </svg> </button> <!-- Separator --> <div class="h-6 w-px bg-gray-200 lg:hidden" aria-hidden="true"></div> <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6"> <div class="relative flex flex-1" > </div> <div class="flex items-center gap-x-4 lg:gap-x-6"> <!-- Separator --> <div class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-200" aria-hidden="true"></div> <!-- Profile dropdown --> <div class="relative"> <button @click="openSidebar" type="button" class="-m-1.5 flex items-center p-1.5" id="user-menu-button" aria-expanded="false" aria-haspopup="true"> <span class="sr-only">Open user menu</span> <img class="h-8 w-8 rounded-full bg-gray-50" src="{{ user.profile_picture.url }}" alt=""> <span class="hidden lg:flex lg:items-center"> <span class="ml-4 text-sm font-semibold leading-6 text-white" aria-hidden="true">{{ user.first_name }} {{ user.last_name }}</span> <svg class="ml-2 h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/> </svg> </span> </button> <div v-if="isOpen" class="absolute right-0 z-10 mt-2.5 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1"> <!-- Active: "bg-gray-50", Not Active: "" --> <a href="/profile" class="block px-3 py-1 text-sm leading-6 text-gray-900" role="menuitem" tabindex="-1" id="user-menu-item-1">Profile</a> <a href="/accounts/logout/" class="block px-3 py-1 text-sm leading-6 text-gray-900" role="menuitem" tabindex="-1" id="user-menu-item-1">Sign out</a> </div> </div> </div> </div> </div> <main class="py-10"> <div class="mx-auto max-w-9xl px-4 sm:px-10 lg:px-8"> {% block layout %} {% endblock %} </div> </main> </div> </div> </div> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script type="module"> const {createApp} = Vue; const delimiters = ['[[', ']]'] const chat_room = "{{ pk }}" const user_id = "{{ user.id }}" createApp({ delimiters, data() { return { isOpen: false, isSideNavOpen: false, todoList: [], inProgressList: [], doneList: [], showTasks: true, showAIModal: false, isLoading: false, isChatFormModalOpen: false, isPageLoading: false, isDeleteModalOpen: false, prompt: "", title: "", assignee: "", deadline: "", description: "", chat_name: "", message: "", chat_room_id: "", chat_members: [], chat_rooms: [], chat_messages: [], currentDate: new Date(), user_id } }, methods: { openSidebar() { this.isOpen = !this.isOpen }, openSideNavOpen() { this.isSideNavOpen = !this.isSideNavOpen }, openTasksForm() { this.showTasks = !this.showTasks }, openAIModal() { this.showAIModal = !this.showAIModal }, openChatFormModalOpen() { this.isChatFormModalOpen = !this.isChatFormModalOpen }, deleteChatRoom(id) { this.isDeleteModalOpen = !this.isDeleteModalOpen this.chat_room_id = id }, async getTasksToDo() { try { const resp = await apiGetTasksToDo() this.todoList = resp } catch (err) { console.log(err) } }, async getTasksInProgress() { try { const resp = await apiGetTasksInProgress() this.inProgressList = resp } catch (err) { console.log(err) } }, async getTasksDone() { try { const resp = await apiGetTasksDone() this.doneList = resp } catch (err) { console.log(err) } }, async moveTasks(id, status) { const payload = { id: id, status: status } try { const resp = await apiMoveTasks(payload) await this.getTasksToDo() await this.getTasksInProgress() await this.getTasksDone() } catch (err) { console.log(err) } }, async aIGenerate() { const payload = { prompt: this.prompt } try { this.isLoading = !this.isLoading const resp = await apiAIGenerate(payload) this.isLoading = !this.isLoading this.description = resp if (resp) { this.isLoading = !this.isLoading this.showAIModal = !this.showAIModal } } catch (err) { console.log(err) } }, async createTasks() { const payload = { title: this.title, assignee: this.assignee, deadline: this.deadline, description: this.description } try { const resp = await apiAddTasks(payload) if (resp) { this.showTasks = !this.showTasks } } catch (err) { console.log(err) } }, async createNewChatRoom() { const payload = { name: this.chat_name, members: this.chat_members } try { const resp = await apiCreateNewChatRoom(payload) if (resp) { this.isChatFormModalOpen = !this.isChatFormModalOpen await this.getChatRoomsLists() } } catch (err) { console.log(err) } }, async getChatRoomsLists() { try { const resp = await apiChatRoomList() this.chat_rooms = resp } catch (err) { console.log(err) } }, formatRelativeTime(dateString) { const now = new Date(); const date = new Date(dateString); const diffInMilliseconds = now - date; const diffInSeconds = Math.floor(diffInMilliseconds / 1000); if (diffInSeconds < 1) { return 'just now'; } else if (diffInSeconds < 60) { return `last ${diffInSeconds} seconds ago`; } else { const diffInMinutes = Math.floor(diffInSeconds / 60); const diffInHours = Math.floor(diffInMinutes / 60); if (diffInMinutes < 1) { return 'just now'; } else if (diffInHours < 1) { return `last ${diffInMinutes} minutes ago`; } else if (diffInHours < 24) { return `last ${diffInHours} hours ago`; } else { const options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', }; return new Intl.DateTimeFormat('en-US', options).format(date); } } }, async getChatMessages() { window.setInterval(async () => { try { const resp = await apiGetChatMessages() this.chat_messages = resp } catch (err) { console.log(err) } }, 1000) }, async sendChatMessage() { const payload = { chat_room: chat_room, message: this.message } try { const resp = await apiSendChatMessage(payload) if (resp) { await this.getChatMessages() this.message = "" } } catch (err) { console.log(err) } }, }, async mounted() { await this.getTasksToDo() await this.getTasksInProgress() await this.getTasksDone() await this.getChatRoomsLists() if (chat_room) { await this.getChatMessages() } this.isPageLoading = false; }, created() { this.isPageLoading = true }, computed: { formattedDate() { const options = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', }; return this.currentDate.toLocaleDateString('en-US', options); }, }, }).mount('#layout') async function apiGetTasksToDo() { try { const resp = await fetch(`/api/tasks/todo`, { method: 'GET' }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiGetTasksInProgress() { try { const resp = await fetch(`/api/tasks/in-progress`, { method: 'GET' }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiGetTasksDone() { try { const resp = await fetch(`/api/tasks/done`, { method: 'GET' }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiMoveTasks(payload) { try { const resp = await fetch(`/api/tasks/move`, { method: 'POST', body: JSON.stringify(payload), headers: {"Content-Type": "application/json"}, }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiAIGenerate(payload) { try { const resp = await fetch(`/api/ai/generate`, { method: 'POST', body: JSON.stringify(payload), headers: {"Content-Type": "application/json"}, }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiAddTasks(payload) { try { const resp = await fetch(`/api/tasks/add`, { method: 'POST', body: JSON.stringify(payload), headers: {"Content-Type": "application/json"}, }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiCreateNewChatRoom(payload) { try { const resp = await fetch(`/api/chat/room/add`, { method: 'POST', body: JSON.stringify(payload), headers: {"Content-Type": "application/json"}, }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiChatRoomList() { try { const resp = await fetch(`/api/chat/rooms`, { method: 'GET' }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiGetChatMessages() { try { const resp = await fetch(`/api/chat/messages/${chat_room}`, { method: 'GET' }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } async function apiSendChatMessage(payload) { try { const resp = await fetch(`/api/chat/message/send`, { method: 'POST', body: JSON.stringify(payload), headers: {"Content-Type": "application/json"}, }).then(resp => resp.json()) return resp } catch (error) { console.log(error) return [] } } </script> <style> [v-cloak] { display: none; } .spinner { width: 10px; height: 10px; border-radius: 50%; border: 9px solid #474bff; animation: spinner-bulqg1 0.8s infinite linear alternate, spinner-oaa3wk 1.6s infinite linear; } @keyframes spinner-bulqg1 { 0% { clip-path: polygon(50% 50%, 0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%); } 12.5% { clip-path: polygon(50% 50%, 0 0, 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0%); } 25% { clip-path: polygon(50% 50%, 0 0, 50% 0%, 100% 0%, 100% 100%, 100% 100%, 100% 100%); } 50% { clip-path: polygon(50% 50%, 0 0, 50% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100%); } 62.5% { clip-path: polygon(50% 50%, 100% 0, 100% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100%); } 75% { clip-path: polygon(50% 50%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 50% 100%, 0% 100%); } 100% { clip-path: polygon(50% 50%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 0% 100%); } } @keyframes spinner-oaa3wk { 0% { transform: scaleY(1) rotate(0deg); } 49.99% { transform: scaleY(1) rotate(135deg); } 50% { transform: scaleY(-1) rotate(0deg); } 100% { transform: scaleY(-1) rotate(-135deg); } } .loading-spinner { width: 44.8px; height: 44.8px; animation: loading-spinner-y0fdc1 2s infinite ease; transform-style: preserve-3d; } .loading-spinner > div { background-color: rgba(71, 75, 255, 0.2); height: 100%; position: absolute; width: 100%; border: 2.2px solid #474bff; } .loading-spinner div:nth-of-type(1) { transform: translateZ(-22.4px) rotateY(180deg); } .loading-spinner div:nth-of-type(2) { transform: rotateY(-270deg) translateX(50%); transform-origin: top right; } .loading-spinner div:nth-of-type(3) { transform: rotateY(270deg) translateX(-50%); transform-origin: center left; } .loading-spinner div:nth-of-type(4) { transform: rotateX(90deg) translateY(-50%); transform-origin: top center; } .loading-spinner div:nth-of-type(5) { transform: rotateX(-90deg) translateY(50%); transform-origin: bottom center; } .loading-spinner div:nth-of-type(6) { transform: translateZ(22.4px); } @keyframes loading-spinner-y0fdc1 { 0% { transform: rotate(45deg) rotateX(-25deg) rotateY(25deg); } 50% { transform: rotate(45deg) rotateX(-385deg) rotateY(25deg); } 100% { transform: rotate(45deg) rotateX(-385deg) rotateY(385deg); } } </style> {% endblock %}
Leave a Comment