diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dec0e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +*.swp +*.swo diff --git a/Plugin.php b/Plugin.php new file mode 100644 index 0000000..8106bfc --- /dev/null +++ b/Plugin.php @@ -0,0 +1,116 @@ +alias('Cart', '\Gloudemans\Shoppingcart\Facades\Cart'); + } + + /** + * Returns information about this plugin. + * + * @return array + */ + public function pluginDetails() + { + return [ + 'name' => 'Shop', + 'description' => 'No description provided yet...', + 'author' => 'Dave Shoreman', + 'icon' => 'icon-shopping-cart' + ]; + } + + public function registerComponents() + { + return [ + 'DShoreman\Shop\Components\Basket' => 'shopBasket', + 'DShoreman\Shop\Components\Categories' => 'shopCategories', + 'DShoreman\Shop\Components\Product' => 'shopProduct', + 'DShoreman\Shop\Components\Products' => 'shopProducts', + ]; + } + + public function registerNavigation() + { + return [ + 'shop' => [ + 'label' => 'Shop', + 'url' => Backend::url('dshoreman/shop/products'), + 'icon' => 'icon-shopping-cart', + 'permissions' => ['dshoreman.shop.*'], + 'order' => 300, + + 'sideMenu' => [ + 'products' => [ + 'label' => 'Products', + 'url' => Backend::url('dshoreman/shop/products'), + 'icon' => 'icon-gift', + 'permissions' => ['dshoreman.shop.access_products'] + ], + 'categories' => [ + 'label' => 'Categories', + 'url' => Backend::url('dshoreman/shop/categories'), + 'icon' => 'icon-list-ul', + 'permissions' => ['dshoreman.shop.access_categories'], + ], + 'orders' => [ + 'label' => 'Orders', + 'url' => Backend::url('dshoreman/shop/orders'), + 'icon' => 'icon-gbp', + 'permissions' => ['dshoreman.shop.access_orders'], + ], + ], + ], + ]; + } + + public function registerFormWidgets() + { + return [ + 'Dshoreman\Shop\FormWidgets\ItemGrid' => [ + 'label' => 'Order Item Grid', + 'alias' => 'itemgrid', + ], + ]; + } + + public function registerPermissions() + { + return [ + 'dshoreman.shop.access_products' => ['label' => "Manage the shop's products"], + 'dshoreman.shop.access_categories' => ['label' => "Manage the shop categories"], + 'dshoreman.shop.access_orders' => ['label' => "Manage the shop orders"], + ]; + } + + public function registerSettings() + { + return [ + 'settings' => [ + 'label' => 'Stripe Settings', + 'description' => 'Manage your Stripe API keys.', + 'category' => 'Shop', + 'icon' => 'icon-credit-card', + 'class' => 'Dshoreman\Shop\Models\Settings', + 'order' => 200, + ], + ]; + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a4371e --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Octoshop - A Shop Plugin for October CMS + +Pretty much does what it says on the tin. Octoshop adds all the basic functionality +needed to sell products from your website running on October CMS. Still in its infancy, +it's fairly barebones right now, though the process from browsing to buying is already +covered. Payments are powered by Stripe JS for now, letting me spend more time worrying +about the things that matter most. + + +## Demo + +Frontend: http://octoshop.demo.dsdev.io/ +Backend: http://octoshop.demo.dsdev.io/backend/ + +Username and password for the backend are both `admin`. + + +## Documentation + +Not yet... Check out the demo, browse the source, and have a gander at the optional +[theme](https://github.com/dshoreman/octoshop-theme) if you want to play about. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/components/Basket.php b/components/Basket.php new file mode 100644 index 0000000..1e8ef49 --- /dev/null +++ b/components/Basket.php @@ -0,0 +1,204 @@ + 'Basket Component', + 'description' => 'No description provided yet...' + ]; + } + + public function defineProperties() + { + return [ + 'paymentPage' => [ + 'title' => 'Checkout Page', + 'description' => 'Name of the page to redirect to when a user clicks Proceed to Checkout.', + 'default' => 'checkout/payment', + 'group' => 'Links', + ], + 'productPage' => [ + 'title' => 'Product Page', + 'description' => 'Name of the product page for the product titles. This property is used by the default component partial.', + 'type' => 'dropdown', + 'default' => 'shop/product', + 'group' => 'Links', + ], + 'basketComponent' => [ + 'title' => 'Basket Component', + 'description' => 'Component to use when adding products to basket', + 'default' => 'shopBasket', + ], + 'basketPartial' => [ + 'title' => 'Basket Partial', + 'description' => 'Partial to use when adding products to basket', + 'default' => 'shopBasket::default', + ], + 'tableClass' => [ + 'title' => 'Table', + 'group' => 'CSS Classes', + ], + 'nameColClass' => [ + 'title' => 'Name Column', + 'group' => 'CSS Classes', + ], + 'qtyColClass' => [ + 'title' => 'Quaantity Column', + 'group' => 'CSS Classes', + ], + 'priceColClass' => [ + 'title' => 'Price Column', + 'group' => 'CSS Classes', + ], + 'subtotalColClass' => [ + 'title' => 'Subtotal Column', + 'group' => 'CSS Classes', + ], + 'totalLabelClass' => [ + 'title' => 'Total Label', + 'group' => 'CSS Classes', + 'description' => 'Class given to the cell containing the "Total" label.', + ], + ]; + } + + public function getProductPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function onRun() + { + $this->prepareVars(); + } + + public function onRender() + { + $this->prepareVars('render'); + } + + public function prepareVars($on = 'run') + { + if ($on == 'run') { + $this->registerBasketInfo(); + $this->registerPages(); + + $this->basketComponent = $this->page['basketComponent'] = $this->property('basketComponent');; + $this->basketPartial = $this->page['basketPartial'] = $this->property('basketPartial');; + } + + if ($on == 'render') { + $this->registerClasses(); + } + + if (Session::has('orderId')) { + $this->orderId = $this->page['orderId'] = Session::get('orderId'); + } + } + + public function registerBasketInfo() + { + $this->basketCount = $this->page['basketCount'] = Cart::count(); + $this->basketItems = $this->page['basketItems'] = Cart::content(); + $this->basketTotal = $this->page['basketTotal'] = Cart::total(); + } + + public function registerPages() + { + $this->paymentPage = $this->page['paymentPage'] = $this->property('paymentPage'); + $this->productPage = $this->page['productPage'] = $this->property('productPage'); + } + + public function registerClasses() + { + $this->tableClass = $this->page['tableClass'] = $this->propertyOrParam('tableClass'); + $this->nameColClass = $this->page['nameColClass'] = $this->property('nameColClass'); + $this->qtyColClass = $this->page['qtyColClass'] = $this->property('qtyColClass'); + $this->priceColClass = $this->page['priceColClass'] = $this->property('priceColClass'); + $this->subtotalColClass = $this->page['subtotalColClass'] = $this->property('subtotalColClass'); + $this->totalLabelClass = $this->page['totalLabelClass'] = $this->property('totalLabelClass'); + } + + public function onAddProduct() + { + $id = post('id'); + $quantity = post('quantity') ?: 1; + + $product = ShopProduct::find($id); + + Cart::add($id, $product->title, $quantity, $product->price); + + $this->registerBasketInfo(); + } + + public function onRemoveProduct() + { + Cart::remove(post('row_id')); + + $this->registerBasketInfo(); + + return [ + 'total' => $this->basketTotal, + 'count' => $this->basketCount, + ]; + } + + public function onCheckout() + { + $this->prepareVars(); + + $this->stripMissingItems(); + + if ($this->items_removed > 0) { + + $removed_many = $this->items_removed > 1; + + Flash::error(sprintf( + "%d %s couldn't be found and %s removed automatically. Please checkout again.", + $this->items_removed, + ($removed_many ? 'item' : 'items'), + ($removed_many ? 'were' : 'was') + )); + + return Redirect::back(); + } + + $order = new ShopOrder; + $order->items = json_encode(Cart::content()->toArray()); + $order->total = Cart::total(); + $order->save(); + + Session::put('orderId', $order->id); + + return Redirect::to($this->paymentPage); + } + + protected function stripMissingItems() + { + $this->items_removed = 0; + + foreach (Cart::content() as $item) + { + if ( ! ShopProduct::find($item->id)) { + Cart::remove($item->rowid); + + $this->items_removed++; + } + } + + return; + } + +} diff --git a/components/Categories.php b/components/Categories.php new file mode 100644 index 0000000..cc74a8f --- /dev/null +++ b/components/Categories.php @@ -0,0 +1,87 @@ + 'Shop Category List', + 'description' => 'Displays a list of shop categories on the page.', + ]; + } + + public function defineProperties() + { + return [ + 'categoryPage' => [ + 'title' => 'Category page', + 'description' => 'Name of the page file to use for the category links. This property is used by the default component partial.', + 'type' => 'dropdown', + 'default' => 'shop/category', + 'group' => 'Links', + ], + 'showItemCount' => [ + 'title' => 'Show Item Count', + 'description' => 'Whether or not to add the number of child products next to category titles when generating the menu.', + 'type' => 'checkbox', + 'default' => '', + ], + 'itemCountClass' => [ + 'title' => 'Item Count class', + 'description' => 'The classes to apply to the optional product count, shown next to the category titles in menus.', + 'group' => 'Styles', + ], + 'listClass' => [ + 'title' => 'List class', + 'description' => 'The classes to apply to the