This commit is contained in:
kusowl 2026-05-03 22:26:37 +05:30
parent 7a2c1f03fa
commit 9853c6128c
13 changed files with 337 additions and 55 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
backend/package-lock.json

15
.zed/debug.json Normal file
View File

@ -0,0 +1,15 @@
[
{
"label": "PHP: Listen to Xdebug",
"adapter": "Xdebug",
"request": "launch",
"port": 9003,
},
{
"label": "PHP: Debug this test",
"adapter": "Xdebug",
"request": "launch",
"program": "vendor/bin/phpunit",
"args": ["--filter", "$ZED_SYMBOL"],
},
]

View File

@ -4,7 +4,7 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
FRONTEND_URL="http://localhost:4200,http://127.0.0.1:4200"
SANCTUM_STATEFUL_DOMAINS="${FRONTEND_URL}"
SANCTUM_STATEFUL_DOMAINS=localhost:4200,127.0.0.1:4200
APP_LOCALE=en
APP_FALLBACK_LOCALE=en

View File

@ -1,18 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
define("LARAVEL_START", microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
require __DIR__ . "/vendor/autoload.php";
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$app = require_once __DIR__ . "/bootstrap/app.php";
$status = $app->handleCommand(new ArgvInput);
$status = $app->handleCommand(new ArgvInput());
exit($status);

View File

@ -18,6 +18,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.0",
"laravel/pail": "^1.2.5",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",

186
backend/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ce000f2979a5d252cefadab87899a5fc",
"content-hash": "3edf1e72a6c813ae77e28811f4fa4c1e",
"packages": [
{
"name": "aws/aws-crt-php",
@ -6982,6 +6982,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.7",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.7"
},
"time": "2026-01-28T22:20:33+00:00"
},
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@ -7042,6 +7083,96 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.9.6",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.7.0",
"illuminate/console": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/container": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/database": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/http": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
"php": "^8.2",
"phpstan/phpstan": "^2.1.44"
},
"require-dev": {
"doctrine/coding-standard": "^13",
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.9.6"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2026-04-16T10:02:43+00:00"
},
{
"name": "laravel/pail",
"version": "v1.2.6",
@ -8165,6 +8296,59 @@
},
"time": "2026-01-25T14:56:51+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.54",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-04-29T13:31:09+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.6",

17
backend/phpstan.neon Normal file
View File

@ -0,0 +1,17 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/nesbot/carbon/extension.neon
parameters:
paths:
- app/
# Level 10 is the highest level
level: 8
# ignoreErrors:
# - '#PHPDoc tag @var#'
#
# excludePaths:
# - ./*/*/FileToBeExcluded.php

View File

@ -5,9 +5,9 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/me', function (Request $request) {
Route::get("/me", function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
})->middleware("auth:sanctum");
Route::apiResource('chats', ChatController::class);
Route::apiResource('chats.messages', ChatMessageController::class);
Route::apiResource("chats", ChatController::class);
Route::apiResource("chats.messages", ChatMessageController::class);

View File

@ -0,0 +1,8 @@
import { InitialsPipe } from './initials-pipe';
describe('InitialsPipe', () => {
it('create an instance', () => {
const pipe = new InitialsPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'initials',
})
export class InitialsPipe implements PipeTransform {
transform(value: string, separator: string = ' ', wordCount: number = 1): string {
if (!value) return '';
const parts = value.split(separator);
let result = '';
for (let i = 0; i < wordCount; i++) {
result += parts[i][0].toUpperCase();
}
return result;
}
}

View File

@ -25,7 +25,7 @@
<div class="flex items-center justify-between px-4 pt-5 pb-4 border-b border-[#1e2f4d]">
<div class="flex items-center gap-2.5">
<div
class="w-8 h-8 rounded-full bg-linear-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]"
class="w-8 h-8 rounded-full bg-linear-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]"
>
AI
</div>
@ -63,7 +63,7 @@
<div class="px-3 pt-3 pb-2">
<button
(click)="newChat()"
class="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-[#2d5be3] hover:bg-[#3468f0] text-white text-sm font-medium transition-all duration-200 shadow-md hover:shadow-[#2d5be3]/30 hover:shadow-lg active:scale-[0.98]"
class="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-[#2d5be3] hover:bg-[#3468f0] text-white text-sm font-medium transition-all duration-200 shadow-md hover:shadow-[#2d5be3]/30 hover:shadow-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -108,13 +108,15 @@
<!-- Chat Items -->
<div class="space-y-0.5">
@for (chat of chatStore.chats(); track chat.id) {
<button (click)="selectChat(chat)" [class]="chatItemClasses(chat)">
<button (click)="selectChat(chat.id)" [class]="chatItemClasses(chat)">
<!-- Active indicator -->
@if(activeChatId() && activeChatId() === chat.id){
<div
class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-[#2d5be3] rounded-r-full"
></div>
}
<div class="flex items-start gap-2.5 w-full min-w-0">
<div class="flex items-center gap-2.5 w-full min-w-0">
<!-- Icon -->
<div [class]="chatIconClasses(chat)">
<svg
@ -135,7 +137,7 @@
<!-- Text -->
<div class="flex-1 min-w-0 text-left">
<p class="text-xs font-medium truncate text-white">{{ chat.attributes.title }}</p>
<p class="text-xs font-medium truncate">{{ chat.attributes.title }}</p>
</div>
<!-- Time -->
@ -173,22 +175,32 @@
<!-- Footer -->
<div class="border-t border-[#1e2f4d] px-3 py-3">
<div class="flex items-center gap-2.5 transition-all duration-150 group">
<div class="flex items-center gap-2.5 transition-all duration-150">
@if(authStore.isLoading()) {
<p>Loading...</p>
} @else if(authStore.user() !== null){
<div
class="flex items-center hover:bg-black/9 px-4 py-2 rounded-xl w-full gap-2.5 transition-all duration-150"
>
<div
class="w-7 h-7 rounded-lg bg-linear-to-br from-[#2d5be3] to-[#1a3a9e] flex items-center justify-center text-white text-xs font-bold shrink-0"
>
K
{{authStore!.user()!.name | initials}}
</div>
<div class="flex-1 min-w-0">
<p class="text-xs font-medium text-[#c8d8f0] truncate">Kushal Saha</p>
<p class="text-xs font-medium text-[#c8d8f0] truncate">{{authStore!.user()!.name}}</p>
<p class="text-[10px] text-[#3a5272] truncate">Free Plan</p>
</div>
<div class="relative flex items-center">
<button
id="cog"
(click)="toggleCogMenu()"
class="bg-transparent border-none p-0 cursor-pointer flex items-center justify-center outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-3.5 h-3.5 text-[#3a5272] group-hover:text-[#5a80a8] transition-colors"
class="w-3.5 h-3.5 text-[#3a5272] hover:text-[#5a80a8] transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -206,6 +218,24 @@
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
@if(isCogMenuOpen()){
<div
id="cog-menu"
class="bg-black/9 backdrop-blur-sm text-xs text-gray-400 min-w-30 px-2 py-2 rounded-xl absolute right-0 bottom-4"
>
<ul class="">
<li>
<button class="w-full py-2 rounded-xl hover:bg-white/4 hover:text-red-400">
Logout
</button>
</li>
</ul>
</div>
}
</div>
</div>
} @else{
<a
routerLink="/user/login"

View File

@ -3,11 +3,14 @@ import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ChatStore } from '../../../chat/chat.store';
import { AuthStore } from '../../../auth/auth.store';
import { InitialsPipe } from './initials-pipe';
import { JsonApiResource } from '../../types/api';
import { Chat } from '../../../chat/chat.types';
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, InitialsPipe],
templateUrl: 'sidebar.html',
styleUrl: 'sidebar.css',
})
@ -16,7 +19,9 @@ export class Sidebar implements OnInit {
protected authStore = inject(AuthStore);
protected isOpen = signal(true);
protected isCogMenuOpen = signal(false);
protected searchQuery = signal('');
protected activeChatId = signal<string | null>(null);
ngOnInit() {
this.chatStore.fetchChats();
@ -29,10 +34,11 @@ export class Sidebar implements OnInit {
return this.isOpen() ? base + ' w-64' : base + ' w-0 border-none opacity-0';
});
protected chatItemClasses(chat: any): string {
protected chatItemClasses(chat: JsonApiResource<Chat>): string {
const base =
'relative w-full flex items-center px-2 py-2 rounded-lg transition-all duration-150 cursor-pointer group';
return chat.isActive
'relative w-full flex items-center justify-center px-2 py-2 rounded-lg transition-all duration-150 cursor-pointer group';
return this.activeChatId() && chat.id === this.activeChatId()
? base + ' bg-[#13213d] text-white'
: base + ' hover:bg-[#111a2e] text-[#6a8faf]';
}
@ -48,18 +54,16 @@ export class Sidebar implements OnInit {
this.isOpen.update((v) => !v);
}
protected toggleCogMenu(): void {
this.isCogMenuOpen.update((v) => !v);
}
protected newChat(): void {
console.log('New chat triggered');
}
protected selectChat(chat: any): void {
// this.activeChatId.set(chat.id);
// this.sections.update((sections) =>
// sections.map((section) => ({
// ...section,
// chats: section.chats.map((c) => ({ ...c, isActive: c.id === chat.id })),
// })),
// );
protected selectChat(chatId: string): void {
this.activeChatId.set(chatId);
}
protected onSearch(event: Event): void {

View File

@ -14,3 +14,8 @@ body {
font-family: 'Outfit', sans-serif;
background: radial-gradient(circle at top right, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
button:active {
transform: scale(0.98);
transition: scale;
transition-duration: 200ms;
}