diff --git a/app/Models/Continent.php b/app/Models/Continent.php new file mode 100644 index 0000000..dcdb25b --- /dev/null +++ b/app/Models/Continent.php @@ -0,0 +1,27 @@ +orderBy('name') + ->get(); + } + + public function countries(): HasMany + { + return $this->hasMany(Country::class); + } + +} diff --git a/app/Models/Country.php b/app/Models/Country.php new file mode 100644 index 0000000..45b754f --- /dev/null +++ b/app/Models/Country.php @@ -0,0 +1,28 @@ +belongsTo(Continent::class); + } + + public function tours(): BelongsToMany + { + return $this->belongsToMany(Tour::class, 'tour_countries', 'country_id', 'tour_id'); + } +} diff --git a/app/Models/Tour.php b/app/Models/Tour.php new file mode 100644 index 0000000..3e6fb3b --- /dev/null +++ b/app/Models/Tour.php @@ -0,0 +1,17 @@ +belongsToMany(Country::class, 'tour_countries', 'tour_id', 'country_id'); + } +} diff --git a/app/Models/TourCountry.php b/app/Models/TourCountry.php new file mode 100644 index 0000000..2fe926d --- /dev/null +++ b/app/Models/TourCountry.php @@ -0,0 +1,12 @@ +id(); + $table->string('name'); + $table->string('internal_name')->unique(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('continents'); + } +}; diff --git a/database/migrations/2025_09_15_013414_create_countries_table.php b/database/migrations/2025_09_15_013414_create_countries_table.php new file mode 100644 index 0000000..a53fe02 --- /dev/null +++ b/database/migrations/2025_09_15_013414_create_countries_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('internal_name')->unique(); + $table->string('name'); + $table->string('country_code', 2)->unique(); + $table->foreignId('continent_id') + ->constrained('continents') + ->onDelete('restrict'); // or ->onDelete('cascade') if you prefer + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('countries'); + } +}; diff --git a/database/migrations/2025_09_15_014700_create_tours_table.php b/database/migrations/2025_09_15_014700_create_tours_table.php new file mode 100644 index 0000000..a6941ca --- /dev/null +++ b/database/migrations/2025_09_15_014700_create_tours_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('internal_name'); + $table->string('short_description'); + $table->integer('length'); + $table->integer('price'); + $table->string('level'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tours'); + } +}; diff --git a/database/migrations/2025_09_15_021144_create_tour_countries_table.php b/database/migrations/2025_09_15_021144_create_tour_countries_table.php new file mode 100644 index 0000000..995c0ae --- /dev/null +++ b/database/migrations/2025_09_15_021144_create_tour_countries_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('country_id')->constrained(); + $table->foreignId('tour_id')->constrained(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tour_countries'); + } +}; diff --git a/database/seeders/ContinentSeeder.php b/database/seeders/ContinentSeeder.php new file mode 100644 index 0000000..56e9e4e --- /dev/null +++ b/database/seeders/ContinentSeeder.php @@ -0,0 +1,24 @@ +insert([ + ['id' => 1, 'name' => 'Australia & Oceania', 'internal_name' => 'oceania'], + ['id' => 2, 'name' => 'Africa', 'internal_name' => 'africa'], + ['id' => 3, 'name' => 'Asia', 'internal_name' => 'asia'], + ['id' => 4, 'name' => 'Europe', 'internal_name' => 'europe'], + ['id' => 5, 'name' => 'North America', 'internal_name' => 'north_america'], + ['id' => 6, 'name' => 'South America', 'internal_name' => 'south_america'], + ['id' => 7, 'name' => 'Antarctica', 'internal_name' => 'antarctica'], + ]); + } + +} diff --git a/database/seeders/CountrySeeder.php b/database/seeders/CountrySeeder.php new file mode 100644 index 0000000..f137ed0 --- /dev/null +++ b/database/seeders/CountrySeeder.php @@ -0,0 +1,29 @@ +pluck('id', 'internal_name')->toArray(); + + \DB::table('countries')->insert([ + ['internal_name'=>'comoros','name'=>'Comoros','country_code'=>'km','continent_id'=>$map['africa']], + ['internal_name'=>'madagascar','name'=>'Madagascar','country_code'=>'mg','continent_id'=>$map['africa']], + ['internal_name'=>'suriname','name'=>'Suriname','country_code'=>'sn','continent_id'=>$map['south_america']], + ['internal_name'=>'mauritania','name'=>'Mauritania','country_code'=>'mr','continent_id'=>$map['africa']], + ['internal_name'=>'china','name'=>'China','country_code'=>'cn','continent_id'=>$map['asia']], + ['internal_name'=>'tajikistan','name'=>'Tajikistan','country_code'=>'tj','continent_id'=>$map['asia']], + ['internal_name'=>'gabon','name'=>'Gabon','country_code'=>'ga','continent_id'=>$map['africa']], + ]); + } + +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a121999..99b7514 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -14,9 +14,12 @@ class DatabaseSeeder extends Seeder public function run(): void { // User::factory(10)->create(); - $this->call(ContinentSeeder::class); - - $this->call(CountrySeeder::class); + $this + ->call(ContinentSeeder::class) + ->call(CountrySeeder::class) + ->call(TourSeeder::class) + ->call(TourCountrySeeder::class) + ; User::factory()->create([ 'name' => 'Test User', diff --git a/database/seeders/TourCountrySeeder.php b/database/seeders/TourCountrySeeder.php new file mode 100644 index 0000000..b2ad21c --- /dev/null +++ b/database/seeders/TourCountrySeeder.php @@ -0,0 +1,27 @@ +pluck('id', 'internal_name')->toArray(); + $countryMap= Country::all()->pluck('id', 'internal_name')->toArray(); + \DB::table('tour_countries')->insert([ + ['tour_id' => $tourMap['cantonese_charm'], 'country_id' => $countryMap['china']], + ['tour_id' => $tourMap['fujianese_fantasy'], 'country_id' => $countryMap['china']], + ['tour_id' => $tourMap['hebei_hijinx'], 'country_id' => $countryMap['china']], + ]); + + } +} diff --git a/database/seeders/TourSeeder.php b/database/seeders/TourSeeder.php new file mode 100644 index 0000000..f64b92e --- /dev/null +++ b/database/seeders/TourSeeder.php @@ -0,0 +1,23 @@ +insert([ + ['length' => 8, 'name' => "Cantonese Charm", 'short_description' => "Guangdong is known for it's big, global cities, but there is so much more to discover and pristine natural beauty.", 'internal_name' => "cantonese_charm", 'level' => "Beginner", 'price' => 1000], + ['length' => 7, 'name' => "Fujianese Fantasy", 'short_description' => "Experience fresh seafood in Xiamen, and then move rurally for an authentic dive into Hakka culture", 'internal_name' => "fujianese_fantasy", 'level' => "Beginner", 'price' => 1200], + ['length' => 10, 'name' => "Hebei Hijinx", 'short_description' => "The Great Wall, Great Food and ancient treasures in one of China's most underrated provinces.", 'internal_name' => "hebei_hijinx", 'level' => "Moderate", 'price' => 1500], + ]); + + + } +} diff --git a/public/img/hero.jpg b/public/img/hero.jpg new file mode 100644 index 0000000..84ca916 Binary files /dev/null and b/public/img/hero.jpg differ diff --git a/public/img/tours/cantonese_charm.jpg b/public/img/tours/cantonese_charm.jpg new file mode 100644 index 0000000..bf657ab Binary files /dev/null and b/public/img/tours/cantonese_charm.jpg differ diff --git a/public/img/tours/fujianese_fantasy.jpg b/public/img/tours/fujianese_fantasy.jpg new file mode 100644 index 0000000..3055dd3 Binary files /dev/null and b/public/img/tours/fujianese_fantasy.jpg differ diff --git a/public/img/tours/hebei_hijinx.jpg b/public/img/tours/hebei_hijinx.jpg new file mode 100644 index 0000000..c9aedcd Binary files /dev/null and b/public/img/tours/hebei_hijinx.jpg differ diff --git a/resources/js/components/dredgy/TourCard.vue b/resources/js/components/dredgy/TourCard.vue index 1da3a80..bd00b99 100644 --- a/resources/js/components/dredgy/TourCard.vue +++ b/resources/js/components/dredgy/TourCard.vue @@ -26,10 +26,10 @@ const props = defineProps<{ - {{tour.countryName}}, {{tour.continentName}} + {{ tour.countries.at(0)?.name }}, {{ tour.countries.at(0)?.continent.name }}

{{tour.title}}

-

{{tour.description}}

+

{{tour.short_description}}

diff --git a/resources/js/components/home/About.vue b/resources/js/components/home/About.vue index 0c93da3..4fa7e03 100644 --- a/resources/js/components/home/About.vue +++ b/resources/js/components/home/About.vue @@ -181,5 +181,6 @@ import EdgyButton from "@/components/dredgy/EdgyButton.vue"; import GradientText from "@/components/dredgy/GradientText.vue"; import SectionContainer from "@/components/dredgy/SectionContainer.vue"; import SectionTitle from "@/components/dredgy/SectionTitle.vue"; + diff --git a/resources/js/components/home/FeaturedTours.vue b/resources/js/components/home/FeaturedTours.vue index d08077a..71b6fd4 100644 --- a/resources/js/components/home/FeaturedTours.vue +++ b/resources/js/components/home/FeaturedTours.vue @@ -99,10 +99,12 @@ import SectionTitle from "@/components/dredgy/SectionTitle.vue"; import TourCard from "@/components/dredgy/TourCard.vue"; import { Tour } from "@/types"; import EdgyButton from "@/components/dredgy/EdgyButton.vue"; +const featuredTours : Tour[] = []; -const featuredTours : Tour[] = [ - {id: 1, length:8, title:"Cantonese Charm", countryId: 1, countryName: "China", continentId:1,continentName:"Asia",description:"Guangdong is known for it's big, global cities, but there is so much more to discover and pristine natural beauty.", internal_name:"cantonese_charm", level: "Beginner", price:1000}, - {id: 2, length: 7, title:"Fujian Fantasy", countryId: 1, countryName: "China", continentId:1,continentName:"Asia",description:"Experience fresh seafood in Xiamen, and then move rurally for an authentic dive into Hakka culture", internal_name:"fujian_fantasy", level: "Beginner", price:1200}, - {id: 3, length: 10, title:"Hebei Hijinx", countryId: 1, countryName: "China", continentId:1,continentName:"Asia",description:"The Great Wall, Great Food and ancient treasures in one of China's most underrated provinces.", internal_name:"hebei_hijinx", level: "Moderate", price:1500}, -] +/* +import { defineProps } from 'vue' + +const props = defineProps<{ + featuredTours: Tour[] +}>()*/ diff --git a/resources/js/composables/useIntersectionObserver.ts b/resources/js/composables/useIntersectionObserver.ts new file mode 100644 index 0000000..53e3c51 --- /dev/null +++ b/resources/js/composables/useIntersectionObserver.ts @@ -0,0 +1,38 @@ +import { onMounted, onBeforeUnmount, nextTick } from 'vue'; + +/** + * useIntersectionObserver + * + * @param selector - CSS selector for elements to observe + * @param callback - called when intersection changes + * @param options - IntersectionObserver options + */ +export const useIntersectionObserver = ( + selector: string, + callback: IntersectionObserverCallback, + options: IntersectionObserverInit = {} +) => { + let observer: IntersectionObserver | null = null; + + const defaultOptions: IntersectionObserverInit = { + threshold: 0.3, + rootMargin: '0px 0px -50px 0px', + }; + + onMounted(async () => { + await nextTick(); // wait for DOM to render + + const elements = document.querySelectorAll(selector); + if (!elements.length) return; + + observer = new IntersectionObserver(callback, { ...defaultOptions, ...options }); + elements.forEach(el => observer!.observe(el)); + }); + + onBeforeUnmount(() => { + if (observer) { + observer.disconnect(); + observer = null; + } + }); +}; diff --git a/resources/js/pages/Home.vue b/resources/js/pages/Home.vue index 3bfe136..6d992ee 100644 --- a/resources/js/pages/Home.vue +++ b/resources/js/pages/Home.vue @@ -1,9 +1,7 @@