19 Commits

Author SHA1 Message Date
Frank John Begornia
4d051276b1 feat: enhance navbar and privacy policy layout for improved user experience 2026-04-17 18:21:48 +08:00
a4456631fb Merge pull request 'feat: enhance team store page layout and styling for improved user experience' (#4) from feat/teamstore-page-enhancement into master
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m24s
Reviewed-on: #4
2026-04-17 10:01:20 +00:00
Frank John Begornia
391df3bb41 feat: enhance team store page layout and styling for improved user experience 2026-04-17 18:00:36 +08:00
cf3b4ea476 Merge pull request 'style: enhance authentication forms with improved layout and design' (#3) from feat/auth-forms into master
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m10s
Reviewed-on: #3
2026-04-17 06:19:50 +00:00
Frank John Begornia
04675dfd37 style: enhance authentication forms with improved layout and design 2026-04-17 14:18:31 +08:00
Frank John Begornia
a29bca1931 fix: update storage paths for team store logo and banner uploads to use 'uploads/images' directory
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m4s
2026-04-17 13:39:15 +08:00
Frank John Begornia
bc5da01735 fix: update storage paths for store logo and banner uploads to include 'upload/images'
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m4s
2026-04-17 13:33:31 +08:00
Frank John Begornia
914e276026 fix: migrate store logo/banner uploads from sftp to minio
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m6s
2026-04-17 13:24:40 +08:00
Frank John Begornia
2e44012b8c fix: pin psr/http-message ^1.0 and guzzlehttp/psr7 ^1.4 to fix PsrStream __toString incompatibility
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m18s
2026-04-17 13:13:42 +08:00
Frank John Begornia
0fe2e2bae6 fix: register minio custom driver using flysystem-aws-s3-v3 (Laravel 5.0 hardcodes v2)
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m5s
2026-04-17 13:03:37 +08:00
Frank John Begornia
c6518e81c9 feat: update Dockerfile and AppServiceProvider for PHP 7.2 compatibility and improved error handling
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 7m7s
2026-04-17 12:52:38 +08:00
Frank John Begornia
289e11f3c5 feat: add MinIO S3 storage configuration to docker-compose for improved file management
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m31s
2026-04-16 23:55:22 +08:00
8eef632ebb Merge pull request 'feat/hifive-filters' (#2) from feat/hifive-filters into master
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 2m33s
Reviewed-on: #2
2026-04-16 15:41:40 +00:00
ef88a6b69b Merge pull request 'Refactor image upload handling in UserController to use MinIO for improved storage management' (#1) from fix/upload-image into master
Some checks failed
Deploy Production (crewsportswear.com) / deploy (push) Has been cancelled
Reviewed-on: #1
2026-04-16 15:40:59 +00:00
Frank John Begornia
a410208c62 Refactor image upload handling in UserController to use MinIO for improved storage management 2026-04-16 23:40:09 +08:00
Frank John Begornia
d4a6028599 chore: remove SSH Keys Setup guide to enhance security practices 2026-04-16 23:21:45 +08:00
Frank John Begornia
4888f93eac feat(teamstore): add league/conference sub-sub-category pill filter (hi-five-franchise-store only) 2026-04-16 23:19:19 +08:00
Frank John Begornia
49921a26a9 Add centering guidelines script and update script references in designer view
All checks were successful
Deploy Production (crewsportswear.com) / deploy (push) Successful in 3m7s
2026-04-02 03:33:19 +08:00
Frank John Begornia
3b6e0ec447 Refactor image handling in PrintPatternController and TemplatesController to use MinIO for improved storage management 2026-04-02 03:31:08 +08:00
26 changed files with 3199 additions and 866 deletions

View File

@@ -1,6 +1,26 @@
# Local Development MinIO Configuration
# Copy to .env.local and update with your MinIO credentials
# Local Development Configuration
# Copy to .env.local and fill in your values.
# MinIO credentials (get from your production server)
MINIO_KEY=secret_key
# ── MinIO credentials ─────────────────────────────────────────────────────────
MINIO_KEY=your_minio_root_user
MINIO_SECRET=your_minio_root_password
# ── SSH tunnel (remote DB) ────────────────────────────────────────────────────
# Only needed when starting with --profile ssh-db.
# NOTE: no inline comments allowed after values — Docker reads them literally.
SSH_HOST=136.114.183.15
SSH_PORT=22
SSH_USER=webmaster
# Must be an absolute path — Docker Compose does NOT expand ~
SSH_KEY_PATH=/Users/webmaster/.ssh/id_ed25519_crew_webmaster
SSH_DB_REMOTE_HOST=127.0.0.1
SSH_DB_REMOTE_PORT=3306
# Tell the app to route DB traffic through the tunnel container:
DB_HOST=db-tunnel
DB_PORT=3306
DB_DATABASE=custom_designs
DB_USERNAME=root
DB_PASSWORD=VeryStrongRootPass2025!

View File

@@ -1,6 +1,26 @@
# Local Development MinIO Configuration
# Copy to .env.local and update with your MinIO credentials
# Local Development Configuration
# Copy to .env.local and fill in your values.
# MinIO credentials (get from your production server)
# ── MinIO credentials ─────────────────────────────────────────────────────────
MINIO_KEY=your_minio_root_user
MINIO_SECRET=your_minio_root_password
# ── SSH tunnel (remote DB) ────────────────────────────────────────────────────
# Only needed when starting with --profile ssh-db.
# IMPORTANT: no inline comments after values — Docker Compose reads them literally.
# IMPORTANT: SSH_KEY_PATH must be an absolute path — ~ is NOT expanded by Docker Compose.
#
SSH_HOST=your.server.ip.or.hostname
SSH_PORT=22
SSH_USER=root
SSH_KEY_PATH=/absolute/path/to/your/private/key
#
SSH_DB_REMOTE_HOST=127.0.0.1 # DB host as seen from the SSH server
SSH_DB_REMOTE_PORT=3306 # DB port as seen from the SSH server
#
# Tell the app to route DB traffic through the tunnel container:
DB_HOST=db-tunnel
DB_PORT=3306
DB_DATABASE=your_remote_db_name
DB_USERNAME=your_remote_db_user
DB_PASSWORD=your_remote_db_password

View File

@@ -1,10 +1,10 @@
# Use PHP 7.0 with Apache (has native mcrypt support for Laravel 5.0)
FROM php:7.0-apache
# Use PHP 7.2 with Apache (mcrypt available via PECL, compatible with Laravel 5.0)
FROM php:7.2-apache
# Update to use archived Debian repositories
# Redirect to archived Debian Buster repositories (EOL)
RUN sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i '/stretch-updates/d' /etc/apt/sources.list
&& sed -i '/buster-updates/d' /etc/apt/sources.list
# Install system dependencies
RUN apt-get update && apt-get install -y --allow-unauthenticated \
@@ -18,17 +18,24 @@ RUN apt-get update && apt-get install -y --allow-unauthenticated \
libfreetype6-dev \
libjpeg62-turbo-dev \
openssh-client \
libzip-dev \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd
# Install PHP extensions (mcrypt is built-in for PHP 7.0)
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath mcrypt tokenizer zip
# Install mcrypt via PECL (removed from core in PHP 7.2)
RUN pecl install mcrypt-1.0.4 && docker-php-ext-enable mcrypt
# Suppress E_DEPRECATED so PECL mcrypt functions don't trigger ErrorException in Laravel 5.0
RUN echo "error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT" > /usr/local/etc/php/conf.d/suppress-deprecated.ini
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath tokenizer zip
# Enable Apache mod_rewrite
RUN a2enmod rewrite
# Install Composer (version 1.x for better compatibility with Laravel 5.0)
COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer
# Install Composer 2.2 (LTS version supporting PHP 7.2+)
COPY --from=composer:2.2 /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
@@ -51,8 +58,8 @@ RUN chown -R www-data:www-data /var/www/html \
# Create .env file if it doesn't exist
RUN if [ ! -f .env ]; then cp .env.example .env; fi
# Install PHP dependencies (Laravel 5.0 compatible)
RUN composer install --no-dev --no-interaction --prefer-dist
# Install PHP dependencies (--no-plugins skips kylekatarnls/update-helper which is Composer 1 only)
RUN composer install --no-dev --no-interaction --prefer-dist --no-plugins
# Generate application key
RUN php artisan key:generate || true
@@ -60,11 +67,7 @@ RUN php artisan key:generate || true
# Run Laravel 5.0 optimization
RUN php artisan clear-compiled && php artisan optimize
# Note: yakpro-po obfuscation requires PHP 7.1+, incompatible with PHP 7.0
# For code protection with PHP 7.0, consider:
# 1. ionCube Encoder (commercial, most secure)
# 2. Keeping source code private and using proper access controls
# 3. Using --optimize flag in composer (already done above)
# Note: PHP 7.2 is compatible with Laravel 5.0 and yakpro-po obfuscation
# Configure Apache DocumentRoot to point to Laravel's public directory
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public

17
Makefile Normal file
View File

@@ -0,0 +1,17 @@
COMPOSE_LOCAL = docker compose -f docker-compose.local.yml
# ── Local stack (local MariaDB) ───────────────────────────────────────────────
up:
$(COMPOSE_LOCAL) up --build
down:
$(COMPOSE_LOCAL) down
# ── Local stack with SSH tunnel to remote DB ──────────────────────────────────
up-ssh:
$(COMPOSE_LOCAL) --env-file .env.local --profile ssh-db up --build
down-ssh:
$(COMPOSE_LOCAL) --env-file .env.local --profile ssh-db down
.PHONY: up down up-ssh down-ssh

View File

@@ -1,125 +0,0 @@
# SSH Keys Setup Guide
## Security Notice
SSH private keys (.ppk, .pem, id_rsa, etc.) should **NEVER** be:
- Stored in the application directory
- Committed to git repositories
- Placed in web-accessible locations
## Recommended Setup
### 1. Create Secure Keys Directory on Server
```bash
# On your production server
sudo mkdir -p /var/crew-keys
sudo chmod 700 /var/crew-keys
```
### 2. Place Your SSH Key
```bash
# Copy your key to the secure location
sudo cp /path/to/your/root.ppk /var/crew-keys/
sudo chmod 600 /var/crew-keys/root.ppk
sudo chown root:root /var/crew-keys/root.ppk
```
### 3. Verify Permissions
```bash
ls -la /var/crew-keys/
# Should show: drwx------ (700) for directory
# Should show: -rw------- (600) for key file
```
## Docker Configuration
The `docker-compose.prod.yml` and `docker-compose.dev.yml` files are configured to mount `/var/crew-keys` as a **read-only** volume:
```yaml
volumes:
- /var/crew-keys:/var/keys:ro
```
The `:ro` flag ensures the container can only read the keys, not modify them.
## Application Configuration
The [config/filesystems.php](config/filesystems.php) references the key at:
```php
'privateKey' => '/var/keys/root.ppk',
```
This path is inside the container and maps to `/var/crew-keys/root.ppk` on the host.
## Testing
To verify the SFTP connection works:
```bash
docker exec crewsportswear_app_prod php -r "
use League\Flysystem\Sftp\SftpAdapter;
try {
\$adapter = new SftpAdapter([
'host' => '35.232.234.8',
'port' => 22,
'username' => 'root',
'privateKey' => '/var/keys/root.ppk',
'root' => '/var/www/html/images',
'timeout' => 10,
]);
echo 'SFTP connection: SUCCESS';
} catch (Exception \$e) {
echo 'SFTP connection failed: ' . \$e->getMessage();
}
"
```
## Troubleshooting
### Permission Denied
If you get permission errors:
```bash
# Fix directory permissions
sudo chmod 700 /var/crew-keys
# Fix key file permissions
sudo chmod 600 /var/crew-keys/root.ppk
```
### Key Format Issues
PuTTY keys (.ppk) may need conversion for Linux/PHP:
```bash
# Convert .ppk to OpenSSH format
puttygen root.ppk -O private-openssh -o /var/crew-keys/root.pem
chmod 600 /var/crew-keys/root.pem
```
Then update `filesystems.php`:
```php
'privateKey' => '/var/keys/root.pem',
```
## Security Best Practices
**DO:**
- Store keys outside application directory
- Use restrictive permissions (600 for files, 700 for directories)
- Mount as read-only in Docker
- Keep keys out of version control
- Use SSH key authentication instead of passwords
- Rotate keys regularly
**DON'T:**
- Commit keys to git
- Store in web-accessible directories
- Use world-readable permissions
- Share keys across multiple services
- Use password-protected keys without proper passphrase management

View File

@@ -5,6 +5,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Request1;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Models\PrintPatternModel;
use App\Models\SizesModel;
@@ -46,7 +47,7 @@ class PrintPatternController extends Controller {
$NewImageName = $templateSize.'.'.$imageExt;
$thumbnail = "uniform-templates/".$templatecode."/".$templateType."/SIZES/" . $NewImageName;
$thumbnail = "uploads/images/uniform-templates/".$templatecode."/".$templateType."/SIZES/" . $NewImageName;
$data = array(
'TemplateCode' => $templatecode,
@@ -58,9 +59,7 @@ class PrintPatternController extends Controller {
$i = $m->insertPrintPattern($data);
//var_dump($data);
if($i){
$r = $request->file('preview_print_template')->move(
base_path() . "/public/images/uniform-templates/".$templatecode."/".$templateType."/SIZES/", $NewImageName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templatecode.'/'.$templateType.'/SIZES/'.$NewImageName, file_get_contents($request->file('preview_print_template')->getRealPath()));
echo '<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<h4><i class="icon fa fa-check"></i> Success!</h4>

View File

@@ -4,6 +4,7 @@ use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Models\TemplatesModel;
use App\Models\SportsModel;
use App\Models\PrintPatternModel;
@@ -48,7 +49,7 @@ class TemplatesController extends Controller {
<h3><?php echo $row->TemplateName ?></h3>
</div>
<div class="sports-border">
<a href=""><img src="<?php echo url('public') . "/" . $row->Thumbnail ?>" alt="Sports" height="400px;" class="img img-responsive product-center" /></a>
<a href=""><img src="<?php echo minio_url($row->Thumbnail) ?>" alt="Sports" height="400px;" class="img img-responsive product-center" /></a>
<div class="sport-edit-btn">
<a href="<?php echo url('admin/templates') . "/edit/" . $row->TemplateCode . "/" ?>" class="btn btn-primary btn-block"><i class="fa fa-edit"></i> Edit</a>
</div>
@@ -129,15 +130,13 @@ class TemplatesController extends Controller {
if($i){
$request->file('tempateImage')->move(
base_path() . '/public/images/templates/thumbnail', $NewImageName
);
Storage::disk('minio')->put('images/templates/thumbnail/' . $NewImageName, file_get_contents($request->file('tempateImage')->getRealPath()));
//for front jersey
if(!empty($request->file('svgJerseyFront')->getClientOriginalName())){
$svgName = $request->file('svgJerseyFront')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
//var_dump($svgThumbnail);
$Templatedata = array(
'TemplateCode' => $templateCode,
@@ -149,16 +148,14 @@ class TemplatesController extends Controller {
$i = $m->insertTempaltePaths($Templatedata);
if($i){
$request->file('svgJerseyFront')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgJerseyFront')->getRealPath()));
}
}
if(!empty($request->file('svgJerseyBack')->getClientOriginalName())){
$svgName = $request->file('svgJerseyBack')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'TemplateCode' => $templateCode,
@@ -170,16 +167,14 @@ class TemplatesController extends Controller {
$i = $m->insertTempaltePaths($Templatedata);
if($i){
$request->file('svgJerseyBack')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgJerseyBack')->getRealPath()));
}
}
if(!empty($request->file('svgShortRight')->getClientOriginalName())){
$svgName = $request->file('svgShortRight')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'TemplateCode' => $templateCode,
@@ -191,16 +186,14 @@ class TemplatesController extends Controller {
$i = $m->insertTempaltePaths($Templatedata);
if($i){
$request->file('svgShortRight')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgShortRight')->getRealPath()));
}
}
if(!empty($request->file('svgShortLeft')->getClientOriginalName())){
$svgName = $request->file('svgShortLeft')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'TemplateCode' => $templateCode,
@@ -212,9 +205,7 @@ class TemplatesController extends Controller {
$i = $m->insertTempaltePaths($Templatedata);
if($i){
$request->file('svgShortLeft')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgShortLeft')->getRealPath()));
}
}
@@ -270,9 +261,7 @@ class TemplatesController extends Controller {
'PatternId' => $getSkins
);
$request->file('tempateImage')->move(
base_path() . '/public/images/templates/thumbnail', $NewImageName
);
Storage::disk('minio')->put('images/templates/thumbnail/' . $NewImageName, file_get_contents($request->file('tempateImage')->getRealPath()));
}else{
@@ -298,7 +287,7 @@ class TemplatesController extends Controller {
//echo 'meron jerset front';
$svgName = $request->file('svgJerseyFront')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'Type' => 'Jersey',
@@ -308,9 +297,7 @@ class TemplatesController extends Controller {
$i = $m->updateTemplatePaths($Templatedata, $post['id_svgJerseyFront']);
if($i){
$request->file('svgJerseyFront')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgJerseyFront')->getRealPath()));
//echo 'image move success';
}
}
@@ -318,7 +305,7 @@ class TemplatesController extends Controller {
if (array_key_exists('svgJerseyBack', $post)) {
$svgName = $request->file('svgJerseyBack')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'Type' => 'Jersey',
@@ -327,16 +314,13 @@ class TemplatesController extends Controller {
);
$i = $m->updateTemplatePaths($Templatedata, $post['id_svgJerseyBack']);
if($i){
$request->file('svgJerseyBack')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgJerseyBack')->getRealPath()));
}
}
if (array_key_exists('svgShortRight', $post)) {
$svgName = $request->file('svgShortRight')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'Type' => 'Shorts',
@@ -346,15 +330,13 @@ class TemplatesController extends Controller {
$i = $m->updateTemplatePaths($Templatedata, $post['id_svgShortRight']);
if($i){
$request->file('svgShortRight')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgShortRight')->getRealPath()));
}
}
if (array_key_exists('svgShortLeft', $post)) {
$svgName = $request->file('svgShortLeft')->getClientOriginalName();
$svgThumbnail = "uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$svgThumbnail = "uploads/images/uniform-templates/".$templateCode."/DISPLAY/".$svgName;
$Templatedata = array(
'Type' => 'Shorts',
@@ -364,9 +346,7 @@ class TemplatesController extends Controller {
$i = $m->updateTemplatePaths($Templatedata, $post['id_svgShortLeft']);
if($i){
$request->file('svgShortLeft')->move(
base_path() . '/public/images/uniform-templates/'.$templateCode. '/DISPLAY' , $svgName
);
Storage::disk('minio')->put('uploads/images/uniform-templates/'.$templateCode.'/DISPLAY/'.$svgName, file_get_contents($request->file('svgShortLeft')->getRealPath()));
}
}

View File

@@ -761,10 +761,7 @@ class UserController extends Controller
);
$u = $UserModel->insertNewProductThumbnails($thumbs);
// var_dump($thumbs);
Storage::disk('sftp')->put($thumbnail, fopen($request->file('imgupload')[$i], 'r+')); //live
//Storage::disk('localdir')->put($thumbnail, fopen($request->file('imgupload')[$i], 'r+'));
// var_dump($s);
Storage::disk('minio')->put('images/' . $thumbnail, file_get_contents($request->file('imgupload')[$i]->getRealPath()));
}
$prod_code = array('ProductCode' => $getYear . '-' . str_pad($id, 10, '0', STR_PAD_LEFT));
@@ -808,9 +805,7 @@ class UserController extends Controller
);
$u = $UserModel->insertNewProductThumbnails($thumbs);
Storage::disk('sftp')->put($thumbnail, fopen($request->file('upload_images')[$i], 'r+')); //live
//Storage::disk('localdir')->put($thumbnail, fopen($request->file('upload_images')[$i], 'r+'));
Storage::disk('minio')->put('images/' . $thumbnail, file_get_contents($request->file('upload_images')[$i]->getRealPath()));
}
@@ -826,9 +821,8 @@ class UserController extends Controller
$id = $request->thumb_id;
$UserModel = new UserModel;
$storagePath = Storage::disk('sftp')->getDriver()->getAdapter()->getPathPrefix();
if (file_exists($storagePath . $file)) {
unlink($storagePath . $file);
if (Storage::disk('minio')->exists('images/' . $file)) {
Storage::disk('minio')->delete('images/' . $file);
}
$i = $UserModel->deleteImageThumb('Id', $id);
@@ -973,11 +967,11 @@ class UserController extends Controller
// var_dump($res);
// if($res){
if ($request->file('store_logo') != null) {
Storage::disk('uploads')->put('/teamstore/' . $orig_store_url . '/' . $store_logo_name, fopen($request->file('store_logo'), 'r+'));
Storage::disk('minio')->put('uploads/images/teamstore/' . $orig_store_url . '/' . $store_logo_name, file_get_contents($request->file('store_logo')->getRealPath()));
}
if ($request->file('store_banner') != null) {
Storage::disk('uploads')->put('/teamstore/' . $orig_store_url . '/' . $store_banner_name, fopen($request->file('store_banner'), 'r+'));
Storage::disk('minio')->put('uploads/images/teamstore/' . $orig_store_url . '/' . $store_banner_name, file_get_contents($request->file('store_banner')->getRealPath()));
}
return response()->json(array(

View File

@@ -4,6 +4,8 @@ use Illuminate\Support\ServiceProvider;
use Storage;
use League\Flysystem\Filesystem;
use League\Flysystem\Sftp\SftpAdapter;
use League\Flysystem\AwsS3v3\AwsS3Adapter as AwsS3v3Adapter;
use Aws\S3\S3Client;
class AppServiceProvider extends ServiceProvider {
@@ -27,6 +29,21 @@ class AppServiceProvider extends ServiceProvider {
Storage::extend('sftp', function ($app, $config) {
return new Filesystem(new SftpAdapter($config));
});
Storage::extend('minio', function ($app, $config) {
$client = new S3Client([
'credentials' => [
'key' => $config['key'],
'secret' => $config['secret'],
],
'region' => $config['region'],
'version' => 'latest',
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => filter_var($config['use_path_style_endpoint'] ?? true, FILTER_VALIDATE_BOOLEAN),
]);
$adapter = new AwsS3v3Adapter($client, $config['bucket']);
return new Filesystem($adapter);
});
}
/**
@@ -40,6 +57,10 @@ class AppServiceProvider extends ServiceProvider {
*/
public function register()
{
// Laravel's HandleExceptions sets error_reporting(-1) which causes PECL mcrypt
// deprecation notices to become ErrorExceptions. Override it here to suppress E_DEPRECATED.
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
$this->app->bind(
'Illuminate\Contracts\Auth\Registrar',
'App\Services\Registrar'

View File

@@ -12,7 +12,10 @@
"google/recaptcha": "~1.1",
"spatie/laravel-analytics": "^1.4",
"league/flysystem-sftp": "^1.0",
"aws/aws-sdk-php": "~3.0"
"league/flysystem-aws-s3-v3": "~1.0",
"aws/aws-sdk-php": "~3.0",
"psr/http-message": "^1.0",
"guzzlehttp/psr7": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "~4.0",

647
composer.lock generated
View File

@@ -4,8 +4,153 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9ad9cbf7c7c319c392284bef379f0004",
"content-hash": "0320d93525d3aeed0db29b492fe6f3cc",
"packages": [
{
"name": "aws/aws-crt-php",
"version": "v1.2.7",
"source": {
"type": "git",
"url": "https://github.com/awslabs/aws-crt-php.git",
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "AWS SDK Common Runtime Team",
"email": "aws-sdk-common-runtime@amazon.com"
}
],
"description": "AWS Common Runtime for PHP",
"homepage": "https://github.com/awslabs/aws-crt-php",
"keywords": [
"amazon",
"aws",
"crt",
"sdk"
],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
},
"time": "2024-10-18T22:15:13+00:00"
},
{
"name": "aws/aws-sdk-php",
"version": "3.226.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "d76d4fe0fa603ddc3f5c54d9664438dc1a808859"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d76d4fe0fa603ddc3f5c54d9664438dc1a808859",
"reference": "d76d4fe0fa603ddc3f5c54d9664438dc1a808859",
"shasum": ""
},
"require": {
"aws/aws-crt-php": "^1.0.2",
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.1.1",
"mtdowling/jmespath.php": "^2.6",
"php": ">=5.5"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"behat/behat": "~3.0",
"doctrine/cache": "~1.4",
"ext-dom": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"nette/neon": "^2.3",
"paragonie/random_compat": ">= 2",
"phpunit/phpunit": "^4.8.35 || ^5.6.3",
"psr/cache": "^1.0",
"psr/simple-cache": "^1.0",
"sebastian/comparator": "^1.2.3"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"ext-curl": "To send requests using cURL",
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Aws\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"homepage": "http://aws.amazon.com/sdkforphp",
"keywords": [
"amazon",
"aws",
"cloud",
"dynamodb",
"ec2",
"glacier",
"s3",
"sdk"
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.226.0"
},
"time": "2022-06-16T18:14:10+00:00"
},
{
"name": "classpreloader/classpreloader",
"version": "1.4.0",
@@ -365,6 +510,190 @@
],
"time": "2019-10-30T09:32:00+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.5.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2023-05-21T12:31:43+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
"reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.9.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2023-04-17T16:00:37+00:00"
},
{
"name": "guzzlehttp/ringphp",
"version": "1.1.1",
@@ -907,6 +1236,71 @@
],
"time": "2019-10-16T21:01:05+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "1.0.30",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "af286f291ebab6877bac0c359c6c2cb017eb061d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af286f291ebab6877bac0c359c6c2cb017eb061d",
"reference": "af286f291ebab6877bac0c359c6c2cb017eb061d",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.20.0",
"league/flysystem": "^1.0.40",
"php": ">=5.5.0"
},
"require-dev": {
"henrikbjorn/phpspec-code-coverage": "~1.0.1",
"phpspec/phpspec": "^2.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3v3\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frenky.net"
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.30"
},
"funding": [
{
"url": "https://offset.earth/frankdejonge",
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
"type": "tidelift"
}
],
"time": "2022-07-02T13:51:38+00:00"
},
{
"name": "league/flysystem-sftp",
"version": "1.0.22",
@@ -1071,6 +1465,72 @@
],
"time": "2017-01-23T04:29:33+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
"reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
"reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"composer/xdebug-handler": "^3.0.3",
"phpunit/phpunit": "^8.5.33"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
},
"autoload": {
"files": [
"src/JmesPath.php"
],
"psr-4": {
"JmesPath\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.8.0"
},
"time": "2024-09-04T18:46:31+00:00"
},
{
"name": "nesbot/carbon",
"version": "1.39.1",
@@ -1409,6 +1869,59 @@
],
"time": "2019-09-17T03:41:22+00:00"
},
{
"name": "psr/http-message",
"version": "1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/1.1"
},
"time": "2023-04-04T09:50:52+00:00"
},
{
"name": "psr/log",
"version": "1.1.2",
@@ -1527,6 +2040,50 @@
],
"time": "2015-03-26T18:43:54+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "react/promise",
"version": "v2.7.1",
@@ -2158,6 +2715,91 @@
],
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php56",
"version": "v1.12.0",
@@ -3909,5 +4551,6 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "2.2.0"
}

View File

@@ -88,7 +88,7 @@ return [
],
'minio' => [
'driver' => 's3',
'driver' => 'minio',
'key' => env('MINIO_KEY'),
'secret' => env('MINIO_SECRET'),
'region' => env('MINIO_REGION', 'us-east-1'),

View File

@@ -1,3 +1,19 @@
# ─────────────────────────────────────────────────────────────────────────────
# Local development stack
#
# Default (local MariaDB):
# docker compose -f docker-compose.local.yml up --build
#
# Remote DB via SSH private-key tunnel:
# 1. Set SSH_HOST, SSH_USER, SSH_KEY_PATH, SSH_DB_REMOTE_HOST,
# SSH_DB_REMOTE_PORT (and DB_* creds) in .env.local
# 2. docker compose -f docker-compose.local.yml --profile ssh-db up --build
# The app will talk to db-tunnel (port 3306) instead of the local db.
#
# App: http://localhost:8082
# phpMyAdmin: http://localhost:8083
# ─────────────────────────────────────────────────────────────────────────────
services:
db:
image: mariadb:10.6
@@ -29,11 +45,7 @@ services:
- APP_DEBUG=true
- APP_URL=http://localhost:8082
- DB_CONNECTION=mysql
- DB_HOST=db
- DB_PORT=3306
- DB_DATABASE=crewsportswear
- DB_USERNAME=crewsportswear
- DB_PASSWORD=secret
- DB_PORT=${DB_PORT:-3306}
- PROD_PRIVATE=http://localhost:8082
- IMAGES_URL=http://localhost:8082
- UPLOAD_URL=http://localhost:8082/uploads/
@@ -56,15 +68,65 @@ services:
- MINIO_REGION=us-east-1
- MINIO_USE_PATH_STYLE=false
- MINIO_URL=https://minio.crewsportswear.app
# DB_HOST defaults to local container; set to db-tunnel in .env.local for SSH mode
- DB_HOST=${DB_HOST:-db}
- DB_DATABASE=${DB_DATABASE:-crewsportswear}
- DB_USERNAME=${DB_USERNAME:-crewsportswear}
- DB_PASSWORD=${DB_PASSWORD:-secret}
env_file:
- path: .env.local
required: false
volumes:
- ./:/var/www/html
- ./storage:/var/www/html/storage
- ./public/uploads:/var/www/html/public/uploads
# Keep the vendor/ directory from the image — prevents the bind-mount
# from wiping out the composer install done during docker build.
- vendor_local:/var/www/html/vendor
depends_on:
- db
networks:
- crewsportswear-local
# ── SSH tunnel to a remote database ────────────────────────────────────────
# Activated only with: --profile ssh-db
# Requires SSH_HOST, SSH_USER, SSH_KEY_PATH (and optionally SSH_PORT,
# SSH_DB_REMOTE_HOST, SSH_DB_REMOTE_PORT) set in .env.local.
db-tunnel:
profiles:
- ssh-db
image: alpine:3.19
container_name: crewsportswear_db_tunnel
restart: unless-stopped
environment:
- SSH_HOST=${SSH_HOST}
- SSH_PORT=${SSH_PORT:-22}
- SSH_USER=${SSH_USER:-root}
- SSH_DB_REMOTE_HOST=${SSH_DB_REMOTE_HOST:-127.0.0.1}
- SSH_DB_REMOTE_PORT=${SSH_DB_REMOTE_PORT:-3306}
volumes:
# Mount your private key read-only; path configured in .env.local
- ${SSH_KEY_PATH:-~/.ssh/id_rsa}:/ssh/id_rsa:ro
command:
- sh
- -c
- |
apk add --no-cache openssh-client
cp /ssh/id_rsa /tmp/id_rsa
chmod 600 /tmp/id_rsa
echo "Starting SSH tunnel to $$SSH_HOST..."
exec ssh -N \
-o StrictHostKeyChecking=no \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-i /tmp/id_rsa \
-L "0.0.0.0:3306:$$SSH_DB_REMOTE_HOST:$$SSH_DB_REMOTE_PORT" \
-p "$$SSH_PORT" \
"$$SSH_USER@$$SSH_HOST"
networks:
- crewsportswear-local
phpmyadmin:
image: arm64v8/phpmyadmin
platform: linux/arm64
@@ -87,3 +149,4 @@ networks:
volumes:
db_data:
vendor_local:

View File

@@ -28,6 +28,14 @@ services:
- ANALYTICS_SITE_ID=${ANALYTICS_SITE_ID}
- ANALYTICS_CLIENT_ID=${ANALYTICS_CLIENT_ID}
- ANALYTICS_SERVICE_EMAIL=${ANALYTICS_SERVICE_EMAIL}
# MinIO S3 Storage
- MINIO_ENDPOINT=${MINIO_ENDPOINT:-http://crew-minio-prod:9000}
- MINIO_KEY=${MINIO_KEY}
- MINIO_SECRET=${MINIO_SECRET}
- MINIO_BUCKET=${MINIO_BUCKET:-crewsportswear}
- MINIO_REGION=${MINIO_REGION:-us-east-1}
- MINIO_USE_PATH_STYLE=${MINIO_USE_PATH_STYLE:-true}
- MINIO_URL=${MINIO_URL:-https://minio.crewsportswear.app}
volumes:
- ./storage:/var/www/html/storage
- ./public/uploads:/var/www/html/public/uploads

View File

@@ -13,5 +13,11 @@ mkdir -p bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
# Install/update Composer dependencies if vendor is missing or composer.json changed
if [ ! -f vendor/autoload.php ]; then
echo "vendor/autoload.php not found — running composer install..."
composer install --no-interaction --prefer-dist
fi
# Execute the main command
exec "$@"

View File

@@ -0,0 +1,90 @@
/**
* Augments canvas by assigning to `onObjectMove` and `onAfterRender`.
* This kind of sucks because other code using those methods will stop functioning.
* Need to fix it by replacing callbacks with pub/sub kind of subscription model.
* (or maybe use existing fabric.util.fire/observe (if it won't be too slow))
*/
function initCenteringGuidelines(canvas) {
var canvasWidth = canvas.getWidth(),
canvasHeight = canvas.getHeight(),
canvasWidthCenter = canvasWidth / 2,
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = { },
canvasHeightCenterMap = { },
centerLineMargin = 4,
centerLineColor = 'rgba(255,0,241,0.5)',
centerLineWidth = 1,
ctx = canvas.getSelectionContext(),
viewportTransform;
for (var i = canvasWidthCenter - centerLineMargin, len = canvasWidthCenter + centerLineMargin; i <= len; i++) {
canvasWidthCenterMap[Math.round(i)] = true;
}
for (var i = canvasHeightCenter - centerLineMargin, len = canvasHeightCenter + centerLineMargin; i <= len; i++) {
canvasHeightCenterMap[Math.round(i)] = true;
}
function showVerticalCenterLine() {
showCenterLine(canvasWidthCenter + 0.5, 0, canvasWidthCenter + 0.5, canvasHeight);
}
function showHorizontalCenterLine() {
showCenterLine(0, canvasHeightCenter + 0.5, canvasWidth, canvasHeightCenter + 0.5);
}
function showCenterLine(x1, y1, x2, y2) {
ctx.save();
ctx.strokeStyle = centerLineColor;
ctx.lineWidth = centerLineWidth;
ctx.beginPath();
ctx.moveTo(x1 * viewportTransform[0], y1 * viewportTransform[3]);
ctx.lineTo(x2 * viewportTransform[0], y2 * viewportTransform[3]);
ctx.stroke();
ctx.restore();
}
var afterRenderActions = [],
isInVerticalCenter,
isInHorizontalCenter;
canvas.on('mouse:down', function () {
viewportTransform = canvas.viewportTransform;
});
canvas.on('object:moving', function(e) {
var object = e.target,
objectCenter = object.getCenterPoint(),
transform = canvas._currentTransform;
if (!transform) return;
isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap,
isInHorizontalCenter = Math.round(objectCenter.y) in canvasHeightCenterMap;
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), 'center', 'center');
}
});
canvas.on('before:render', function() {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on('after:render', function() {
if (isInVerticalCenter) {
showVerticalCenterLine();
}
if (isInHorizontalCenter) {
showHorizontalCenterLine();
}
});
canvas.on('mouse:up', function() {
// clear these values, to stop drawing guidelines once mouse is up
isInVerticalCenter = isInHorizontalCenter = null;
canvas.renderAll();
});
}

View File

@@ -2,58 +2,168 @@
@section('content')
<style>
.error{
color: red;
html,
body {
height: 100%;
background: #ffffff;
margin: 0;
overflow: hidden;
}
.form-wrapper{
margin-top: 20%;
.navbar-custom {
display: block;
margin-bottom: 0;
}
.login-wrapper {
height: calc(100vh - 100px);
height: calc(100dvh - 100px);
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.login-card {
width: 100%;
max-width: 380px;
background: #ffffff;
border: 1px solid #eceff3;
border-radius: 12px;
padding: 28px;
}
.login-title {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
margin: 0 0 8px;
color: #111827;
}
.login-subtitle {
font-size: 14px;
margin: 0 0 24px;
color: #6b7280;
}
.login-card .form-group {
margin-bottom: 16px;
}
.login-card label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #374151;
}
.login-card .form-control {
height: 42px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
font-size: 14px;
padding: 0 12px;
box-shadow: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.login-card .form-control:focus {
border-color: #9ca3af;
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.2);
outline: none;
}
.login-remember-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 6px 0 18px;
}
.login-remember-row label {
margin: 0;
font-size: 13px;
font-weight: 400;
color: #4b5563;
}
.login-remember-row a {
font-size: 13px;
color: #4b5563;
text-decoration: none;
}
.login-remember-row a:hover {
color: #111827;
}
.btn-login {
width: 100%;
height: 44px;
border: 0;
border-radius: 8px;
background: #111827;
color: #ffffff;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s;
}
.btn-login:hover {
opacity: 0.92;
color: #ffffff;
}
.login-footer {
margin-top: 18px;
text-align: center;
font-size: 13px;
color: #6b7280;
}
.login-footer a {
color: #111827;
text-decoration: none;
font-weight: 500;
}
.login-footer a:hover {
text-decoration: underline;
}
#login-response-msg .alert {
border-radius: 8px;
font-size: 13px;
margin-bottom: 14px;
}
</style>
<div class="container">
<div class="col-md-4 col-md-offset-4">
<div class="form-wrapper">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="text-center">S I G N - I N</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<div id="login-response-msg"></div>
<form role="form" id="frm-login">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="username" class="control-label">Email Address</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" title="Please enter your email address" placeholder="example@gmail.com">
<span class="help-block"></span>
</div>
<div class="form-group">
<label for="password" class="control-label">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password" title="Please enter your password">
<span class="help-block"></span>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember login
</label>
<p class="help-block">(if this is a private computer)</p>
</div>
<button type="submit" id="btn-login" class="btn btn-success btn-block"><i class="fa fa-sign-in"></i> &nbsp; Sign in</button>
<a href="{{ url('/password/email') }}" class="btn btn-link btn-block">Forgot Your Password?</a>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<hr />
<p class="text-center">Don't have an account? Register Now!</p>
<a href="{{ url('/auth/register') }}" type="submit" id="btn-login" class="btn btn-primary btn-block">Register</a>
</div>
</div>
</div>
<div class="login-wrapper">
<div class="login-card">
<h1 class="login-title">Sign in</h1>
<p class="login-subtitle">Welcome back. Please enter your details.</p>
<div id="login-response-msg"></div>
<form role="form" id="frm-login">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" class="form-control" name="email" id="email" value="{{ old('email') }}" placeholder="you@example.com" title="Please enter your email address">
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="Enter your password" title="Please enter your password">
</div>
<div class="login-remember-row">
<label><input type="checkbox" name="remember"> Remember me</label>
<a href="{{ url('/password/email') }}">Forgot password?</a>
</div>
<button type="submit" id="btn-login" class="btn-login">Sign in</button>
</form>
<p class="login-footer">Dont have an account? <a href="{{ url('/auth/register') }}">Create one</a></p>
</div>
</div>
@endsection

View File

@@ -2,54 +2,154 @@
@section('content')
<style>
.error{
color: red;
body {
background: #ffffff;
margin: 0;
}
.form-wrapper{
margin-top: 20%;
.navbar-custom {
margin-bottom: 0;
}
.auth-wrapper {
min-height: calc(100vh - 100px);
min-height: calc(100dvh - 100px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
box-sizing: border-box;
}
.auth-card {
width: 100%;
max-width: 380px;
background: #ffffff;
border: 1px solid #eceff3;
border-radius: 12px;
padding: 28px;
}
.auth-title {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
margin: 0 0 8px;
color: #111827;
}
.auth-subtitle {
font-size: 14px;
margin: 0 0 20px;
color: #6b7280;
}
.auth-card .form-group {
margin-bottom: 14px;
}
.auth-card label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #374151;
}
.auth-card .form-control {
height: 42px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
font-size: 14px;
padding: 0 12px;
box-shadow: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.auth-card .form-control:focus {
border-color: #9ca3af;
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.2);
outline: none;
}
.btn-auth {
width: 100%;
height: 44px;
border: 0;
border-radius: 8px;
background: #111827;
color: #ffffff;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s;
}
.btn-auth:hover {
opacity: 0.92;
color: #ffffff;
}
.auth-footer {
margin-top: 16px;
text-align: center;
font-size: 13px;
color: #6b7280;
}
.auth-footer a {
color: #111827;
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
.auth-card .alert {
border-radius: 8px;
font-size: 13px;
}
.auth-card .alert ul {
padding-left: 18px;
margin-bottom: 0;
}
</style>
<div class="container">
<div class="col-md-4 col-md-offset-4">
<div class="form-wrapper">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="text-center">R E S E T &nbsp; P A S S W O R D</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form role="form" method="POST" action="{{ url('/password/email') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="username" class="control-label">Email Address</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" title="Please enter your email address" placeholder="example@gmail.com">
<span class="help-block"></span>
</div>
<button type="submit" class="btn btn-default btn-block">Send Password Reset Link</button>
</form>
<br><br>
</div>
</div>
</div>
<div class="auth-wrapper">
<div class="auth-card">
<h1 class="auth-title">Forgot password</h1>
<p class="auth-subtitle">Enter your email and well send you a reset link.</p>
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
</div>
@endif
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form role="form" method="POST" action="{{ url('/password/email') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" class="form-control" name="email" id="email" value="{{ old('email') }}" title="Please enter your email address" placeholder="you@example.com">
</div>
<button type="submit" class="btn-auth">Send reset link</button>
</form>
<p class="auth-footer"><a href="{{ url('/auth/login') }}">Back to sign in</a></p>
</div>
</div>
@endsection

View File

@@ -2,136 +2,242 @@
@section('content')
<style>
.error {
color: red;
body {
background: #ffffff;
margin: 0;
}
.navbar-custom {
margin-bottom: 0;
}
.auth-wrapper {
min-height: calc(100vh - 100px);
min-height: calc(100dvh - 100px);
display: flex;
align-items: flex-start;
justify-content: center;
padding: 24px 16px;
box-sizing: border-box;
}
.auth-card {
width: 100%;
max-width: 560px;
background: #ffffff;
border: 1px solid #eceff3;
border-radius: 12px;
padding: 28px;
}
.auth-title {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
margin: 0 0 8px;
color: #111827;
}
.auth-subtitle {
font-size: 14px;
margin: 0 0 20px;
color: #6b7280;
}
.auth-section-title {
font-size: 12px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 8px 0 12px;
}
.auth-card .form-group {
margin-bottom: 14px;
}
.auth-card label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #374151;
}
.auth-card .form-control {
height: 42px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
font-size: 14px;
padding: 0 12px;
box-shadow: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.auth-card .form-control:focus {
border-color: #9ca3af;
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.2);
outline: none;
}
.auth-card select.form-control {
padding-right: 28px;
}
.g-recaptcha {
width: 100%;
}
.auth-terms {
font-size: 13px;
color: #6b7280;
margin-bottom: 14px;
}
.auth-terms a {
color: #111827;
text-decoration: none;
}
.auth-terms a:hover {
text-decoration: underline;
}
.btn-register-modern {
width: 100%;
height: 44px;
border: 0;
border-radius: 8px;
background: #111827;
color: #ffffff;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s;
}
.btn-register-modern:hover {
opacity: 0.92;
color: #ffffff;
}
.auth-footer {
margin-top: 16px;
text-align: center;
font-size: 13px;
color: #6b7280;
}
.auth-footer a {
color: #111827;
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
#register-response-msg .alert {
border-radius: 8px;
font-size: 13px;
margin-bottom: 14px;
}
</style>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="form-wrapper">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="text-center">R E G I S T E R</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<div id="register-response-msg"></div>
<form role="form" id="frm-register">
<div class="form-group text-center">
<h5>Personal Information</h5>
</div>
<input type="hidden" name="redirect" value="{{ Request::get('redirectUrl') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label class="control-label">First name</label>
<input type="text" class="form-control" name="firstname" placeholder="First name">
</div>
<div class="auth-wrapper">
<div class="auth-card">
<h1 class="auth-title">Create account</h1>
<p class="auth-subtitle">Fill in your details to get started.</p>
<div id="register-response-msg"></div>
<form role="form" id="frm-register">
<div class="auth-section-title">Personal Information</div>
<input type="hidden" name="redirect" value="{{ Request::get('redirectUrl') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label class="control-label">Last name</label>
<input type="text" class="form-control" name="lastname" placeholder="Last name">
</div>
<!-- <div class="form-group">
<label class="control-label">Username</label>
<input type="text" class="form-control" name="username" value="{{ old('username') }}" placeholder="Username">
</div> -->
<div class="form-group">
<label class="control-label">Email Address</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" placeholder="Email Address">
</div>
<div class="form-group">
<label class="control-label">Phone Number</label>
<input type="text" class="form-control" name="mobilenumber" placeholder="Phone Number">
</div>
<div class="form-group">
<label class="control-label">Password</label>
<input type="password" class="form-control" name="password" id="password" required placeholder="Password">
</div>
<div class="form-group">
<label class="control-label">Confirm Password</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Confirm Password" data-rule-equalTo="#password" required>
</div>
<div class="form-group text-center">
<h5>Address Information</h5>
</div>
<div class="form-group">
<label class="control-label">Select Country</label>
<select name="countryCode" id="select_country" class="form-control" onchange="selectCountry(this)">
<option value="">Select Country</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
</select>
</div>
<div class="form-group">
<label class="control-label">State / Province</label>
<label></label>
<select class="form-control" name="state" id="lst-states">
<option value="">Select State</option>
</select>
</div>
<div class="form-group">
<label class="control-label">City</label>
<select class="form-control" name="city" id="lst-cities">
<option value="">Select City</option>
</select>
</div>
<div class="form-group">
<label class="control-label">Address 1</label>
<input type="text" class="form-control" name="address" placeholder="Address 1">
</div>
<div class="form-group">
<label class="control-label">Address 2</label>
<input type="text" class="form-control" name="address2" placeholder="Address 2">
</div>
<div class="form-group">
<label class="control-label">Zip Code</label>
<input type="text" class="form-control" name="zipcode" placeholder="Please enter your zip code">
</div>
<div class="form-group">
<div class="g-recaptcha text-center" data-sitekey="{{ env('CAPTCHA_SITE_KEY') }}"></div>
</div>
<div class="form-group">
<p>By clicking Register, you agree to our <a href="#">Terms of Use</a> and that you have read our <a href="#">Privacy Policy</a>.</p>
</div>
<div class="form-group">
<button type="submit" id="btn-register" class="btn btn-primary btn-block">Register</button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<hr />
<p class="text-center">Have already an account?</p>
<a href="{{ url('/auth/login') }}" type="submit" id="btn-login" class="btn btn-success btn-block">Login here</a>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>First name</label>
<input type="text" class="form-control" name="firstname" placeholder="First name">
</div>
</div>
<div class="form-group">
<label>Last name</label>
<input type="text" class="form-control" name="lastname" placeholder="Last name">
</div>
<div class="form-group">
<label>Email Address</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" placeholder="you@example.com">
</div>
<div class="form-group">
<label>Phone Number</label>
<input type="text" class="form-control" name="mobilenumber" placeholder="Phone Number">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" name="password" id="password" required placeholder="Password">
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Confirm Password" data-rule-equalTo="#password" required>
</div>
<div class="auth-section-title">Address Information</div>
<div class="form-group">
<label>Select Country</label>
<select name="countryCode" id="select_country" class="form-control" onchange="selectCountry(this)">
<option value="">Select Country</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
</select>
</div>
<div class="form-group">
<label>State / Province</label>
<select class="form-control" name="state" id="lst-states">
<option value="">Select State</option>
</select>
</div>
<div class="form-group">
<label>City</label>
<select class="form-control" name="city" id="lst-cities">
<option value="">Select City</option>
</select>
</div>
<div class="form-group">
<label>Address 1</label>
<input type="text" class="form-control" name="address" placeholder="Address 1">
</div>
<div class="form-group">
<label>Address 2</label>
<input type="text" class="form-control" name="address2" placeholder="Address 2">
</div>
<div class="form-group">
<label>Zip Code</label>
<input type="text" class="form-control" name="zipcode" placeholder="Please enter your zip code">
</div>
<div class="form-group">
<div class="g-recaptcha text-center" data-sitekey="{{ env('CAPTCHA_SITE_KEY') }}"></div>
</div>
<p class="auth-terms">By clicking Register, you agree to our <a href="#">Terms of Use</a> and that you have read our <a href="#">Privacy Policy</a>.</p>
<div class="form-group">
<button type="submit" id="btn-register" class="btn-register-modern">Register</button>
</div>
</form>
<p class="auth-footer">Already have an account? <a href="{{ url('/auth/login') }}">Sign in</a></p>
</div>
</div>
@endsection

View File

@@ -3,60 +3,158 @@
@section('content')
<style>
.error{
color: red;
body {
background: #ffffff;
margin: 0;
}
.form-wrapper{
margin-top: 20%;
.navbar-custom {
margin-bottom: 0;
}
.auth-wrapper {
min-height: calc(100vh - 100px);
min-height: calc(100dvh - 100px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
box-sizing: border-box;
}
.auth-card {
width: 100%;
max-width: 380px;
background: #ffffff;
border: 1px solid #eceff3;
border-radius: 12px;
padding: 28px;
}
.auth-title {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
margin: 0 0 8px;
color: #111827;
}
.auth-subtitle {
font-size: 14px;
margin: 0 0 20px;
color: #6b7280;
}
.auth-card .form-group {
margin-bottom: 14px;
}
.auth-card label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #374151;
}
.auth-card .form-control {
height: 42px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: #ffffff;
font-size: 14px;
padding: 0 12px;
box-shadow: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.auth-card .form-control:focus {
border-color: #9ca3af;
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.2);
outline: none;
}
.btn-auth {
width: 100%;
height: 44px;
border: 0;
border-radius: 8px;
background: #111827;
color: #ffffff;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s;
}
.btn-auth:hover {
opacity: 0.92;
color: #ffffff;
}
.auth-footer {
margin-top: 16px;
text-align: center;
font-size: 13px;
color: #6b7280;
}
.auth-footer a {
color: #111827;
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
.auth-card .alert {
border-radius: 8px;
font-size: 13px;
}
.auth-card .alert ul {
padding-left: 18px;
margin-bottom: 0;
}
</style>
<div class="container">
<div class="col-md-4 col-md-offset-4">
<div class="form-wrapper">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="text-center">R E S E T &nbsp; P A S S W O R D</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form role="form" method="POST" action="{{ url('/password/reset') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label class="control-label">Email Address</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" title="Please enter your email address" placeholder="example@gmail.com">
<span class="help-block"></span>
</div>
<div class="form-group">
<label class="control-label">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password">
<span class="help-block"></span>
</div>
<div class="form-group">
<label class="control-label">Confirm Password</label>
<input type="password" class="form-control" name="password_confirmation" placeholder="Confirm Password">
<span class="help-block"></span>
</div>
<button type="submit" class="btn btn-default btn-block">Reset Password</button>
</form>
<br><br>
</div>
</div>
</div>
<div class="auth-wrapper">
<div class="auth-card">
<h1 class="auth-title">Reset password</h1>
<p class="auth-subtitle">Set a new password for your account.</p>
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
@endif
<form role="form" method="POST" action="{{ url('/password/reset') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" class="form-control" name="email" id="email" value="{{ old('email') }}" title="Please enter your email address" placeholder="you@example.com">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="New password">
</div>
<div class="form-group">
<label for="password_confirmation">Confirm Password</label>
<input type="password" class="form-control" name="password_confirmation" id="password_confirmation" placeholder="Confirm password">
</div>
<button type="submit" class="btn-auth">Reset password</button>
</form>
<p class="auth-footer"><a href="{{ url('/auth/login') }}">Back to sign in</a></p>
</div>
</div>
@endsection

View File

@@ -886,8 +886,8 @@
<script src="{{asset('/designer/js/custom-script.js')}}"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.min.js"></script>
<script src="https://rawgit.com/fabricjs/fabric.js/master/lib/centering_guidelines.js"></script>
<script src="https://rawgit.com/fabricjs/fabric.js/master/lib/aligning_guidelines.js"></script>
<script src="{{asset('/designer/js/centering_guidelines.js')}}"></script>
<script src="{{asset('/designer/js/aligning_guidelines.js')}}"></script>
<script>
$(document).ready(function() {

View File

@@ -21,24 +21,22 @@
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right navbar-nav-custom">
@if (Request::segment(2) == "hi-five-sports-club" || Request::segment(2) == "hi-five-sports-zone")
<li style="font-size: 14px;"></li>
<li class="nav-item-main"></li>
@else
<li style="font-size: 14px;">
<a href="{{ url('teamstore') }}"><span style="text-transform:uppercase;">Team Store</span></a>
<li class="nav-item-main">
<a href="{{ url('teamstore') }}" class="nav-link-main"><span>Team Store</span></a>
</li>
@endif
<li style="font-size: 14px;">
<a href="{{ url('cart') }}"><span style="text-transform:uppercase;">My Cart</span> <i class="fa fa-shopping-cart"></i>
<li class="nav-item-main">
<a href="{{ url('cart') }}" class="nav-link-main"><span>My Cart</span> <i class="fa fa-shopping-cart"></i>
<span class="badge" id="my-cart-count">{{ \App\Http\Controllers\MainController::getCountCart() }}</span>
</a>
</li>
<li>
<a href="#" class="dropdown-toggle user-profile" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-bars" style="font-size: 21px;"></i></a>
<ul class="dropdown-menu">
<a href="#" class="dropdown-toggle user-profile nav-menu-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-bars"></i></a>
<ul class="dropdown-menu navbar-menu-dropdown">
@if (Auth::guest())
@if(Request::segment(1) == "designer")
@@ -88,6 +86,155 @@
</nav>
<style>
.navbar-custom {
min-height: 78px;
background: #ffffff;
border: 0;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
margin-bottom: 18px;
}
.navbar-custom .container {
position: relative;
}
.navbar-brand {
height: 78px;
padding: 10px 0;
}
.navbar-brand>img {
height: 58px;
padding: 0;
width: auto;
}
.navbar-nav-custom > li > a.nav-link-main {
color: #1f2937 !important;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.45px;
border-radius: 8px;
padding: 8px 12px;
margin-top: 0;
transition: background-color 0.2s, color 0.2s;
}
.navbar-nav-custom > li > a.nav-link-main:hover,
.navbar-nav-custom > li > a.nav-link-main:focus {
color: #111827 !important;
background: #f3f4f6 !important;
}
.nav-menu-toggle {
margin-top: 0 !important;
border: 1px solid #d1d5db;
border-radius: 8px;
width: 42px;
height: 42px;
display: flex !important;
align-items: center;
justify-content: center;
color: #374151 !important;
transition: background-color 0.2s, border-color 0.2s;
}
.nav-menu-toggle:hover,
.nav-menu-toggle:focus {
background: #f3f4f6 !important;
border-color: #9ca3af;
}
.nav-menu-toggle i {
font-size: 18px;
}
.navbar-menu-dropdown {
margin-top: 8px;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
}
.navbar-menu-dropdown > li > a {
padding: 10px 14px;
font-size: 13px;
}
.navbar-menu-dropdown > li > a:hover {
background: #f8fafc;
color: #111827;
}
.navbar-menu-dropdown .dropdown-header {
font-size: 12px;
font-weight: 700;
color: #334155;
padding: 10px 14px;
}
.badge {
font-size: 11px;
min-width: 24px;
height: 24px;
line-height: 24px;
border-radius: 999px;
background: #16a34a;
padding: 0 7px;
margin-left: 6px;
vertical-align: middle;
}
.navbar-toggle {
margin-top: 22px;
border: 1px solid #d1d5db;
}
.navbar-toggle .icon-bar {
background: #374151 !important;
}
@media (min-width: 768px) {
.navbar-nav-custom {
min-height: 78px;
display: flex;
align-items: center;
margin: 0;
}
.navbar-nav-custom > li {
float: none;
}
}
@media (max-width: 768px){
.navbar-default .navbar-collapse,
.navbar-default .navbar-form {
margin-top: 12px;
border-top: 1px solid #e5e7eb;
background: #ffffff;
border-radius: 10px;
padding: 8px;
}
.navbar-nav-custom > li > a.nav-link-main {
margin-top: 0;
}
.nav-menu-toggle {
margin-top: 0 !important;
}
}
@media (min-width: 768px){
.navbar-default .navbar-nav>li>a {
margin-top: 0;
}
}
#nav{
list-style:none;
margin-bottom:10px;
@@ -143,41 +290,8 @@
background:#3d4248;
}
.navbar-brand {
padding: 0px; /* firefox bug fix */
}
.navbar-brand>img {
height: 200%;
padding: 15px; /* firefox bug fix */
width: auto;
}
.navbar-custom{
min-height: 100px;
}
@media (max-width: 768px){
.navbar-default .navbar-collapse, .navbar-default .navbar-form{
margin-top: 50px;
}
}
@media (min-width: 768px){
.navbar-default .navbar-nav>li>a{
margin-top: 23px;
}
}
.badge{
font-size: 14px;
width:35px;
border-radius: 0px;
}
.navbar-default .navbar-nav>li>a:hover {
background-color: #777;
background-color: #f3f4f6;
}
.modal {
@@ -198,6 +312,158 @@
text-align: left;
vertical-align: middle;
}
#about_us_modal .modal-content {
border: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);
}
#about_us_modal .modal-header {
background: #f8fafc;
border-bottom: 1px solid #e5e7eb;
padding: 16px 20px;
}
#about_us_modal .modal-title {
font-size: 20px;
font-weight: 700;
color: #111827;
}
.about-us-content {
color: #334155;
font-size: 14px;
line-height: 1.7;
}
.about-us-intro {
margin: 0 0 14px;
color: #475569;
}
.about-us-block {
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
background: #ffffff;
}
.about-us-block h5 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 700;
color: #111827;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.about-us-block p {
margin: 0;
}
.about-us-list {
margin: 0;
padding-left: 18px;
}
.about-us-list li {
margin-bottom: 6px;
}
#about_us_modal .modal-footer {
border-top: 1px solid #e5e7eb;
background: #f8fafc;
padding: 12px 20px;
}
@media (max-width: 767px) {
.about-us-block {
padding: 12px;
}
}
#contact_us_modal .modal-content {
border: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);
}
#contact_us_modal .modal-header {
background: #f8fafc;
border-bottom: 1px solid #e5e7eb;
padding: 16px 20px;
}
#contact_us_modal .modal-title {
font-size: 20px;
font-weight: 700;
color: #111827;
}
#contact_us_modal .modal-body {
padding: 20px;
}
.contact-modal-subtitle {
margin: 0 0 16px;
font-size: 14px;
color: #6b7280;
}
.contact-card {
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 14px;
background: #fff;
min-height: 140px;
}
.contact-card-title {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #6b7280;
}
.contact-card-value {
margin: 0;
font-size: 15px;
line-height: 1.6;
color: #111827;
word-break: break-word;
}
.contact-card-value a {
color: #1d4ed8;
text-decoration: none;
}
.contact-card-value a:hover {
text-decoration: underline;
}
#contact_us_modal .modal-footer {
border-top: 1px solid #e5e7eb;
background: #f8fafc;
padding: 12px 20px;
}
@media (max-width: 767px) {
#contact_us_modal .modal-body {
padding: 16px;
}
.contact-card {
min-height: 0;
margin-bottom: 12px;
}
}
</style>
<!-- Privacy Modal -->
@@ -248,7 +514,31 @@
<h4 class="modal-title">About Us</h4>
</div>
<div class="modal-body">
<p>Coming soon.</p>
<div class="about-us-content">
<p class="about-us-intro">Crew Sportswear helps teams, schools, and organizations bring their identity to life through high-quality custom apparel and team store experiences.</p>
<div class="about-us-block">
<h5>Who We Are</h5>
<p>We are a team focused on custom uniforms, spirit wear, and branded gear built for athletes, students, and supporters. Our goal is to make ordering team apparel simple, consistent, and reliable.</p>
</div>
<div class="about-us-block">
<h5>What We Do</h5>
<ul class="about-us-list">
<li>Custom team uniforms and apparel across multiple sports</li>
<li>Print-on-demand production for flexible ordering and fulfillment</li>
<li>Design-your-own apparel with our online designer tool</li>
<li>Private or public online team stores for easy ordering</li>
<li>Batch and individual order workflows depending on program needs</li>
<li>Support for team branding, sizing, and product selection</li>
</ul>
</div>
<div class="about-us-block">
<h5>Our Commitment</h5>
<p>Were committed to clear communication, dependable production quality, and a smooth customer experience from store launch through fulfillment.</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@@ -266,15 +556,28 @@
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Contact Us</h4>
</div>
<div class="modal-body text-center">
<h1>Contact Us</h1>
<hr>
<h3>Email Address: <small><a href="#">customer-service@crewsportswear.com</a></small></h3>
<h3>Address: <small><a href="#">1281 Humbracht Circle
Suite J
Bartlett, Illinois
60103</a></small>
</h3>
<div class="modal-body">
<p class="contact-modal-subtitle">Were here to help. Reach out through any of the contact options below.</p>
<div class="row">
<div class="col-sm-6">
<div class="contact-card">
<h5 class="contact-card-title">Email Address</h5>
<p class="contact-card-value">
<a href="mailto:customer-service@crewsportswear.com">customer-service@crewsportswear.com</a>
</p>
</div>
</div>
<div class="col-sm-6">
<div class="contact-card">
<h5 class="contact-card-title">Address</h5>
<p class="contact-card-value">
1281 Humbracht Circle<br>
Suite J<br>
Bartlett, Illinois 60103
</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View File

@@ -1,96 +1,171 @@
<div>
<div>Privacy Policy</div>
<br />
<div>Your privacy is important to us. It is Crew Sportswear's policy to respect your privacy regarding any information we may collect from you across our website, http://crewsportswear.com, and other sites we own and operate.</div>
<br />
<div>1. Information we collect</div>
<br />
<div>Log data</div>
<br />
<div>When you visit our website, our servers may automatically log the standard data provided by your web browser. It may include your computer&rsquo;s Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details.</div>
<br /><br />
<div>Personal information</div>
<br />
<div>We may ask for personal information, such as your:</div>
<br /><br />
<div>NameEmailSocial media profilesDate of birthPhone/mobile numberHome/Mailing addressWork addressPayment information</div>
<br />
<div>2. Legal bases for processing</div>
<br />
<div>We will process your personal information lawfully, fairly and in a transparent manner. We collect and process information about you only where we have legal bases for doing so.</div>
<div>&nbsp;</div>
<div>These legal bases depend on the services you use and how you use them, meaning we collect and use your information only where:</div>
<div>&nbsp;</div>
<br />
<div>&nbsp;&nbsp;&nbsp;&nbsp;it&rsquo;s necessary for the performance of a contract to which you are a party or to take steps at your request before entering into such a contract (for example, when we provide a service you request from us);</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;it satisfies a legitimate interest (which is not overridden by your data protection interests), such as for research and development, to market and promote our services, and to protect our legal rights and interests;</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;you give us consent to do so for a specific purpose (for example, you might consent to us sending you our newsletter); or</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;we need to process your data to comply with a legal obligation.</div>
<br />
<div>&nbsp;</div>
<div>Where you consent to our use of information about you for a specific purpose, you have the right to change your mind at any time (but this will not affect any processing that has already taken place).</div>
<div>&nbsp;</div>
<div>We don&rsquo;t keep personal information for longer than is necessary. While we retain this information, we will protect it within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification. That said, we advise that no method of electronic transmission or storage is 100% secure and cannot guarantee absolute data security. If necessary, we may retain your personal information for our compliance with a legal obligation or in order to protect your vital interests or the vital interests of another natural person.</div>
<br />
<div>3. Collection and use of information</div>
<br />
<div>We may collect, hold, use and disclose information for the following purposes and personal information will not be further processed in a manner that is incompatible with these purposes:</div>
<br /><br />
<div>to enable you to customise or personalise your experience of our website;to enable you to access and use our website, associated applications and associated social media platforms;to contact and communicate with you;for internal record keeping and administrative purposes;for analytics, market research and business development, including to operate and improve our website, associated applications and associated social media platforms;to run competitions and/or offer additional benefits to you;for advertising and marketing, including to send you promotional information about our products and services and information about third parties that we consider may be of interest to you; andto comply with our legal obligations and resolve any disputes that we may have.</div>
<br />
<div>4. Disclosure of personal information to third parties</div>
<br />
<div>We may disclose personal information to:</div>
<br /><br />
<div>third party service providers for the purpose of enabling them to provide their services, including (without limitation) IT service providers, data storage, hosting and server providers, ad networks, analytics, error loggers, debt collectors, maintenance or problem-solving providers, marketing or advertising providers, professional advisors and payment systems operators;our employees, contractors and/or related entities; andcredit reporting agencies, courts, tribunals and regulatory authorities, in the event you fail to pay for goods or services we have provided to you.</div>
<br />
<div>5. International transfers of personal information</div>
<br />
<div>The personal information we collect is stored and processed in United States, or where we or our partners, affiliates and third-party providers maintain facilities. By providing us with your personal information, you consent to the disclosure to these overseas third parties.</div>
<br />
<div>We will ensure that any transfer of personal information from countries in the European Economic Area (EEA) to countries outside the EEA will be protected by appropriate safeguards, for example by using standard data protection clauses approved by the European Commission, or the use of binding corporate rules or other legally accepted means.</div>
<br />
<div>Where we transfer personal information from a non-EEA country to another country, you acknowledge that third parties in other jurisdictions may not be subject to similar data protection laws to the ones in our jurisdiction. There are risks if any such third party engages in any act or practice that would contravene the data privacy laws in our jurisdiction and this might mean that you will not be able to seek redress under our jurisdiction&rsquo;s privacy laws.</div>
<br />
<div>6. Your rights and controlling your personal information</div>
<br />
<div>Choice and consent: By providing personal information to us, you consent to us collecting, holding, using and disclosing your personal information in accordance with this privacy policy. If you are under 16 years of age, you must have, and warrant to the extent permitted by law to us, that you have your parent or legal guardian&rsquo;s permission to access and use the website and they (your parents or guardian) have consented to you providing us with your personal information. You do not have to provide personal information to us, however, if you do not, it may affect your use of this website or the products and/or services offered on or through it.</div>
<br />
<div>Information from third parties: If we receive personal information about you from a third party, we will protect it as set out in this privacy policy. If you are a third party providing personal information about somebody else, you represent and warrant that you have such person&rsquo;s consent to provide the personal information to us.</div>
<br />
<div>Restrict: You may choose to restrict the collection or use of your personal information. If you have previously agreed to us using your personal information for direct marketing purposes, you may change your mind at any time by contacting us using the details below. If you ask us to restrict or limit how we process your personal information, we will let you know how the restriction affects your use of our website or products and services.</div>
<br />
<div>Access and data portability: You may request details of the personal information that we hold about you. You may request a copy of the personal information we hold about you. Where possible, we will provide this information in CSV format or other easily readable machine format. You may request that we erase the personal information we hold about you at any time. You may also request that we transfer this personal information to another third party.</div>
<br />
<div>Correction: If you believe that any information we hold about you is inaccurate, out of date, incomplete, irrelevant or misleading, please contact us using the details below. We will take reasonable steps to correct any information found to be inaccurate, incomplete, misleading or out of date.</div>
<br />
<div>Notification of data breaches: We will comply laws applicable to us in respect of any data breach.</div>
<br />
<div>Complaints: If you believe that we have breached a relevant data protection law and wish to make a complaint, please contact us using the details below and provide us with full details of the alleged breach. We will promptly investigate your complaint and respond to you, in writing, setting out the outcome of our investigation and the steps we will take to deal with your complaint. You also have the right to contact a regulatory body or data protection authority in relation to your complaint.</div>
<br />
<div>Unsubscribe: To unsubscribe from our e-mail database or opt-out of communications (including marketing communications), please contact us using the details below or opt-out using the opt-out facilities provided in the communication.</div>
<br />
<div>7. Cookies</div>
<br />
<div>We use &ldquo;cookies&rdquo; to collect information about you and your activity across our site. A cookie is a small piece of data that our website stores on your computer, and accesses each time you visit, so we can understand how you use our site. This helps us serve you content based on preferences you have specified. Please refer to our Cookie Policy for more information.</div>
<br />
<div>8. Business transfers</div>
<br />
<div>If we or our assets are acquired, or in the unlikely event that we go out of business or enter bankruptcy, we would include data among the assets transferred to any parties who acquire us. You acknowledge that such transfers may occur, and that any parties who acquire us may continue to use your personal information according to this policy.</div>
<br />
<div>9. Limits of our policy</div>
<br />
<div>Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and policies of those sites, and cannot accept responsibility or liability for their respective privacy practices.</div>
<br />
<div>10. Changes to this policy</div>
<br />
<div>At our discretion, we may change our privacy policy to reflect current acceptable practices. We will take reasonable steps to let users know about changes via our website. Your continued use of this site after any changes to this policy will be regarded as acceptance of our practices around privacy and personal information.</div>
<br />
<div>If we make a significant change to this privacy policy, for example changing a lawful basis on which we process your personal information, we will ask you to re-consent to the amended privacy policy.</div>
<br /><br />
<div>Crew Sportswear Data Controller</div>
<div>Angelo Garcia</div>
<div>angelo@crewsportswear.com</div>
<br /><br /><br />
<div>This policy is effective as of 23 February 2019.</div>
<style>
.privacy-policy {
color: #334155;
font-size: 14px;
line-height: 1.7;
}
.privacy-title {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
color: #111827;
}
.privacy-intro {
margin: 0 0 18px;
color: #64748b;
}
.privacy-section {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 12px;
}
.privacy-section h5 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
color: #111827;
}
.privacy-section p {
margin: 0 0 10px;
}
.privacy-section p:last-child {
margin-bottom: 0;
}
.privacy-section ul {
margin: 0 0 10px 18px;
padding: 0;
}
.privacy-section li {
margin-bottom: 5px;
}
.privacy-footnote {
margin-top: 12px;
border-top: 1px solid #e5e7eb;
padding-top: 12px;
font-size: 13px;
color: #6b7280;
}
.privacy-contact {
background: #f8fafc;
}
</style>
<div class="privacy-policy">
<h3 class="privacy-title">Privacy Policy</h3>
<p class="privacy-intro">Your privacy is important to us. It is Crew Sportswear's policy to respect your privacy regarding any information we may collect from you across our website, http://crewsportswear.com, and other sites we own and operate.</p>
<div class="privacy-section">
<h5>1. Information we collect</h5>
<p><strong>Log data:</strong> When you visit our website, our servers may automatically log the standard data provided by your web browser. It may include your computers Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details.</p>
<p><strong>Personal information:</strong> We may ask for personal information, such as your:</p>
<ul>
<li>Name</li>
<li>Email</li>
<li>Social media profiles</li>
<li>Date of birth</li>
<li>Phone/mobile number</li>
<li>Home/Mailing address</li>
<li>Work address</li>
<li>Payment information</li>
</ul>
</div>
<div class="privacy-section">
<h5>2. Legal bases for processing</h5>
<p>We will process your personal information lawfully, fairly and in a transparent manner. We collect and process information about you only where we have legal bases for doing so.</p>
<p>These legal bases depend on the services you use and how you use them, meaning we collect and use your information only where:</p>
<ul>
<li>its necessary for the performance of a contract to which you are a party or to take steps at your request before entering into such a contract;</li>
<li>it satisfies a legitimate interest (which is not overridden by your data protection interests), such as research and development, marketing and legal protection;</li>
<li>you give us consent to do so for a specific purpose; or</li>
<li>we need to process your data to comply with a legal obligation.</li>
</ul>
<p>Where you consent to our use of information about you for a specific purpose, you have the right to change your mind at any time (but this will not affect any processing that has already taken place).</p>
<p>We dont keep personal information for longer than is necessary. While we retain this information, we will protect it within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification. That said, we advise that no method of electronic transmission or storage is 100% secure and cannot guarantee absolute data security.</p>
</div>
<div class="privacy-section">
<h5>3. Collection and use of information</h5>
<p>We may collect, hold, use and disclose information for the following purposes and personal information will not be further processed in a manner that is incompatible with these purposes:</p>
<ul>
<li>to enable you to customise or personalise your experience of our website;</li>
<li>to enable you to access and use our website, associated applications and associated social media platforms;</li>
<li>to contact and communicate with you;</li>
<li>for internal record keeping and administrative purposes;</li>
<li>for analytics, market research and business development;</li>
<li>to run competitions and/or offer additional benefits;</li>
<li>for advertising and marketing communications; and</li>
<li>to comply with legal obligations and resolve disputes.</li>
</ul>
</div>
<div class="privacy-section">
<h5>4. Disclosure of personal information to third parties</h5>
<p>We may disclose personal information to:</p>
<ul>
<li>third party service providers (including IT, hosting, analytics, marketing, advisors, and payment providers);</li>
<li>our employees, contractors and/or related entities; and</li>
<li>credit reporting agencies, courts, tribunals and regulatory authorities where required.</li>
</ul>
</div>
<div class="privacy-section">
<h5>5. International transfers of personal information</h5>
<p>The personal information we collect is stored and processed in United States, or where we or our partners, affiliates and third-party providers maintain facilities. By providing us with your personal information, you consent to disclosure to these overseas third parties.</p>
<p>We will ensure that any transfer of personal information from countries in the European Economic Area (EEA) to countries outside the EEA is protected by appropriate safeguards.</p>
<p>Where we transfer personal information from a non-EEA country to another country, you acknowledge that third parties in other jurisdictions may not be subject to similar data protection laws.</p>
</div>
<div class="privacy-section">
<h5>6. Your rights and controlling your personal information</h5>
<p><strong>Choice and consent:</strong> By providing personal information to us, you consent to us collecting, holding, using and disclosing your personal information in accordance with this privacy policy.</p>
<p><strong>Information from third parties:</strong> If we receive personal information about you from a third party, we will protect it as set out in this privacy policy.</p>
<p><strong>Restrict:</strong> You may choose to restrict the collection or use of your personal information.</p>
<p><strong>Access and data portability:</strong> You may request details or a copy of the personal information we hold about you, and request transfer to another third party where possible.</p>
<p><strong>Correction:</strong> If you believe any information we hold is inaccurate or out of date, please contact us so we can correct it.</p>
<p><strong>Notification of data breaches:</strong> We will comply with laws applicable to us in respect of any data breach.</p>
<p><strong>Complaints:</strong> If you believe we have breached a relevant data protection law, please contact us with full details and we will promptly investigate.</p>
<p><strong>Unsubscribe:</strong> To unsubscribe from communications, contact us using the details below or use opt-out facilities provided in communications.</p>
</div>
<div class="privacy-section">
<h5>7. Cookies</h5>
<p>We use “cookies” to collect information about you and your activity across our site. A cookie is a small piece of data that our website stores on your computer and accesses each time you visit. Please refer to our Cookie Policy for more information.</p>
</div>
<div class="privacy-section">
<h5>8. Business transfers</h5>
<p>If we or our assets are acquired, or if we go out of business or enter bankruptcy, data may be transferred to parties who acquire us. Any acquiring parties may continue to use your personal information according to this policy.</p>
</div>
<div class="privacy-section">
<h5>9. Limits of our policy</h5>
<p>Our website may link to external sites that are not operated by us. We have no control over the content and policies of those sites and cannot accept responsibility or liability for their respective privacy practices.</p>
</div>
<div class="privacy-section">
<h5>10. Changes to this policy</h5>
<p>At our discretion, we may change our privacy policy to reflect current acceptable practices. We will take reasonable steps to let users know about changes via our website. Your continued use of this site after changes to this policy will be regarded as acceptance of our practices around privacy and personal information.</p>
<p>If we make a significant change to this privacy policy, such as changing a lawful basis on which we process your personal information, we will ask you to re-consent to the amended privacy policy.</p>
</div>
<div class="privacy-section privacy-contact">
<h5>Crew Sportswear Data Controller</h5>
<p>Angelo Garcia</p>
<p><a href="mailto:angelo@crewsportswear.com">angelo@crewsportswear.com</a></p>
</div>
<p class="privacy-footnote">This policy is effective as of 23 February 2019.</p>
</div>

View File

@@ -7,14 +7,17 @@
@endif
<style>
body {
background: #f4f6f8;
}
h2 {
width: 100%;
margin: 0;
padding: 0;
text-align: center;
margin-top: 40px;
margin-bottom: 40px;
margin-top: 34px;
margin-bottom: 24px;
}
/* h2:after {
display: inline-block;
@@ -41,6 +44,15 @@
font-size: 12px;
margin-top: 5px;
}
.featured-title {
font-size: 24px;
font-weight: 700;
letter-spacing: 0.6px;
color: #111827;
text-transform: uppercase;
}
.price{
font-size: 25px;
margin: 0 auto;
@@ -51,19 +63,107 @@
border-bottom: 2px solid #4B8E4B;
}
.thumbnail{
/* opacity:0.70; */
-webkit-transition: all 0.5s;
transition: all 0.5s;
}
.thumbnail:hover{
opacity:1.00;
box-shadow: 0px 0px 10px #4bc6ff;
}
.line{
margin-bottom: 5px;
}
.thumbnail>img{
height:201.84px;
.products-grid {
margin-top: 6px;
}
.product-col {
margin-bottom: 22px;
}
.product-card {
height: 100%;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
border-color: #d1d5db;
box-shadow: 0 10px 24px rgba(2, 6, 23, 0.08);
}
.product-image-link {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
min-height: 240px;
background: #f8fafc;
border-bottom: 1px solid #e5e7eb;
}
.product-image {
width: auto;
max-width: 100%;
height: auto;
max-height: 202px;
object-fit: contain;
}
.product-card-body {
padding: 12px 14px 14px;
}
.product-name-holder {
margin: 0;
font-size: 14px;
font-weight: 700;
line-height: 1.35;
color: #111827;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eef2f7;
}
.price {
margin: 0;
font-size: 22px;
line-height: 1;
font-weight: 700;
color: #111827;
}
.price small,
.price-currency {
font-size: 13px;
color: #6b7280;
font-weight: 600;
}
.btn-view-details {
border-radius: 8px;
min-height: 36px;
font-weight: 600;
font-size: 12px;
padding: 8px 12px;
letter-spacing: 0.2px;
}
.empty-state {
margin: 50px 0;
color: #6b7280;
font-size: 15px;
}
@media screen and (max-width: 770px) {
.right{
@@ -127,6 +227,164 @@
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Category nav ──────────────────────────────────────────────────────── */
.cat-nav {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
padding: 0;
margin-bottom: 24px;
overflow: visible;
}
.cat-nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
.cat-nav > ul > li {
position: relative;
}
.cat-nav > ul > li > a {
display: block;
padding: 14px 20px;
font-weight: 700;
font-size: 13px;
text-transform: uppercase;
color: #222;
text-decoration: none;
letter-spacing: .5px;
border-bottom: 3px solid transparent;
transition: border-color .2s, color .2s;
}
.cat-nav > ul > li > a:hover,
.cat-nav > ul > li > a.active {
color: #4B8E4B;
border-bottom-color: #4B8E4B;
}
/* sub-category dropdown */
.cat-nav .sub-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: #fff;
border: 1px solid #ddd;
border-top: 3px solid #4B8E4B;
box-shadow: 0 4px 12px rgba(0,0,0,.12);
z-index: 999;
list-style: none;
margin: 0;
padding: 6px 0;
}
.cat-nav > ul > li:hover .sub-menu {
display: block;
}
.cat-nav .sub-menu li a {
display: block;
padding: 9px 18px;
font-size: 13px;
color: #333;
text-decoration: none;
white-space: nowrap;
}
.cat-nav .sub-menu li a:hover,
.cat-nav .sub-menu li a.active {
background: #f0f9f0;
color: #4B8E4B;
}
.cat-count {
display: inline-block;
background: #ddd;
border-radius: 10px;
font-size: 11px;
padding: 1px 7px;
margin-left: 4px;
vertical-align: middle;
}
/* ── League / conference pill filter (sub-sub-category) ─────────────────── */
.league-filter {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px 12px 14px;
margin-bottom: 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fff;
align-items: center;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.league-filter .lf-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
color: #888;
letter-spacing: .5px;
margin-right: 4px;
white-space: nowrap;
}
.league-filter a {
display: inline-block;
padding: 5px 14px;
border-radius: 20px;
background: #f0f0f0;
color: #333;
font-size: 12px;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
border: 1px solid #ddd;
transition: background .2s, color .2s, border-color .2s;
}
.league-filter a:hover {
background: #e0f2e0;
color: #4B8E4B;
border-color: #4B8E4B;
}
.league-filter a.active {
background: #4B8E4B;
color: #fff;
border-color: #4B8E4B;
}
@media screen and (max-width: 991px) {
.product-image-link {
min-height: 220px;
}
.btn-view-details {
padding: 7px 10px;
}
}
@media screen and (max-width: 767px) {
h2 {
margin-top: 24px;
margin-bottom: 16px;
}
.featured-title {
font-size: 20px;
}
.product-card-footer {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.btn-view-details {
width: 100%;
}
}
[v-cloak] { display: none; }
</style>
<div class="container">
@@ -147,83 +405,277 @@
</div>
<div class="row">
<div class="col-md-12">
<h2>FEATURED PRODUCTS</h2>
<h2 class="featured-title">Featured Products</h2>
</div>
</div>
<div class="row">
{{-- ── Vue category + product grid ─────────────────────────────────────── --}}
<script>
window._tsProducts = {!! json_encode(
collect($product_array)
->where('PrivacyStatus', 'public')
->map(function($p) use ($thumbnails) {
$thumbList = isset($thumbnails) ? $thumbnails : [];
$thumb = collect($thumbList)->filter(function($t) use ($p) {
return $t['product_id'] == $p->Id;
})->first();
return [
'id' => $p->Id,
'name' => $p->ProductName,
'price' => $p->ProductPrice,
'url' => $p->ProductURL,
'img' => $thumb ? $thumb['thumb'] : 'product-image-placeholder.png',
'folder'=> $thumb ? $thumb['folder'] : '',
];
})->values()->toArray(),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
) !!};
window._tsStore = {
url : {!! json_encode($store_array[0]->StoreUrl, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) !!},
currency : {!! json_encode($store_array[0]->StoreCurrency, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) !!},
minoBase : {!! json_encode(rtrim(config('filesystems.disks.minio.url', ''), '/') . '/' . env('MINIO_BUCKET', 'crewsportswear') . '/', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) !!},
};
</script>
<div id="ts-app" v-cloak>
{{-- ── Category nav ── --}}
<nav class="cat-nav" v-if="categories.length > 0">
<ul>
<li>
<a href="#" :class="{active: activeCategory===null && activeSubCategory===null}"
@click.prevent="activeCategory=null; activeSubCategory=null">
All
<span class="cat-count">@{{ totalPublic }}</span>
</a>
</li>
<li v-for="cat in categories" :key="cat.name">
<a href="#"
:class="{active: activeCategory===cat.name}"
@click.prevent="selectCategory(cat.name)">
@{{ cat.name }}
<span class="cat-count">@{{ cat.count }}</span>
</a>
<ul class="sub-menu" v-if="cat.subs.length > 0">
<li v-for="sub in cat.subs" :key="sub.name">
<a href="#"
:class="{active: activeCategory===cat.name && activeSubCategory===sub.name}"
@click.prevent="selectSub(cat.name, sub.name)">
@{{ sub.name }}
<span class="cat-count">@{{ sub.count }}</span>
</a>
</li>
</ul>
</li>
</ul>
</nav>
{{-- ── League / conference pill filter ── --}}
<div class="league-filter" v-if="isLeagueStore && activeCategory !== null && leagues.length > 0">
<span class="lf-label">League / Conf:</span>
<a href="#"
:class="{active: activeLeague === null}"
@click.prevent="activeLeague = null">All</a>
<a href="#"
v-for="lg in leagues" :key="lg.name"
:class="{active: activeLeague === lg.name}"
@click.prevent="activeLeague = lg.name">
@{{ lg.name }}
<span class="cat-count">@{{ lg.count }}</span>
</a>
</div>
{{-- ── Announcements (kept outside loop) ── --}}
@if ($announcement->IsActive)
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
<p><b>Shop Announcements:</b></p>
{!! nl2br(e($announcement->Announcement)) !!}
</div>
</div>
</div>
@endif
@if($store_array[0]->Id == 174 || $store_array[0]->Id == 175 || $store_array[0]->Id == 178 || $store_array[0]->Id == 179 || $store_array[0]->Id == 177 || $store_array[0]->Id == 189 || $store_array[0]->Id == 176 || $store_array[0]->Id == 190 || $store_array[0]->Id == 191 || $store_array[0]->Id == 192 || $store_array[0]->Id == 194)
<div class="col-md-12">
<div class="alert alert-warning">
<p><b>Please read:</b></p>
1. All orders will be batch shipped to your school for pick up.<br>
2. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.<br>
3. Masks and gaiters sold on Crew are not intended for medical use. Crew does not make any medical or health claims.<br>
4. Refunds and exchanges are not allowed due to the hygenic nature of the product.<br>
@if($store_array[0]->Id == 175)
5. $1 from every item sold will benefit the 2020-2021 Maine South Schoolwide Fundraiser.
@endif
</div>
</div>
@else
<div class="col-md-12">
<div class="alert alert-warning">
@if($store_array[0]->Id == 174 || $store_array[0]->Id == 175 || $store_array[0]->Id == 178 || $store_array[0]->Id == 179 || $store_array[0]->Id == 177 || $store_array[0]->Id == 189 || $store_array[0]->Id == 176 || $store_array[0]->Id == 190 || $store_array[0]->Id == 191 || $store_array[0]->Id == 192 || $store_array[0]->Id == 194)
<div class="row">
<div class="col-md-12">
<div class="alert alert-warning">
<p><b>Please read:</b></p>
1. Items purchased are made on demand. Orders will be shipped based on dates set by your store administrator.<br>
2. Store payments are processed through PayPal. A PayPal account is not required to make a purchase.<br>
@if($store_array[0]->Id == 222)
3. Orders will be batch processed on a monthly basis, please allow 4-6 weeks for delivery.<br>
@else
3. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.<br>
1. All orders will be batch shipped to your school for pick up.<br>
2. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.<br>
3. Masks and gaiters sold on Crew are not intended for medical use. Crew does not make any medical or health claims.<br>
4. Refunds and exchanges are not allowed due to the hygenic nature of the product.<br>
@if($store_array[0]->Id == 175)
5. $1 from every item sold will benefit the 2020-2021 Maine South Schoolwide Fundraiser.
@endif
4. We are currently only shipping to US locations. For international orders, please contact <b>orders@crewsportswear.com</b> if you'd like to place an order.<br>
5. Expect shipping delays due to COVID-19.<br>
6. All sales are final. No returns or exchanges will be accepted.<br><br>
<b>DISCLAIMER:</b> Masks and gaiters sold by Crew Sportswear are not intended for medical use. Crew Sportswear does not make any medical or health claims.
</div>
</div>
@endif
<!-- BEGIN PRODUCTS -->
@foreach($product_array as $i => $product)
@if($product->PrivacyStatus == "public")
@foreach($thumbnails as $t => $thumb)
@if($thumb['product_id'] == $product->Id)
@define $storeFolder = $thumb['folder']
@define $filename = $thumb['thumb']
@endif
@endforeach
<div class="col-md-3 col-sm-6">
<span class="thumbnail">
<a href="{{ url('teamstore') }}/{{ $store_array[0]->StoreUrl }}/product/{{ $product->ProductURL }}">
<img style="height: 201.84px;" src="{{ minio_url('images/' . $filename) }}" alt="{{ $product->ProductName }}" >
</a>
<h4 class="text-center product-name-holder">{{ $product->ProductName }}</h4>
<hr class="line">
<div class="row">
<div class="col-md-7 col-sm-7">
<p class="price">{{ $product->ProductPrice }} <small style="font-size: 15px;"> {{ $store_array[0]->StoreCurrency }}</small></p>
</div>
<div class="col-md-5 col-sm-5">
<a href="{{ url('teamstore') }}/{{ $store_array[0]->StoreUrl }}/product/{{ $product->ProductURL }}" class="btn btn-success right" > View Details</a>
</div>
</div>
</span>
</div>
@endif
@endforeach
<!-- END PRODUCTS -->
</div>
</div>
@endif
</div>
</div> <!-- cotainer -->
{{-- ── Product grid ── --}}
<div class="row products-grid">
<div class="col-md-12 text-center" v-if="filtered.length === 0">
<p class="empty-state">No products found in this category.</p>
</div>
<div class="col-md-3 col-sm-6 product-col" v-for="p in filtered" :key="p.id">
<div class="product-card">
<a :href="productUrl(p)" class="product-image-link">
<img class="product-image" :src="imgUrl(p)" :alt="p.name">
</a>
<div class="product-card-body">
<h4 class="text-center product-name-holder">@{{ p.name }}</h4>
<div class="product-card-footer">
<p class="price">@{{ p.price }} <small class="price-currency">@{{ store.currency }}</small></p>
<a :href="productUrl(p)" class="btn btn-success btn-view-details">View Details</a>
</div>
</div>
</div>
</div>
</div>
</div>{{-- /ts-app --}}
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
(function () {
// ── sport & item-type keyword maps ──────────────────────────────────
const SPORTS = [
'Basketball','Football','Soccer','Baseball','Softball',
'Volleyball','Hockey','Lacrosse','Wrestling','Tennis',
'Swimming','Track','Cross Country','Golf','Cheerleading',
'Dance','Rugby','Bowling','Gymnastics','Cycling',
];
const ITEMS = [
'Jersey','T-Shirt','Tee','Hoodie','Sweatshirt','Jacket',
'Shorts','Pants','Tank','Top','Pullover','Zip-Up',
'Hat','Cap','Beanie','Polo','Uniform','Warmup','Pinnie',
];
// ── league / conference keyword map ──────────────────────────────────
// Order matters: more specific phrases first
const LEAGUES = [
{ key: 'WNBA', terms: ['\\bwnba\\b'] },
{ key: 'NBA', terms: ['\\bnba\\b'] },
{ key: 'NFL', terms: ['\\bnfl\\b'] },
{ key: 'MLB', terms: ['\\bmlb\\b'] },
{ key: 'NHL', terms: ['\\bnhl\\b'] },
{ key: 'MLS', terms: ['\\bmls\\b'] },
{ key: 'Big Ten', terms: ['\\bbig\\s*ten\\b','\\bbig\\s*10\\b','\\bbig10\\b'] },
{ key: 'ACC', terms: ['\\bacc\\b'] },
{ key: 'SEC', terms: ['\\bsec\\b'] },
{ key: 'Big 12', terms: ['\\bbig\\s*12\\b','\\bbig12\\b'] },
{ key: 'Pac-12', terms: ['\\bpac[-\\s]*12\\b','\\bpac12\\b'] },
{ key: 'AAC', terms: ['\\baac\\b'] },
{ key: 'CUSA', terms: ['\\bcusa\\b','\\bc-usa\\b'] },
{ key: 'MAC', terms: ['\\bmac\\b'] },
{ key: 'Sun Belt',terms: ['\\bsun\\s+belt\\b'] },
{ key: 'Mountain West', terms: ['\\bmountain\\s+west\\b','\\bmwc\\b'] },
];
function classify(name) {
const n = name.toLowerCase();
const sport = SPORTS.find(s => n.includes(s.toLowerCase())) || 'Other';
const item = ITEMS.find(i => n.includes(i.toLowerCase())) || 'Other';
let league = null;
for (const lg of LEAGUES) {
if (lg.terms.some(t => {
try { return new RegExp(t, 'i').test(n); } catch(e) { return n.includes(t); }
})) {
league = lg.key;
break;
}
}
return { sport, item, league };
}
const { createApp } = Vue;
const store = window._tsStore || { url:'', currency:'', minoBase:'' };
const allProducts = Array.isArray(window._tsProducts) ? window._tsProducts : [];
createApp({
data() {
return {
products : allProducts,
store : store,
activeCategory : null,
activeSubCategory: null,
activeLeague : null,
};
},
computed: {
totalPublic() {
return this.products.length;
},
isLeagueStore() {
return this.store.url === 'hi-five-franchise-store';
},
// Build [ { name:'Basketball', count:N, subs:[{name:'Jersey',count:N},...] }, ... ]
categories() {
const map = {};
this.products.forEach(p => {
const { sport, item } = classify(p.name);
if (!map[sport]) map[sport] = {};
map[sport][item] = (map[sport][item] || 0) + 1;
});
return Object.entries(map)
.sort((a,b) => a[0].localeCompare(b[0]))
.map(([name, subs]) => ({
name,
count: Object.values(subs).reduce((a,b) => a+b, 0),
subs: Object.entries(subs)
.sort((a,b) => a[0].localeCompare(b[0]))
.map(([s, count]) => ({ name: s, count }))
.filter(s => s.name !== 'Other' || Object.keys(subs).length === 1),
}));
},
// Available leagues for the currently active category (+ optional sub)
// Only computed for hi-five-franchise-store
leagues() {
if (!this.isLeagueStore) return [];
const map = {};
this.products.forEach(p => {
const { sport, item, league } = classify(p.name);
if (!league) return;
if (this.activeCategory && sport !== this.activeCategory) return;
if (this.activeSubCategory && item !== this.activeSubCategory) return;
map[league] = (map[league] || 0) + 1;
});
return Object.entries(map)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([name, count]) => ({ name, count }));
},
filtered() {
if (!this.activeCategory && !this.activeLeague) return this.products;
return this.products.filter(p => {
const { sport, item, league } = classify(p.name);
if (this.activeCategory && sport !== this.activeCategory) return false;
if (this.activeSubCategory && item !== this.activeSubCategory) return false;
if (this.isLeagueStore && this.activeLeague && league !== this.activeLeague) return false;
return true;
});
},
},
methods: {
selectCategory(cat) {
this.activeCategory = cat;
this.activeSubCategory = null;
this.activeLeague = null;
},
selectSub(cat, sub) {
this.activeCategory = cat;
this.activeSubCategory = sub;
this.activeLeague = null;
},
productUrl(p) {
return '/teamstore/' + store.url + '/product/' + p.url;
},
imgUrl(p) {
return store.minoBase + 'images/' + p.img;
},
},
}).mount('#ts-app');
})();
</script>
</div>{{-- /container --}}
@endsection

View File

@@ -7,6 +7,45 @@
@endif
<style>
body {
background: #f4f6f8;
}
.product-page {
padding-top: 14px;
padding-bottom: 28px;
}
.product-breadcrumb {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
margin-bottom: 18px;
padding: 10px 14px;
}
.product-breadcrumb .breadcrumb {
margin: 0;
background: transparent;
padding: 0;
}
.product-breadcrumb .breadcrumb > li,
.product-breadcrumb .breadcrumb > li.active,
.product-breadcrumb .breadcrumb > li + li:before {
color: #6b7280;
font-size: 12px;
}
.product-breadcrumb .breadcrumb > li > a {
color: #374151;
text-decoration: none;
}
.product-breadcrumb .breadcrumb > li > a:hover {
color: #111827;
}
p{
font-size: 12px;
margin-top: 5px;
@@ -30,7 +69,8 @@
box-shadow: 0px 0px 10px #4bc6ff;
} */
.line{
margin-bottom: 5px;
margin: 14px 0;
border-color: #e5e7eb;
}
@media screen and (max-width: 770px) {
.right{
@@ -117,18 +157,45 @@
border: none;
}
.carousel-control.left {
margin-left: -35px;
margin-top: 7px;
}
.carousel-control.right {
margin-right: -35px;
margin-top: 7px;
#myCarousel {
position: relative;
width: 100%;
}
.carousel-control {
width: 0%;
width: 34px;
height: 34px;
top: 50%;
margin-top: -17px;
opacity: 1;
text-shadow: none;
}
.carousel-control.left {
left: -18px;
margin-left: 0;
}
.carousel-control.right {
right: -18px;
margin-right: 0;
}
.carousel-control .glyphicon {
width: 34px;
height: 34px;
line-height: 34px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #d1d5db;
color: #374151;
font-size: 14px;
}
.carousel-control:hover .glyphicon {
background: #ffffff;
border-color: #9ca3af;
color: #111827;
}
.custom-chevron-left,
@@ -139,11 +206,127 @@
.hide-bullets {
list-style:none;
margin-left: -40px;
margin-top:20px;
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
padding-left: 0;
}
.hide-bullets > li {
margin-bottom: 12px;
}
.main-gallery-card,
.product-info-card,
.description-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.main-gallery-card {
padding: 16px;
}
.main-image-stage {
border: 1px solid #eef0f3;
border-radius: 10px;
background: #fcfcfd;
min-height: 430px;
display: flex;
align-items: center;
justify-content: center;
}
.main-product-image {
max-height: 400px;
width: auto;
max-width: 100%;
}
.thumbnail {
border-radius: 10px;
border: 1px solid #d1d5db;
background: #fff;
margin-bottom: 0;
transition: border-color 0.2s, box-shadow 0.2s;
}
.thumbnail:hover {
border-color: #9ca3af;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08);
}
.a_thumbnail.active {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.image-thumbnails {
border-radius: 8px;
object-fit: contain;
}
.product-info-card {
padding: 22px;
}
.product-title {
margin: 0;
font-size: 28px;
line-height: 1.2;
font-weight: 700;
color: #111827;
}
.price {
font-size: 30px;
margin: 8px 0 18px;
color: #0f172a;
font-weight: 700;
}
.price small {
font-size: 14px;
color: #6b7280;
font-weight: 500;
}
#frm-order-list {
padding-top: 2px;
}
#btn-add-to-cart {
min-height: 44px;
padding: 10px 18px;
border-radius: 9px;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.2px;
margin-top: 10px;
}
.description-card {
margin-top: 18px;
padding: 18px 22px;
}
.description-title {
margin: 0 0 10px;
font-size: 16px;
font-weight: 600;
color: #111827;
}
.description-text {
margin: 0;
font-size: 14px;
line-height: 1.65;
color: #4b5563;
}
.spacer-top{
margin-top: 40px;
margin-top: 24px;
}
.roster-input{
border-radius: 0px;
@@ -208,12 +391,49 @@
overflow: auto;
}
@media screen and (max-width: 991px) {
.product-info-card {
margin-top: 16px;
}
.main-image-stage {
min-height: 350px;
}
#btn-add-to-cart {
width: 100%;
float: none !important;
}
}
@media screen and (max-width: 768px) {
.main-image-stage {
min-height: 300px;
}
.carousel-control.left {
left: -10px;
}
.carousel-control.right {
right: -10px;
}
.product-title {
font-size: 23px;
}
.price {
font-size: 26px;
}
}
</style>
<div class="container">
<div class="container product-page">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<nav aria-label="breadcrumb" class="product-breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url('teamstore') }}/{{ $store_array[0]->StoreUrl }}">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ $product_array[0]->ProductName }}</li>
@@ -224,68 +444,62 @@
<div class="row">
<div class="col-md-5 col-sm-5">
<div class="row">
<div class="col-sm-12" id="carousel-bounding-box">
<div class="carousel slide" id="myCarousel" data-interval="false">
<!-- Carousel items -->
<div class="carousel-inner">
@define $i = 0
@foreach($thumbnails_array as $thumbnail)
@if($thumbnail->ImageClass == 'active')
<div class="active item text-center" data-slide-number="{{ $i }}">
<span class="zoom img-zoom">
<img style="height:400px;" src="{{ minio_url('images/' . $thumbnail->Image) }}">
</span>
</div>
@else
<div class="item text-center" data-slide-number="{{ $i }}">
<span class="zoom img-zoom">
<img style="height:400px;" src="{{ minio_url('images/' . $thumbnail->Image) }}">
</span>
</div>
@endif
@define $i++
@endforeach
</div>
<div class="main-gallery-card">
<div id="carousel-bounding-box">
<div class="main-image-stage">
<div class="carousel slide" id="myCarousel" data-interval="false">
<div class="carousel-inner">
@define $i = 0
@foreach($thumbnails_array as $thumbnail)
@if($thumbnail->ImageClass == 'active')
<div class="active item text-center" data-slide-number="{{ $i }}">
<span class="zoom img-zoom">
<img class="main-product-image" src="{{ minio_url('images/' . $thumbnail->Image) }}">
</span>
</div>
@else
<div class="item text-center" data-slide-number="{{ $i }}">
<span class="zoom img-zoom">
<img class="main-product-image" src="{{ minio_url('images/' . $thumbnail->Image) }}">
</span>
</div>
@endif
@define $i++
@endforeach
</div>
<!-- Carousel nav -->
<a class="left carousel-control" href="#myCarousel" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
<a class="right carousel-control" href="#myCarousel" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
<a class="left carousel-control" href="#myCarousel" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
<a class="right carousel-control" href="#myCarousel" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
</div>
</div>
</div>
<!-- </div> -->
</div>
<hr class="line">
<hr class="line">
<div class="row">
<div class="col-md-12">
<ul class="hide-bullets">
@define $j = 0
@foreach($thumbnails_array as $thumbnail)
<li class="col-sm-3 col-xs-3">
<a class="thumbnail a_thumbnail {{ $thumbnail->ImageClass }}" id="carousel-selector-{{ $j }}">
<img class="img img-responsive product-center image-thumbnails" style="height: 59.45px;" src="{{ minio_url('images/' . $thumbnail->Image) }}"/>
</a>
</li>
@define $j++
@endforeach
</ul>
</div>
<ul class="hide-bullets row">
@define $j = 0
@foreach($thumbnails_array as $thumbnail)
<li class="col-sm-3 col-xs-3">
<a class="thumbnail a_thumbnail {{ $thumbnail->ImageClass }}" id="carousel-selector-{{ $j }}">
<img class="img img-responsive product-center image-thumbnails" style="height: 59.45px;" src="{{ minio_url('images/' . $thumbnail->Image) }}"/>
</a>
</li>
@define $j++
@endforeach
</ul>
</div>
</div>
<div class="col-md-7 col-sm-7">
<div class="panel panel-default">
<div class="panel-heading">
<h1>{{ $product_array[0]->ProductName }}</h1> <p class="price">{{ $product_array[0]->ProductPrice }} <small> {{ $store_array[0]->StoreCurrency }} </small></p>
</div>
<div class="panel-body">
<div class="product-info-card">
<h1 class="product-title">{{ $product_array[0]->ProductName }}</h1>
<p class="price">{{ $product_array[0]->ProductPrice }} <small> {{ $store_array[0]->StoreCurrency }} </small></p>
<div class="row">
<div class="col-md-12">
<form id="frm-order-list">
@@ -300,19 +514,23 @@
</div>
</div>
<div class="spacer-top"></div>
<div class="row">
<div class="col-md-12">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#productDescription" aria-controls="productDescription" role="tab" data-toggle="tab">Description</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="productDescription">
<div class="row">
<div class="col-md-12">
<p>{{ $product_array[0]->ProductDescription }}</p>
</div>
<div class="description-card">
<h3 class="description-title">Description</h3>
<p class="description-text">{{ $product_array[0]->ProductDescription }}</p>
</div>
<div class="spacer-top"></div>
<div class="row">
<div class="col-md-12">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#productDescription" aria-controls="productDescription" role="tab" data-toggle="tab">More Details</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="productDescription">
<div class="row">
<div class="col-md-12">
<p>{{ $product_array[0]->ProductDescription }}</p>
</div>
</div>
</div>

View File

@@ -2,108 +2,237 @@
@section('content')
<style>
a.thumbnail>img {
/* height: 250px; */
body {
background: #f4f6f8;
}
.hide-bullets {
list-style:none;
margin-left: -40px;
margin-top:20px;
position: relative;
.store-page {
padding-top: 14px;
padding-bottom: 26px;
}
.thumbnail{
border: none;
display: unset;
background-color: transparent;
.store-header {
margin-bottom: 14px;
}
.li-custom{
padding:10px;
.store-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #111827;
}
.store-logo{
/* height: 250px;
width: 250px;
.store-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: #6b7280;
}
.store-toolbar {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
padding: 14px;
margin-bottom: 18px;
}
.store-toolbar label {
font-size: 12px;
color: #4b5563;
font-weight: 600;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.store-toolbar .form-control,
.store-toolbar .btn {
height: 40px;
border-radius: 8px;
}
.store-toolbar .form-control {
border-color: #d1d5db;
box-shadow: none;
}
.store-toolbar .form-control:focus {
border-color: #9ca3af;
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.18);
}
.store-grid {
list-style: none;
padding-left: 0;
margin: 0 -10px;
}
.store-grid > li {
padding: 10px;
}
.store-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
overflow: hidden;
object-fit: contain; */
/* cursor: pointer; */
height: 100%;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
a.thumbnail>img{
height: 150px
.store-card:hover {
transform: translateY(-2px);
border-color: #d1d5db;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.store-link {
display: block;
text-decoration: none;
}
.store-link:hover,
.store-link:focus {
text-decoration: none;
}
.store-logo-wrap {
height: 170px;
background: #f8fafc;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
padding: 14px;
}
.store-logo {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
}
.store-meta {
padding: 12px;
min-height: 72px;
display: flex;
align-items: center;
justify-content: center;
}
.store-name {
margin: 0;
font-size: 15px;
line-height: 1.35;
font-weight: 700;
color: #111827;
text-align: center;
text-transform: uppercase;
word-break: break-word;
}
.store-lock {
color: #6b7280;
margin-left: 4px;
}
.store-pagination {
margin-top: 18px;
}
.store-pagination .pagination {
margin: 0;
}
@media (max-width: 767px) {
.store-title {
font-size: 21px;
}
.store-toolbar {
padding: 12px;
}
.store-logo-wrap {
height: 155px;
}
}
</style>
<div class="container">
<div class="row">
<div class="container store-page">
<div class="row store-header">
<div class="col-lg-12">
<h2 style="font-size: 20px; font-weight: bold; ">TEAM STORES</h2>
<hr>
<h2 class="store-title">Team Stores</h2>
<p class="store-subtitle">Browse and open your team store.</p>
</div>
</div><!-- /row -->
<div class="row">
<!-- <div class="col-sm-12"> -->
<!-- <div class="well"> -->
<form class="form-horizontal" role="search" id="frm_search_store">
<div class="col-lg-7">
<div class="form-group">
<div class="col-sm-12">
<label>Seach Store</label>
<div class="input-group">
<input type="text" class="form-control" placeholder="Search Store" value="{{ $keyword }}" name="q">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="form-group">
<label class="col-sm-7 control-label hidden-xs">&nbsp;</label>
<div class="col-sm-5">
<label>Sort by:</label>
<select class="form-control" name="s" id="select_sort_stores">
<option @if($filter == "al-asc") selected @endif value="al-asc">Store Name A &rarr; Z</option>
<option @if($filter == "al-desc") selected @endif value="al-desc">Store Name Z &rarr; A</option>
<option @if($filter == "latest") selected @endif value="latest">Newest &rarr; Oldest</option>
<option @if($filter == "oldest") selected @endif value="oldest">Oldest &rarr; Newest</option>
</select>
</div>
</div>
</div>
</form>
<div class="clearfix"></div>
<!-- </div> -->
<!-- </div> -->
</div>
<div class="row" id="slider-thumbs">
<!-- Bottom switcher of slider -->
<ul class="hide-bullets">
@foreach ($stores_array as $store)
<li class="li-custom col-lg-3 col-md-3 col-sm-4 col-xs-12">
<div style="border: 1px solid #dddddd; padding: 5px;">
@if($store->Password != null )
<a class="thumbnail password-protected" href="#" data-store-id="{{ $store->Id }}" data-store-url="{{ $store->StoreUrl }}">
<img class="store-logo" src="{{ minio_url('uploads/images/teamstore/' . $store->ImageFolder . '/' . $store->StoreLogo) }}">
</a>
<h4 style="border-top: 1px solid #dddddd; padding: 10px; font-size: 16px; font-weight: bold; text-transform: uppercase;" class="text-center">{{ $store->StoreName }} <i class="fa fa-lock" title="This store is password protected."></i></h4>
@else
<a class="thumbnail" href="{{ url('teamstore') . '/' . $store->StoreUrl }}">
<img class="store-logo" src="{{ minio_url('uploads/images/teamstore/' . $store->ImageFolder . '/' . $store->StoreLogo) }}">
</a>
<h4 style="border-top: 1px solid #dddddd; padding: 10px; font-size: 16px; font-weight: bold; text-transform: uppercase;" class="text-center">{{ $store->StoreName }}</h4>
@endif
<div class="store-toolbar">
<form role="search" id="frm_search_store">
<div class="row">
<div class="col-md-8 col-sm-7">
<label>Search Store</label>
<div class="input-group">
<input type="text" class="form-control" placeholder="Search store" value="{{ $keyword }}" name="q">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</li>
<div class="col-md-4 col-sm-5">
<label>Sort by</label>
<select class="form-control" name="s" id="select_sort_stores">
<option @if($filter == "al-asc") selected @endif value="al-asc">Store Name A &rarr; Z</option>
<option @if($filter == "al-desc") selected @endif value="al-desc">Store Name Z &rarr; A</option>
<option @if($filter == "latest") selected @endif value="latest">Newest &rarr; Oldest</option>
<option @if($filter == "oldest") selected @endif value="oldest">Oldest &rarr; Newest</option>
</select>
</div>
</div>
</form>
</div>
<div id="slider-thumbs">
<ul class="store-grid row">
@foreach ($stores_array as $store)
<li class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
<div class="store-card">
@if($store->Password != null)
<a class="store-link password-protected" href="#" data-store-id="{{ $store->Id }}" data-store-url="{{ $store->StoreUrl }}">
<div class="store-logo-wrap">
<img class="store-logo" src="{{ minio_url('uploads/images/teamstore/' . $store->ImageFolder . '/' . $store->StoreLogo) }}">
</div>
</a>
<div class="store-meta">
<h4 class="store-name">{{ $store->StoreName }} <i class="fa fa-lock store-lock" title="This store is password protected."></i></h4>
</div>
@else
<a class="store-link" href="{{ url('teamstore') . '/' . $store->StoreUrl }}">
<div class="store-logo-wrap">
<img class="store-logo" src="{{ minio_url('uploads/images/teamstore/' . $store->ImageFolder . '/' . $store->StoreLogo) }}">
</div>
<div class="store-meta">
<h4 class="store-name">{{ $store->StoreName }}</h4>
</div>
</a>
@endif
</div>
</li>
@endforeach
</ul>
</div>
<div class="row">
<div class="row store-pagination">
<div class="col-sm-12">
<div class="text-center">
{!! $stores_array->render() !!}
</div>
</div>
</div>
</div><!-- /container -->
</div>
<div id="team-store-login" class="modal fade" role="dialog">