Compare commits
12 Commits
3ed7519afd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 18d74cddff | |||
| 537ba783d1 | |||
| 093563a458 | |||
| e63c0f5998 | |||
| 2492720470 | |||
| c4b509c1eb | |||
| 577b63b7e7 | |||
| 84c24b2e92 | |||
| b628bbb83f | |||
| 0e6b54b9e4 | |||
| 097f911889 | |||
| 621e5222fa |
4
bizmatch-server/src/config/pictures.config.ts
Normal file
4
bizmatch-server/src/config/pictures.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Single source of truth for the pictures directory.
|
||||||
|
// MUST match the container-side path of the volume mount in docker-compose.yml:
|
||||||
|
// ./bizmatch-server/pictures:/app/dist/pictures
|
||||||
|
export const PICTURES_DIR = process.env.PICTURES_DIR || '/app/dist/pictures';
|
||||||
@@ -3,14 +3,15 @@ import fs from 'fs-extra';
|
|||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
|
import { PICTURES_DIR } from '../config/pictures.config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||||
fs.ensureDirSync(`./pictures`);
|
fs.ensureDirSync(`${PICTURES_DIR}`);
|
||||||
fs.ensureDirSync(`./pictures/profile`);
|
fs.ensureDirSync(`${PICTURES_DIR}/profile`);
|
||||||
fs.ensureDirSync(`./pictures/logo`);
|
fs.ensureDirSync(`${PICTURES_DIR}/logo`);
|
||||||
fs.ensureDirSync(`./pictures/property`);
|
fs.ensureDirSync(`${PICTURES_DIR}/property`);
|
||||||
}
|
}
|
||||||
// ############
|
// ############
|
||||||
// Profile
|
// Profile
|
||||||
@@ -22,10 +23,10 @@ export class FileService {
|
|||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
await sharp(output).toFile(`${PICTURES_DIR}/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
hasProfile(adjustedEmail: string) {
|
hasProfile(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
|
return fs.existsSync(`${PICTURES_DIR}/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
// ############
|
// ############
|
||||||
// Logo
|
// Logo
|
||||||
@@ -37,18 +38,18 @@ export class FileService {
|
|||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`${PICTURES_DIR}/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
// await fs.outputFile(`${PICTURES_DIR}/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
hasCompanyLogo(adjustedEmail: string) {
|
hasCompanyLogo(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
return fs.existsSync(`${PICTURES_DIR}/logo/${adjustedEmail}.avif`) ? true : false;
|
||||||
}
|
}
|
||||||
// ############
|
// ############
|
||||||
// Property
|
// Property
|
||||||
// ############
|
// ############
|
||||||
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
const directory = `${PICTURES_DIR}/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
@@ -60,7 +61,7 @@ export class FileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
const directory = `${PICTURES_DIR}/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
return files.length > 0;
|
return files.length > 0;
|
||||||
@@ -69,7 +70,7 @@ export class FileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
const directory = `${PICTURES_DIR}/property/${imagePath}/${serial}`;
|
||||||
fs.ensureDirSync(`${directory}`);
|
fs.ensureDirSync(`${directory}`);
|
||||||
const imageName = await this.getNextImageName(directory);
|
const imageName = await this.getNextImageName(directory);
|
||||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||||
@@ -111,7 +112,7 @@ export class FileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteDirectoryIfExists(imagePath) {
|
deleteDirectoryIfExists(imagePath) {
|
||||||
const dirPath = `pictures/property/${imagePath}`;
|
const dirPath = `${PICTURES_DIR}/property/${imagePath}`;
|
||||||
try {
|
try {
|
||||||
const exists = fs.pathExistsSync();
|
const exists = fs.pathExistsSync();
|
||||||
if (exists) {
|
if (exists) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Logger } from 'winston';
|
|||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { CommercialPropertyService } from '../listings/commercial-property.service';
|
import { CommercialPropertyService } from '../listings/commercial-property.service';
|
||||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||||
|
import { PICTURES_DIR } from '../config/pictures.config';
|
||||||
|
|
||||||
@Controller('image')
|
@Controller('image')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
@@ -28,7 +29,7 @@ export class ImageController {
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
this.fileService.deleteImage(`${PICTURES_DIR}/property/${imagePath}/${serial}/${imagename}`);
|
||||||
await this.listingService.deleteImage(imagePath, serial, imagename);
|
await this.listingService.deleteImage(imagePath, serial, imagename);
|
||||||
}
|
}
|
||||||
// ############
|
// ############
|
||||||
@@ -43,7 +44,7 @@ export class ImageController {
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Delete('profile/:email/')
|
@Delete('profile/:email/')
|
||||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
this.fileService.deleteImage(`${PICTURES_DIR}/profile/${email}.avif`);
|
||||||
}
|
}
|
||||||
// ############
|
// ############
|
||||||
// Logo
|
// Logo
|
||||||
@@ -57,6 +58,6 @@ export class ImageController {
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Delete('logo/:email/')
|
@Delete('logo/:email/')
|
||||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
this.fileService.deleteImage(`${PICTURES_DIR}/logo/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { GeoService } from '../geo/geo.service';
|
|||||||
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
|
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
|
||||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
||||||
import { getDistanceQuery, splitName } from '../utils';
|
import { getDistanceQuery, splitName } from '../utils';
|
||||||
import { generateSlug, extractShortIdFromSlug, isSlug } from '../utils/slug.utils';
|
import { generateSlug, extractShortIdFromSlug, isSlug, isUUID } from '../utils/slug.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BusinessListingService {
|
export class BusinessListingService {
|
||||||
@@ -271,6 +271,9 @@ export class BusinessListingService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`Detected as UUID: ${slugOrId}`);
|
this.logger.debug(`Detected as UUID: ${slugOrId}`);
|
||||||
|
if (!isUUID(slugOrId)) {
|
||||||
|
throw new BadRequestException(`Invalid identifier format: ${slugOrId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.findBusinessesById(id, user);
|
return this.findBusinessesById(id, user);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { GeoService } from '../geo/geo.service';
|
|||||||
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model';
|
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model';
|
||||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
||||||
import { getDistanceQuery, splitName } from '../utils';
|
import { getDistanceQuery, splitName } from '../utils';
|
||||||
import { generateSlug, extractShortIdFromSlug, isSlug } from '../utils/slug.utils';
|
import { generateSlug, extractShortIdFromSlug, isSlug, isUUID } from '../utils/slug.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommercialPropertyService {
|
export class CommercialPropertyService {
|
||||||
@@ -166,6 +166,9 @@ export class CommercialPropertyService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`Detected as UUID: ${slugOrId}`);
|
this.logger.debug(`Detected as UUID: ${slugOrId}`);
|
||||||
|
if (!isUUID(slugOrId)) {
|
||||||
|
throw new BadRequestException(`Invalid identifier format: ${slugOrId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.findCommercialPropertiesById(id, user);
|
return this.findCommercialPropertiesById(id, user);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Controller, Get, Inject, Param, Request, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, Param, Request, UseGuards, BadRequestException } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { BusinessListingService } from './business-listing.service';
|
import { BusinessListingService } from './business-listing.service';
|
||||||
import { CommercialPropertyService } from './commercial-property.service';
|
import { CommercialPropertyService } from './commercial-property.service';
|
||||||
|
import { isUUID } from '../utils/slug.utils';
|
||||||
|
|
||||||
@Controller('listings/undefined')
|
@Controller('listings/undefined')
|
||||||
export class UnknownListingsController {
|
export class UnknownListingsController {
|
||||||
@@ -16,6 +17,9 @@ export class UnknownListingsController {
|
|||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||||
|
if (!isUUID(id)) {
|
||||||
|
throw new BadRequestException(`Invalid identifier format: ${id}`);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return await this.businessListingsService.findBusinessesById(id, req.user);
|
return await this.businessListingsService.findBusinessesById(id, req.user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import express from 'express';
|
|||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { PICTURES_DIR } from './config/pictures.config';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const server = express();
|
const server = express();
|
||||||
@@ -14,7 +15,12 @@ async function bootstrap() {
|
|||||||
app.useLogger(logger);
|
app.useLogger(logger);
|
||||||
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
||||||
// Serve static files from pictures directory
|
// Serve static files from pictures directory
|
||||||
app.use('/pictures', express.static('pictures'));
|
app.use('/pictures', express.static(PICTURES_DIR));
|
||||||
|
// Prevent browsers from caching 404s on /pictures/*
|
||||||
|
app.use('/pictures', (_req, res) => {
|
||||||
|
res.set('Cache-Control', 'no-store');
|
||||||
|
res.status(404).end();
|
||||||
|
});
|
||||||
|
|
||||||
app.setGlobalPrefix('bizmatch');
|
app.setGlobalPrefix('bizmatch');
|
||||||
|
|
||||||
@@ -54,6 +60,17 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reject CSS/JS sourcemap requests before they reach any API controller.
|
||||||
|
// Sourcemap URLs resolve relative to the current page URL and can match
|
||||||
|
// wildcard route params (e.g. /bizmatch/user/default.css.map → @Get(':id')).
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.endsWith('.css.map') || req.path.endsWith('.js.map')) {
|
||||||
|
res.status(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
await app.listen(process.env.PORT || 3001);
|
await app.listen(process.env.PORT || 3001);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
|||||||
import { User } from '../models/db.model';
|
import { User } from '../models/db.model';
|
||||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model';
|
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
import { isUUID } from '../utils/slug.utils';
|
||||||
|
|
||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
@@ -29,6 +30,9 @@ export class UserController {
|
|||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findById(@Param('id') id: string): Promise<User> {
|
async findById(@Param('id') id: string): Promise<User> {
|
||||||
|
if (!isUUID(id)) {
|
||||||
|
throw new BadRequestException(`Invalid identifier format: ${id}`);
|
||||||
|
}
|
||||||
const user = await this.userService.getUserById(id);
|
const user = await this.userService.getUserById(id);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -81,6 +85,9 @@ export class UserController {
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Get('subscriptions/:id')
|
@Get('subscriptions/:id')
|
||||||
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
||||||
|
if (!isUUID(id)) {
|
||||||
|
throw new BadRequestException(`Invalid identifier format: ${id}`);
|
||||||
|
}
|
||||||
const subscriptions = [];
|
const subscriptions = [];
|
||||||
const user = await this.userService.getUserById(id);
|
const user = await this.userService.getUserById(id);
|
||||||
subscriptions.forEach(s => {
|
subscriptions.forEach(s => {
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ export function isValidSlug(slug: string): boolean {
|
|||||||
return lastPart.length === 8 && /^[a-z0-9]{8}$/.test(lastPart);
|
return lastPart.length === 8 && /^[a-z0-9]{8}$/.test(lastPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid UUID v4
|
||||||
|
*/
|
||||||
|
export function isUUID(param: string): boolean {
|
||||||
|
return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(param);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a parameter is a slug (vs a UUID)
|
* Check if a parameter is a slug (vs a UUID)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
],
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true,
|
"sourceMap": false,
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,9 +29,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<div class="relative">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="flex text-sm bg-neutral-400 rounded-full md:me-0 focus:ring-4 focus:ring-neutral-300 dark:focus:ring-neutral-600"
|
class="flex text-sm bg-neutral-400 rounded-full md:me-0 focus:ring-4 focus:ring-neutral-300 dark:focus:ring-neutral-600"
|
||||||
id="user-menu-button" aria-expanded="false" [attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'">
|
id="user-menu-button" aria-expanded="false" (click)="toggleUserMenu()">
|
||||||
<span class="sr-only">Open user menu</span>
|
<span class="sr-only">Open user menu</span>
|
||||||
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
|
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
|
||||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}"
|
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}"
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
<!-- Dropdown menu -->
|
<!-- Dropdown menu -->
|
||||||
@if(user){
|
@if(user){
|
||||||
<div
|
<div
|
||||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
class="absolute right-0 z-50 mt-2 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
||||||
id="user-login">
|
id="user-login" [class.hidden]="!userMenuVisible">
|
||||||
<div class="px-4 py-3">
|
<div class="px-4 py-3">
|
||||||
<span class="block text-sm text-neutral-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
<span class="block text-sm text-neutral-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
||||||
<span class="block text-sm text-neutral-500 truncate dark:text-neutral-400">{{ user.email }}</span>
|
<span class="block text-sm text-neutral-500 truncate dark:text-neutral-400">{{ user.email }}</span>
|
||||||
@@ -122,8 +123,8 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div
|
<div
|
||||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
class="absolute right-0 z-50 mt-2 text-base list-none bg-white divide-y divide-neutral-100 rounded-lg shadow dark:bg-neutral-700 dark:divide-neutral-600"
|
||||||
id="user-unknown">
|
id="user-unknown" [class.hidden]="!userMenuVisible">
|
||||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
<li>
|
<li>
|
||||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }"
|
<a routerLink="/login" [queryParams]="{ mode: 'login' }"
|
||||||
@@ -161,6 +162,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||||
<ul
|
<ul
|
||||||
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-neutral-100 rounded-lg bg-neutral-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-neutral-800 md:dark:bg-neutral-900 dark:border-neutral-700">
|
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-neutral-100 rounded-lg bg-neutral-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-neutral-800 md:dark:bg-neutral-900 dark:border-neutral-700">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||||
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
||||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
import { Collapse } from 'flowbite';
|
||||||
import { filter, Observable, Subject, takeUntil } from 'rxjs';
|
import { filter, Observable, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
|
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||||
@@ -38,8 +38,8 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
faUserGear = APP_ICONS.faUserGear;
|
faUserGear = APP_ICONS.faUserGear;
|
||||||
profileUrl: string;
|
profileUrl: string;
|
||||||
env = environment;
|
env = environment;
|
||||||
private filterDropdown: Dropdown | null = null;
|
|
||||||
isMobile: boolean = false;
|
isMobile: boolean = false;
|
||||||
|
userMenuVisible = false;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
prompt: string;
|
prompt: string;
|
||||||
private platformId = inject(PLATFORM_ID);
|
private platformId = inject(PLATFORM_ID);
|
||||||
@@ -109,15 +109,9 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.profileUrl = photoUrl;
|
this.profileUrl = photoUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
// User Updates - re-initialize Flowbite when user state changes
|
// User Updates
|
||||||
// This ensures the dropdown bindings are updated when the dropdown target changes
|
|
||||||
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
|
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
|
||||||
const previousUser = this.user;
|
|
||||||
this.user = u;
|
this.user = u;
|
||||||
// Re-initialize Flowbite if user logged in/out state changed
|
|
||||||
if ((previousUser === null) !== (u === null) && this.isBrowser) {
|
|
||||||
setTimeout(() => initFlowbite(), 50);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Router Events
|
// Router Events
|
||||||
@@ -235,15 +229,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
if (!this.isBrowser) return;
|
this.userMenuVisible = false;
|
||||||
|
|
||||||
const dropdownButton = document.getElementById('user-menu-button');
|
|
||||||
const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown');
|
|
||||||
|
|
||||||
if (dropdownButton && dropdownMenu) {
|
|
||||||
const dropdown = new Dropdown(dropdownMenu, dropdownButton);
|
|
||||||
dropdown.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeMobileMenu() {
|
closeMobileMenu() {
|
||||||
@@ -286,6 +272,10 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.sortDropdownVisible = !this.sortDropdownVisible;
|
this.sortDropdownVisible = !this.sortDropdownVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleUserMenu() {
|
||||||
|
this.userMenuVisible = !this.userMenuVisible;
|
||||||
|
}
|
||||||
|
|
||||||
get isProfessional() {
|
get isProfessional() {
|
||||||
return this.user?.customerType === 'professional';
|
return this.user?.customerType === 'professional';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,17 +99,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
||||||
<div *ngIf="listing.location.latitude && listing.location.longitude" class="mt-6">
|
<div *ngIf="listing.location?.latitude && listing.location?.longitude" class="mt-6">
|
||||||
<h2 class="text-xl font-semibold mb-2">Location Map</h2>
|
<h2 class="text-xl font-semibold mb-2">Location Map</h2>
|
||||||
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
||||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
||||||
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||||
<p class="mt-3 text-xs text-neutral-500 leading-relaxed">
|
|
||||||
The information on this listing has been provided by either the seller or a business broker representing the seller.
|
|
||||||
BizMatch, Inc. has no interest or stake in the sale of this business and has not verified any of the information
|
|
||||||
and assumes no responsibility for its accuracy, veracity, or completeness.
|
|
||||||
See our full <a class="text-primary-600 hover:underline hover:cursor-pointer" routerLink="/terms-of-use">Terms of Use</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,6 +135,16 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Disclaimer: volle Breite, unterhalb beider Spalten -->
|
||||||
|
<div class="px-6 pb-6">
|
||||||
|
<p class="disclaimer text-sm text-neutral-500 leading-relaxed border-t border-neutral-200 pt-4">
|
||||||
|
The information on this listing has been provided by either the seller or a business broker representing the
|
||||||
|
seller. BizMatch, Inc. has no interest or stake in the sale of this business and has not verified any of the
|
||||||
|
information and assumes no responsibility for its accuracy, veracity, or completeness. See our full
|
||||||
|
<a class="text-primary-600 hover:underline hover:cursor-pointer" routerLink="/terms-of-use">Terms of Use</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -209,8 +213,8 @@
|
|||||||
}
|
}
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="font-medium">Location:</span>
|
<span class="font-medium">Location:</span>
|
||||||
<span>{{ related.location.name || related.location.county }}, {{
|
<span>{{ related.location ? (related.location.name || related.location.county) : '—' }}, {{
|
||||||
selectOptions.getState(related.location.state) }}</span>
|
selectOptions.getState(related.location?.state) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|||||||
@@ -100,17 +100,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
||||||
<div *ngIf="listing.location.latitude && listing.location.longitude" class="mt-6">
|
<div *ngIf="listing.location?.latitude && listing.location?.longitude" class="mt-6">
|
||||||
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
|
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
|
||||||
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
||||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers"
|
||||||
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
[leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||||
<p class="mt-3 text-xs text-neutral-500 leading-relaxed">
|
|
||||||
The information on this listing has been provided by either the seller or a business broker representing the seller.
|
|
||||||
BizMatch, Inc. has no interest or stake in the sale of this business and has not verified any of the information
|
|
||||||
and assumes no responsibility for its accuracy, veracity, or completeness.
|
|
||||||
See our full <a class="text-primary-600 hover:underline hover:cursor-pointer" routerLink="/terms-of-use">Terms of Use</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,6 +148,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Disclaimer: volle Breite, unterhalb beider Spalten -->
|
||||||
|
<div class="pt-6">
|
||||||
|
<p class="disclaimer text-sm text-neutral-500 leading-relaxed border-t border-neutral-200 pt-4">
|
||||||
|
The information on this listing has been provided by either the seller or a business broker representing the
|
||||||
|
seller. BizMatch, Inc. has no interest or stake in the sale of this business and has not verified any of the
|
||||||
|
information and assumes no responsibility for its accuracy, veracity, or completeness. See our full
|
||||||
|
<a class="text-primary-600 hover:underline hover:cursor-pointer" routerLink="/terms-of-use">Terms of Use</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -217,8 +221,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="font-medium">Location:</span>
|
<span class="font-medium">Location:</span>
|
||||||
<span>{{ related.location.name || related.location.county }}, {{
|
<span>{{ related.location ? (related.location.name || related.location.county) : '—' }}, {{
|
||||||
selectOptions.getState(related.location.state) }}</span>
|
selectOptions.getState(related.location?.state) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||||||
}
|
}
|
||||||
if (this.listing.imageOrder && Array.isArray(this.listing.imageOrder)) {
|
if (this.listing.imageOrder && Array.isArray(this.listing.imageOrder)) {
|
||||||
this.listing.imageOrder.forEach(image => {
|
this.listing.imageOrder.forEach(image => {
|
||||||
const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}`;
|
const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}?_ts=${new Date(this.listing.updated).getTime()}`;
|
||||||
this.images.push(new ImageItem({ src: imageURL, thumb: imageURL }));
|
this.images.push(new ImageItem({ src: imageURL, thumb: imageURL }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||||||
yearBuilt: (this.listing as any).yearBuilt,
|
yearBuilt: (this.listing as any).yearBuilt,
|
||||||
images: this.listing.imageOrder?.length > 0
|
images: this.listing.imageOrder?.length > 0
|
||||||
? this.listing.imageOrder.map(img =>
|
? this.listing.imageOrder.map(img =>
|
||||||
`${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${img}`)
|
`${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${img}?_ts=${new Date(this.listing.updated).getTime()}`)
|
||||||
: []
|
: []
|
||||||
};
|
};
|
||||||
this.seoService.updateCommercialPropertyMeta(propertyData);
|
this.seoService.updateCommercialPropertyMeta(propertyData);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
:host ::ng-deep p {
|
:host ::ng-deep p:not(.disclaimer) {
|
||||||
display: block;
|
display: block;
|
||||||
//margin-top: 1em;
|
//margin-top: 1em;
|
||||||
//margin-bottom: 1em;
|
//margin-bottom: 1em;
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span
|
<span
|
||||||
class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-neutral-200 text-neutral-700 rounded-full">
|
class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-neutral-200 text-neutral-700 rounded-full">
|
||||||
{{ selectOptions.getState(listing.location.state) }}
|
{{ selectOptions.getState(listing.location?.state) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if (getListingBadge(listing); as badge) {
|
@if (getListingBadge(listing); as badge) {
|
||||||
@@ -127,8 +127,8 @@
|
|||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-neutral-600 mb-2">
|
<p class="text-sm text-neutral-600 mb-2">
|
||||||
<strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county ?
|
<strong>Location:</strong> {{ listing.location ? (listing.location.name ? listing.location.name : listing.location.county ?
|
||||||
listing.location.county : this.selectOptions.getState(listing.location.state) }}
|
listing.location.county : selectOptions.getState(listing.location.state)) : '—' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-neutral-600 mb-4"><strong>Years established:</strong> {{ listing.established }}</p>
|
<p class="text-sm text-neutral-600 mb-4"><strong>Years established:</strong> {{ listing.established }}</p>
|
||||||
@if(listing.imageName) {
|
@if(listing.imageName) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img [appLazyLoad]="env.imageBaseUrl + '/pictures/property/' + listing.imagePath + '/' + listing.serialId + '/' + listing.imageOrder[0]"
|
<img [appLazyLoad]="env.imageBaseUrl + '/pictures/property/' + listing.imagePath + '/' + listing.serialId + '/' + listing.imageOrder[0] + '?_ts=' + listing.updated"
|
||||||
[alt]="altText.generatePropertyListingAlt(listing)"
|
[alt]="altText.generatePropertyListingAlt(listing)"
|
||||||
class="w-full h-48 object-cover"
|
class="w-full h-48 object-cover"
|
||||||
width="400"
|
width="400"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
><i [class]="selectOptions.getIconAndTextColorTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span
|
><i [class]="selectOptions.getIconAndTextColorTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between my-2">
|
<div class="flex items-center justify-between my-2">
|
||||||
<span class="bg-neutral-200 text-neutral-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.location.state) }}</span>
|
<span class="bg-neutral-200 text-neutral-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.location?.state) }}</span>
|
||||||
<p class="text-sm text-neutral-600 mb-4">
|
<p class="text-sm text-neutral-600 mb-4">
|
||||||
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
|
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-neutral-600 mb-2">{{ listing.location.name ? listing.location.name : listing.location.county }}</p>
|
<p class="text-neutral-600 mb-2">{{ listing.location ? (listing.location.name ? listing.location.name : listing.location.county) : '—' }}</p>
|
||||||
<p class="text-xl font-bold mb-4">{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
|
<p class="text-xl font-bold mb-4">{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
<button [routerLink]="['/commercial-property', listing.slug || listing.id]" class="bg-success-500 text-white px-4 py-2 rounded-full w-full hover:bg-success-600 transition duration-300 mt-auto">
|
<button [routerLink]="['/commercial-property', listing.slug || listing.id]" class="bg-success-500 text-white px-4 py-2 rounded-full w-full hover:bg-success-600 transition duration-300 mt-auto">
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
<td class="py-2 px-4">{{ $any(listing).title }}</td>
|
<td class="py-2 px-4">{{ $any(listing).title }}</td>
|
||||||
<td class="py-2 px-4">{{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial Property' :
|
<td class="py-2 px-4">{{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial Property' :
|
||||||
'Business' }}</td>
|
'Business' }}</td>
|
||||||
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county }}, {{
|
<td class="py-2 px-4">{{ listing.location ? (listing.location.name ? listing.location.name : listing.location.county) : '—' }}, {{
|
||||||
listing.location.state }}</td>
|
listing.location?.state || '' }}</td>
|
||||||
<td class="py-2 px-4">${{ $any(listing).price.toLocaleString() }}</td>
|
<td class="py-2 px-4">${{ $any(listing).price ? $any(listing).price.toLocaleString() : 'Price on Request' }}</td>
|
||||||
<td class="py-2 px-4 flex">
|
<td class="py-2 px-4 flex">
|
||||||
@if($any(listing).listingsCategory==='business'){
|
@if($any(listing).listingsCategory==='business'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
@@ -74,9 +74,9 @@
|
|||||||
<h2 class="text-xl font-semibold mb-2">{{ $any(listing).title }}</h2>
|
<h2 class="text-xl font-semibold mb-2">{{ $any(listing).title }}</h2>
|
||||||
<p class="text-gray-600 mb-2">Category: {{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial
|
<p class="text-gray-600 mb-2">Category: {{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial
|
||||||
Property' : 'Business' }}</p>
|
Property' : 'Business' }}</p>
|
||||||
<p class="text-gray-600 mb-2">Located in: {{ listing.location.name ? listing.location.name :
|
<p class="text-gray-600 mb-2">Located in: {{ listing.location ? (listing.location.name ? listing.location.name :
|
||||||
listing.location.county }}, {{ listing.location.state }}</p>
|
listing.location.county) : '—' }}, {{ listing.location?.state || '' }}</p>
|
||||||
<p class="text-gray-600 mb-2">Price: ${{ $any(listing).price.toLocaleString() }}</p>
|
<p class="text-gray-600 mb-2">Price: ${{ $any(listing).price ? $any(listing).price.toLocaleString() : 'Price on Request' }}</p>
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
@if($any(listing).listingsCategory==='business'){
|
@if($any(listing).listingsCategory==='business'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<tr *ngFor="let listing of myListings" class="border-b">
|
<tr *ngFor="let listing of myListings" class="border-b">
|
||||||
<td class="py-2 px-4">{{ listing.title }}</td>
|
<td class="py-2 px-4">{{ listing.title }}</td>
|
||||||
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
|
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
|
||||||
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county ? listing.location.county : this.selectOptions.getState(listing.location.state) }}</td>
|
<td class="py-2 px-4">{{ listing.location ? (listing.location.name ? listing.location.name : listing.location.county ? listing.location.county : selectOptions.getState(listing.location.state)) : '—' }}</td>
|
||||||
<td class="py-2 px-4">${{ listing.price ? listing.price.toLocaleString() : '' }}</td>
|
<td class="py-2 px-4">${{ listing.price ? listing.price.toLocaleString() : '' }}</td>
|
||||||
<td class="py-2 px-4 flex justify-center">
|
<td class="py-2 px-4 flex justify-center">
|
||||||
{{ listing.internalListingNumber ?? '—' }}
|
{{ listing.internalListingNumber ?? '—' }}
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
|
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
|
||||||
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
|
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
|
||||||
<p class="text-gray-600 mb-2">Located in: {{ listing.location?.name ? listing.location.name : listing.location?.county }} - {{ listing.location?.state }}</p>
|
<p class="text-gray-600 mb-2">Located in: {{ listing.location?.name ? listing.location.name : listing.location?.county }} - {{ listing.location?.state }}</p>
|
||||||
<p class="text-gray-600 mb-2">Price: ${{ listing.price.toLocaleString() }}</p>
|
<p class="text-gray-600 mb-2">Price: ${{ listing.price ? listing.price.toLocaleString() : 'Price on Request' }}</p>
|
||||||
<p class="text-gray-600 mb-2">Internal #: {{ listing.internalListingNumber ?? '—' }}</p>
|
<p class="text-gray-600 mb-2">Internal #: {{ listing.internalListingNumber ?? '—' }}</p>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<span class="text-gray-600">Publication Status:</span>
|
<span class="text-gray-600">Publication Status:</span>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ./bizmatch-server/.env
|
- ./bizmatch-server/.env
|
||||||
|
environment:
|
||||||
|
# MUST match the container-side path of the volume mount below
|
||||||
|
PICTURES_DIR: /app/dist/pictures
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Reference in New Issue
Block a user